Authentication with SSR

You should have noticed that we have removed most of the authentication logic from the server-side React code. The reason is that the localStorage cannot be transmitted to the server on the initial loading of a page, which is the only case where SSR can be used at all. This leads to the problem that we cannot render the correct route, because we cannot verify whether a user is logged in. The authentication has to be transitioned to cookies, which are sent with every request.

It is important to understand that cookies also introduce some security issues. We will continue to use the regular HTTP authorization header for the GraphQL API that we have written. If we use cookies for the GraphQL API, we will expose our application to potential CSRF attacks. The front end code continues to send all GraphQL requests with the HTTP authorization header.

We will only use the cookies to verify the authentication status of a user, and to initiate requests to our GraphQL API for server-side rendering of the React code. The SSR GraphQL requests will include the authorization cookie's value in the HTTP authorization header. Our GraphQL API only reads and verifies this header, and does not accept cookies. As long as you do not mutate data when loading a page and only query for data to render, there will be no security issues.

As the whole topic of CSRF and XSS is big, I recommend that you read up on it, in order to fully understand how to protect yourself and your users. You can find a great article at https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF).

The first thing to do is install a new package with npm, as follows:

npm install --save cookies

The cookies package allows us to easily interact through the Express request object with the cookies sent by the browser. Instead of parsing and reading through the cookie string (which is just a comma-separated list) manually, you can access the cookies with simple get and set methods. To get this package working, you have to initialize it inside Express.

Import the cookies and jwt packages, and also extract the JWT_SECRET from the environment variables at the top of the server index.js file:

import Cookies from 'cookies';
import JWT from 'jsonwebtoken';
const { JWT_SECRET } = process.env;

To use the cookies package, we are going to set up a new middleware route. Insert the following code before initializing the webpack modules and the services routine:

app.use(
(req, res, next) => {
const options = { keys: ['Some random keys'] };
req.cookies = new Cookies(req, res, options);
next();
}
);

This new Express middleware initializes the cookies package under the req.cookies property for every request that it processes. The first parameter of the Cookies constructor is the request, the second is the response object, and the last one is an options parameter. It takes an array of keys, with which the cookies are signed. The keys are required if you want to sign your cookies for security reasons. You should take care of this in a production environment. You can specify a secure property, which ensures that the cookies are only transmitted on secure HTTPS connections.

We can now extract the authorization cookie and verify the authentication of the user. To do this, replace the beginning of the SSR route with the following code in the server's index.js file:

app.get('*', async (req, res) => {
const token = req.cookies.get('authorization', { signed: true });
var loggedIn;
try {
await JWT.verify(token, JWT_SECRET);
loggedIn = true;
} catch(e) {
loggedIn = false;
}

Here, I have added the async declaration to the callback function, because we use the await statement inside it. The second step is to extract the authorization cookie from the request object with req.cookies.get. Importantly, we specify the signed field in the options parameter, because only then will it successfully return the signed cookies.

The extracted value represents the JWT that we generate when a user logs in. We can verify this with the typical approach that we implemented in Chapter 6, Authentication with Apollo and React. We use the await statement while verifying the JWT. If an error is thrown, the user is not logged in. The state is saved in the loggedIn variable. Pass the loggedIn variable to the Graphbook component, as follows:

const App = (<Graphbook client={client} loggedIn={loggedIn} location={req.url} context={context}/>);

Now, we can access the loggedIn property inside index.js from the ssr folder. Extract the loggedIn sate from the properties, and pass it to the App component in the ssr index.js file, as follows:

<App location={location} context={context} loggedIn={loggedIn}/>

Inside the App component, we do not need to set the loggedIn state directly to false, but we can take the property's value, because it is determined before the App class is rendered. This flow is different from the client procedure, where the loggedIn state is determined inside the App class. Change the App class in the app.js file in order to match the following code:

class App extends Component {
state = {
loggedIn: this.props.loggedIn
}

The result is that we pass down the loggedIn value from our Express.js route, over the Graphbook and App components, to our Router. It already accepts the loggedIn property, in order to render the correct path for the user. At the moment, we still do not set the cookie on the back end when a user successfully logs in.

Open the resolvers.js file of our GraphQL server to fix that. We will change a few lines for the login and signup functions. Both resolver functions need the same changes, as both set the authentication token after login or signup. Insert the following code directly above the return statement:

context.cookies.set(
'authorization',
token, { signed: true, expires: expirationDate, httpOnly: true,
secure: false, sameSite: 'strict' }
);

The preceding function sets the cookies for the user's browser. The context object is only the Express.js request object where we have initialized the cookies package. The properties of the cookies.set function are pretty self-explanatory, as follows:

  • The signed field specifies whether the keys entered during the initialization of the cookies object should be used to sign the cookie's value.
  • The expires property takes a date object. It represents the time until which the cookie is valid. You can set the property to whatever date you want, but I would recommend a short period, such as one day. Insert the following code above the context.cookies.set statement, in order to initialize the expirationDate variable correctly:
const cookieExpiration = 1;
var expirationDate = new Date();
expirationDate.setDate(
expirationDate.getDate() + cookieExpiration
);
  • The httpOnly field secures the cookie so that it is not accessible by client-side JavaScript.
  • The secure property has the same meaning as it did when initializing the Cookie package. It restricts cookies to SSL connections only. This is a must when going to production, but it cannot be used while developing, since most developers develop locally, without an SSL certificate.
  • The sameSite field takes either strict or lax as a value. I recommend setting it to strict, since you only want your GraphQL API or server to receive the cookie with every request, but to exclude all cross-site requests, as this could be dangerous.

Now, we should clean up our code. Since we are using cookies, we can remove the localStorage authentication flow in the front end code. Open the App.js of the client folder. Remove the componentWillMount method, as we are reading from the localStorage there.

The cookies are automatically sent with any request, and they do not need a separate binding, like the localStorage. That also means that we need a special logout mutation that removes the cookie from the browser. JavaScript is not able to access or remove the cookie, because we specified it as httpOnly. Only the server can delete it from the client.

Create a new logout.js inside the mutations folder, in order to create the new LogoutMutation class. The content should look as follows:

import React, { Component } from 'react';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag';

const LOGOUT = gql`
mutation logout {
logout {
success
}
}
`;

export default class LogoutMutation extends Component {
render() {
const { children } = this.props;
return (
<Mutation
mutation={LOGOUT}>
{(logout, { loading, error}) =>
React.Children.map(children, function(child){
return React.cloneElement(child, { logout, loading, error });
})
}
</Mutation>
)
}
}

The preceding mutation component only sends a simple logout mutation, without any parameters or further logic. We should use the LogoutMutation component inside the index.js file of the bar folder in order to send the GraphQL request. Import the component at the top of the file, as follows:

import LogoutMutation from '../mutations/logout';

The Logout component renders our current Log out button in the application bar. It removes the token and cache from the client upon clicking it. Use the LogoutMutation class as a wrapper for the Logout component, to pass the mutation function:

<LogoutMutation><Logout changeLoginState={this.props.changeLoginState}/></LogoutMutation>

Inside the bar folder, we have to edit the logout.js file, because we should make use of the logout mutation that this component receives from its parent LogoutMutation component. Replace the logout method with the following code, in order to send the mutation upon clicking the logout button:

logout = () => {
this.props.logout().then(() => {
localStorage.removeItem('jwt');
this.props.client.resetStore();
});
}

We have wrapped the original two functions inside the call to the parent logout mutation function. It sends the GraphQL request to our server.

To implement the mutation on the back end, add one line to the GraphQL RootMutation type, inside schema.js:

logout: Response @auth

It's required that the user that's trying to log out is authorized, so we use the @auth directive. The corresponding resolver function looks as follows. Add it to the resolvers.js file, in the RootMutation property:

logout(root, params, context) {
context.cookies.set(
'authorization',
'', { signed: true, expires: new Date(), httpOnly: true, secure:
false, sameSite: 'strict' }
);
return {
message: true
};
},

The resolver function is minimal. It removes the cookie by setting the expiration date to the current time. This removes the cookie on the client when the browser receives the response, because it is expired then. This behavior is an advantage, in comparison to the localStorage.

We have completed everything to make the authorization work with SSR. It is a very complex task, since authorization, server-side rendering, and client-side rendering have effects on the whole application. Every framework out there has its own approach to implementing this feature, so please take a look at them too.

If you look at the source code returned from our server after the rendering, you should see that the login form is returned correctly, like before. Furthermore, the server now recognizes whether the user is logged in. However, the server does not return the rendered news feed, the application bar, or the chats yet. Only a loading message is included in the returned HTML. The client-side code also does not recognize that the user is logged in. We will take a look at these problems in the next section.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.181.52