Headless WordPress: Cookie Based Login using GraphQL

For a fun learning & experimentation project, I’ve been building a Dungeons and Dragons character generator. The frontend is built using React JS and deployed to Netlify, and the backend is powered by WordPress and WP GraphQL.

I could have tried using a serverless database such as FaunaDB, but I chose WordPress because of familiarity, what it gives you for free, and it’s flexibility when combined with WP GraphQL. I did however need to build a login system. To do this I created a GraphQL login endpoint, and handled user sessions using Cookies.

Cookies vs JWT

JWT (JSON Web Tokens) can be used to authenticate requests between parties. There is a decent overview of how JWT works here.

A plugin is available for WP GraphQL which handles authentication using JWT and adds a login endpoint. This looked promising, however, the most difficult thing about using JWT in the context of a web based React app is storage of the token. Whilst some articles recommend using localStorage, doing so is not secure—the token could be leaked using XSS.

Don’t store it in local storage (or session storage). If any of the third-party scripts you include in your page gets compromised, it can access all your users’ tokens.

JWT Authentication: When and how to use it

To keep our tokens secure our only option would be to store them in memory. The session would not persist if the browser tab was closed, making it less than ideal.

A better option, and the solution I went with, is using good old fashioned cookies (specifically HTTP Cookies which are inaccessible by JavaScript). I created a new GraphQL route that would set a HTTP cookie after successful login.

Side note: I did find a plugin for WP GraphQL which supports WP cookies, but I encountered browser compatibility (CORS) issues, so ended up rolling my own based on that.

Register a GraphQL Login Mutation

In order to login, we need to register a new endpoint with WP GraphQL. We do this by hooking in to graphql_register_types and adding our new mutation using the register_graphql_mutation function documented here.

We’ll call our mutation loginWithCookies and we’ll use inputFields to define which fields are accepted (username and password), outputFields to define the response, and mutateAndGetPayload to perform the login action:

add_action( 'graphql_register_types', function() {
	register_graphql_mutation(
		'loginWithCookies',
		array(
			'inputFields' => array(
				'login'      => array(
					'type'        => array( 'non_null' => 'String' ),
					'description' => __( 'Input your username/email.' ),
				),
				'password'   => array(
					'type'        => array( 'non_null' => 'String' ),
					'description' => __( 'Input your password.' ),
				),
			),
			'outputFields'        => array(
				'status' => array(
					'type'        => 'String',
					'description' => 'Login operation status',
					'resolve'     => function( $payload ) {
						return $payload['status'];
					},
				),
			),
			'mutateAndGetPayload' => function( $input ) {
				$user = wp_signon( 
					array(
						'user_login'    => wp_unslash( $input['login'] ),
						'user_password' => $input['password'],
					), 
					true 
				);

				if ( is_wp_error( $user ) ) {
					throw new \GraphQL\Error\UserError\UserError( ! empty( $user->get_error_code() ) ? $user->get_error_code() : 'invalid login' );
				}

				return array( 'status' => 'SUCCESS' );
			},
		)
	);
} );Code language: PHP (php)

We can use this mutation using a GraphQL query. You can view my usage of the login mutation in the React app here.

Setting the logged-in cookie

When you log in, WordPress sets cookies to track your session. These cookies have a httponly parameter so it is not readable through JavaScript.

I found the easiest way to give the headless site access to cookies was to set an additional cookie. After reviewing this, it doesn’t appear to be needed—previously I had issues with Safari but this no longer seems to be a problem. The regular WordPress login cookie is enough.

It doesn’t work yet though, if we try to login we’ll see a CORS error in the browser:

A CORS error in the network inspector

We need to send some additional headers so the browser does not reject the cookie!

Setting CORS Headers

The final piece of the puzzle is telling the browser it should accept the custom cookie when logging in from the headless site. We can define headers for our GraphQL routes using the graphql_response_headers_to_send hook.

add_filter( 'graphql_response_headers_to_send', function( $headers ) {
	$http_origin     = get_http_origin();
	$allowed_origins = [
		HEADLESS_FRONTEND_URL,
	];

	// If the request is coming from an allowed origin (HEADLESS_FRONTEND_URL), tell the browser it can accept the response.
	if ( in_array( $http_origin, $allowed_origins, true ) ) {
		$headers['Access-Control-Allow-Origin'] = $http_origin;
	}

	// Tells browsers to expose the response to frontend JavaScript code when the request credentials mode is "include".
	$headers['Access-Control-Allow-Credentials'] = 'true';
	
	return $headers;
}, 20 );Code language: PHP (php)

A detailed explanation of CORS can be found in MDN, as well as docs for allow credentials and allow origin.

Wrapping up

We’ve now created a GraphQL mutation which accepts login credentials and logs users in using a custom cookie, and we’ve set CORS headers so the browser allows the cookies to be used. Great! We can now hook this up to a login form in the React app.

  1. Headless WordPress: Cookie Based Login using GraphQL
  2. Headless WordPress: Log-out using GraphQL & ReactJS
  3. Headless WordPress: Password Reset with ReactJS & WPGraphQL
  4. Headless WordPress: User Registration with ReactJS & WPGraphQL

Posted

in

by

%d bloggers like this: