Show Navigation

Creating a React app with Spring Security

Learn how to add Spring Security to your React app

Authors: Ben Rhine, Zachary Klein

Grails Version: 3.3.2

1 Grails Training

Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.

2 Getting Started

In this guide you will learn how to secure your React/Grails application with Spring Security REST (using the default JWT Token authentication).

In many React apps, libraries like React Router are used to handle client-side routing, including login/logout redirects. In this guide we will not use any dedicated routing solution, in order to focus the learning experience on the authentication functionality.

2.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

2.2 How to complete the guide

To get started do the following:

or

The Grails guides repositories contain two folders:

  • initial Initial project. Often a simple Grails app with some additional code to give you a head-start.

  • complete A completed example. It is the result of working through the steps presented by the guide and applying those changes to the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-guides/react-spring-security/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/react-spring-security/complete

3 Writing the Application

For this guide, you should use the initial project included in the guide’s repo to get started. This project contains a working Grails/React app which provides basic CRUD functionality and a simple (unsecured) RESTful API. Feel free to start up the app and play around with it, and look at the existing code.

4 Running the Application

The app in this guide uses the react profile, which provides a multiproject client/server build. This means you must start both the server (Grails) and client (React) apps independently.

~ cd initial/

To launch the server application, run the following command.

~ ./gradlew server:bootRun

This will start up the Grails application, which will be running on http://localhost:8080

To start the client app, open a second terminal session (in the same directory), and run the following command:

~ ./gradlew client:start

The React app will be available at http://localhost:3000. Browse to that URL and you should see the home page of the app.

5 Building the Server

The initial project is based on the completed project from the Building a React App Guide. Please refer to that guide for details on the provided code.

Our first step in adding security to this project is to install the Spring Security plugin/s in our Grails app, and secure our API endpoints. The Spring Security REST plugin supports a stateless, token-based authentication model that is ideal for securing APIs and Single Page Applications. See the diagram below for an overview of the security model.

Stateless Authentication Model

5.1 Installing Spring Security

To secure our application, we will use the Spring Security Core plugin as well as the Spring Security REST plugin. Install these plugins in the project by adding these lines to server/build.gradle (under the dependencies section):

compile "org.grails.plugins:spring-security-core:3.2.0"
compile "org.grails.plugins:spring-security-rest:2.0.0.M2"
Please see the documentation for more information on the Spring Security Core plugin and Spring Security REST plugin.

5.2 Configuring Spring Security

Now that Spring Security has been added to our application, we can generate the default Spring Security configuration. The plugin provides a s2-quickstart command that will generate a set of domain classes and configuration to get us started.

cd initial/server

Then execute the following

grails s2-quickstart demo User Role

This should generate the following domain classes:

/server/grails-app/domain/demo/User.groovy
/server/grails-app/domain/demo/Role.groovy
/server/grails-app/domain/demo/UserRole.groovy

In addition, the s2-quickstart has added an extra configuration file at server/grails-app/conf/application.groovy. Grails projects support both YML and Groovy configuration, but YML is preferred. Let’s remove the application.groovy file and add the following snippet at the end of application.yml

---
grails:
    plugin:
        springsecurity:
            userLookup:
                userDomainClassName: demo.User
                authorityJoinClassName: demo.UserRole
            authority:
                className: demo.Role
            filterChain:
                chainMap:
                    -
                        pattern: /assets/**
                        filters: none
                    -
                        pattern: /**/js/**
                        filters: none
                    -
                        pattern: /**/css/**
                        filters: none
                    -
                        pattern: /**/images/**
                        filters: none
                    - # Stateless chain
                        pattern: /api/**
                        filters: JOINED_FILTERS,-anonymousAuthenticationFilter,-exceptionTranslationFilter,-authenticationProcessingFilter,-securityContextPersistenceFilter,-rememberMeAuthenticationFilter
                    - # Traditional Chain
                        pattern: /**
                        filters: JOINED_FILTERS,-restTokenValidationFilter,-restExceptionTranslationFilter
                controllerAnnotations:
                    staticRules:
                        -
                            pattern: /
                            access:
                                - permitAll
                        -
                            pattern: /error
                            access:
                                - permitAll
                        -
                            pattern: /index
                            access:
                                - permitAll
                        -
                            pattern: /index.gsp
                            access:
                                - permitAll
                        -
                            pattern: /shutdown
                            access:
                                - permitAll
                        -
                            pattern: /assets/**
                            access:
                                - permitAll
                        -
                            pattern: /**/js/**
                            access:
                                - permitAll
                        -
                            pattern: /**/css/**
                            access:
                                - permitAll
                        -
                            pattern: /**/images/**
                            access:
                                - permitAll
                        -
                            pattern: /**/favicon.ico/**
                            access:
                                - permitAll

5.3 Securing our API

Our first step in securing our API is to update our Driver Domain Class. In our app, Driver would be a "user", however Spring Security has generated a User class for authentication purposes. We could modify the configuration to use Driver instead, however we’ll take another approach and make Driver a subclass of User.

Edit server/grails-app/domain/demo/Driver.groovy:

server/grails-app/domain/demo/Driver.groovy
package demo

import grails.compiler.GrailsCompileStatic
import grails.plugin.springsecurity.annotation.Secured
import grails.rest.Resource

@GrailsCompileStatic
@Secured(['ROLE_DRIVER'])
@Resource(uri = '/api/driver')
class Driver extends User {

    String name

    static hasMany = [ vehicles: Vehicle ]

    static constraints = {
        vehicles nullable: true
    }
}

In addition to extending the User class, we have also restricted access to this domain resource to users with the ROLE_DRIVER role, using the @Secured annotation. We haven’t created this role yet, but we’ll fix that shortly.

If you were now to run the server application, you would get a 401 response to any attempt to access /api/driver.

Let’s secure the remaining domain resources, as shown below:

server/grails-app/domain/demo/Make.groovy
package demo

import grails.plugin.springsecurity.annotation.Secured
import grails.rest.Resource
import groovy.transform.CompileStatic

@CompileStatic
@Secured(['ROLE_DRIVER'])
@Resource(uri = '/api/make')
class Make {

    String name

    static constraints = {
    }
}
server/grails-app/domain/demo/Model.groovy
package demo

import grails.plugin.springsecurity.annotation.Secured
import grails.rest.Resource
import groovy.transform.CompileStatic

@CompileStatic
@Secured(['ROLE_DRIVER'])
@Resource(uri = '/api/model')
class Model {

    String name

    static constraints = {
    }
}
server/grails-app/domain/demo/Vehicle.groovy
package demo

import grails.plugin.springsecurity.annotation.Secured
import grails.rest.Resource
import groovy.transform.CompileStatic

@CompileStatic
@Secured(['ROLE_DRIVER'])
@Resource(uri = '/api/vehicle')
class Vehicle {

    String name

    Make make
    Model model

    static belongsTo = [driver: Driver]

    static constraints = {
    }
}

5.4 Updating our Initial Data

Believe it or not, we’re done with the server-side security portion of our application! The last thing we need to do is to create the ROLE_DRIVER role that we mentioned earlier. We also should prepopulate our database with some user/passwords that we can use to login.

Edit server/grails-app/init/demo/BootStrap.groovy:

server/grails-app/init/demo/BootStrap.groovy
@Slf4j
class BootStrap {

    def init = { servletContext ->
        log.info "Loading database..."
        def driver1 = new Driver(name: "Susan", username: "susan", password: "password1").save() (1)
        def driver2 = new Driver(name: "Pedro", username:  "pedro", password: "password2").save()

        Role role = new Role(authority: "ROLE_DRIVER").save()  (2)

        UserRole.create(driver1, role, true)  (3)
        UserRole.create(driver2, role, true)

        def nissan = new Make(name: "Nissan").save()
        def ford = new Make(name: "Ford").save()

        def titan = new Model(name: "Titan").save()
        def leaf = new Model(name: "Leaf").save()
        def windstar = new Model(name: "Windstar").save()

        new Vehicle(name: "Pickup", driver: driver1, make: nissan, model: titan).save()
1 Since we’ve extended the User class, we can now set a username and password property on our Driver domain objects. The password will be encrypted prior to persistence.
2 Here we’re creating our ROLE_DRIVER role - we can create as many roles as we need, and even create role hierarchies to support complex access controls.
3 UserRole represents the join table between User and Role. The class includes a create method which we can use to quickly associate our User and Role objects.

5.5 Test-driving our Secured API

Start up the server app (if it’s not already running):

~ ./gradlew server:bootRun

In another terminal session, if you make a curl request to one of our resources, you’ll get a 401 response.

~ curl -i 0:8080/api/vehicle
HTTP/1.1 401
WWW-Authenticate: Bearer
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 08 Jun 2017 05:42:05 GMT

{"timestamp":1496900525475,"status":401,"error":"Unauthorized","message":"No message available","path":"/api/vehicle"}

In order to make a secure request, we need to first authenticate with the server using valid user credentials. By default, we can use the /api/login endpoint for this purpose.

Make a request to /api/login using the credentials for one of our Driver objects in BootStrap.groovy:

curl -i -H "Content-Type: application/json" --data '{"username":"susan","password":"password1"}' 0:8080/api/login
HTTP/1.1 200
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/json;charset=UTF-8
Content-Length: 2157
Date: Thu, 08 Jun 2017 05:45:44 GMT

{"username":"susan","roles":["ROLE_DRIVER"],"token_type":"Bearer","access_token":"eyJhbGciOiJIUzI1NiJ9...","expires_in":3600,"refresh_token":"eyJhbGciOiJIUzI1NiJ9..."}

Notice that in the response, in addition to user details/granted roles, we received an access_token - this is what we need to provide to our server in order to authenticate our request. We do this by setting the Authorization header with our token. Let’s try our /api/vehicle request again, using this access_token:

curl -i -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..." 0:8080/api/vehicle
HTTP/1.1 200
X-Application-Context: application:development
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 08 Jun 2017 05:49:01 GMT

[{"id":1,"name":"Pickup","make":{"name":"Nissan","id":1},"model":{"name":"Titan","id":1},"driver":{"name":"Susan","id":1}},{"id":2,"name":"Economy","make":{"name":"Nissan","id":1},"model":{"name":"Leaf","id":2},"driver":{"name":"Susan","id":1}},{"id":3,"name":"Minivan","make":{"name":"Ford","id":2},"model":{"name":"Windstar","id":3},"driver":{"name":"Pedro","id":2}}]

Congratulations, your API is now secured! Now we can move on to supporting authentication in our React client app.

Notice there’s actually another token in the /api/login response: refresh_token. When the access_token expires, you can use the refresh_token to obtain a new access_token. We’ll see how that works later on in this guide.

6 Building the Client

Now that the API is secured, we need to provide the user a means to authenticate with the server from the React app. Once the user logs in, we need to use their credentials to make requests to the API, as well as give the user the ability to logout and invalidate their token.

6.1 Stateless Login Component

Let’s start by creating the login component. We will pass 4 props to this component: userDetails and error variables (to represent the form input and possible error message), and changeHandler and onSubmit functions.

If you’re familiar with React, you may recognize this is a "stateless functional component". This style of React component is literally a simple function, with no internal state. This component gets its state and dynamic functionality via props only, which are passed in from the parent component, in this case, App.
client/src/Login.js
import React from 'react';
import {Jumbotron, Row, Col, Form, FormGroup, ControlLabel, FormControl, Button} from 'react-bootstrap';

const Login = ({userDetails, error, inputChangeHandler, onSubmit}) => {

  return (
    <Row>
      <Jumbotron>
        <h1>Welcome to the Garage</h1>
      </Jumbotron>
      <Row>
        <Col sm={4} smOffset={4}>

          {error ? <p className="alert alert-danger">{error} </p> : null} (1)

          <Form onSubmit={onSubmit}> (2)
            <FormGroup>
              <ControlLabel>Login</ControlLabel >
              <FormControl type='text' name='username' placeholder='Username'
                           value={userDetails.username} (3)
                           onChange={inputChangeHandler}/>  (4)
              <FormControl type='password' name='password' placeholder='Password'
                           value={userDetails.password} (3)
                           onChange={inputChangeHandler}/>  (4)
            </FormGroup>
            <FormGroup>
              <Button bsStyle="success" type="submit">Login</Button>
            </FormGroup>
          </Form>
        </Col>
      </Row>
    </Row>
  );
};

export default Login;
1 If we have an error, render the error message - otherwise render nothing.
2 The onSubmit function will be called when the login form is submitted.
3 The userDetails prop contains the username and password that the user has typed so far.
4 The inputChangeHandler function will fire every time the user types character into the form fields. We’ll see what it does in the next section.
This style of form is an example of a "controlled component". This means that the value of the inputs is set "upstream" (in this case, in the userDetails prop) and is updated only when that upstream value is changed. That change will take place when our inputChangeHandler function is called. This is also referred to as "one-way data-binding".

7 Adding Client security

7.1 Creating our Configuration

At this point we will begin adding the security configuration to the React app. Let’s begin by creating a security directory underneath client/src.

~ cd client/src
~ mkdir security

Create a file named auth.js in your new security directory

~ cd security
~ touch auth.js

7.2 Handling Authentication

Let’s continue by editing auth.js. This file is a "plain" JavaScript file (not a React component), and will contain four core authentication-related functions (logIn, logOut, `refreshToken, and isLoggedIn). These functions will be exported as a module, so they can be imported and used in any React component (or JavaScript file). We will be making use of HTML5’s localStorage object to store the user’s token after a successful login.

client/src/security/auth.js
import {SERVER_URL} from './../config';
import {checkResponseStatus} from './../handlers/responseHandlers';
import headers from './../security/headers';
import 'whatwg-fetch';
import qs from 'qs';

(1)
export default {
  logIn(auth) { (2)
    localStorage.auth = JSON.stringify(auth);
  },

  logOut() { (3)
    delete localStorage.auth;
  },

  refreshToken() { (4)
    return fetch(
      `${SERVER_URL}/oauth/access_token`,
      { method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
        },
        body: qs.stringify({ (5)
          grant_type: 'refresh_token',
          refresh_token: JSON.parse(localStorage.auth).refresh_token
        })
      })
      .then(checkResponseStatus)
      .then((a) => localStorage.auth = JSON.stringify(a))
      .catch(() => { throw new Error("Unable to refresh!")})
  },

  loggedIn() {  (6)
    return localStorage.auth && fetch(
        `${SERVER_URL}/api/vehicle`, (7)
        {headers: headers()})
        .then(checkResponseStatus)
        .then(() => { return true })
        .catch(this.refreshToken)
        .catch(() => { return false });
  }
};
1 The export keyword indicates that this is a JavaScript module, which can be imported using the import keyword. A single JavaScript file can export multiple modules, so the default indicates which module is imported by default (without specifying a particular module).
2 logIn takes in an auth object, converts it into a JSON string, and stores it in our localStorage for later use. use.
3 logOut is even simpler - we simply remove the object that was stored at localStorage.auth.
4 refreshToken is the most complex function. It makes a POST request against the /oauth/access_token endpoint (which is set up by default by Spring Security REST), including the refresh_token as a URL parameter. If successful, it will receive the same JWT token response that a normal login would have - in which case we store the new token in our localStorage as before (overwriting the earlier one).
5 Because the refresh endpoint expects form-urlencoded body, we’re using the stringify function from the qs package to convert our JavaScript object into the correct format.
6 isLoggedIn returns whether we have an auth object in localStorage, and in addition verifies that our token is still valid by making a fetch request against the secured API and checking the response (in a real-world app, you would probably have a special endpoint for this purpose). If the checkResponseStatus function throws an error, the first catch statement is called, which will call the refreshToken function described above. If that function throws another error, the final catch statement will fire and the authentication will fail.
7 We are using backticks instead of quotes to denote our strings. This is an ES6 syntax called "template strings", and they allow us to write multi-line strings as well as use expressions in our strings with the ${expression} syntax.
The server’s api/login endpoint responds us with an access_token which will expire and a refresh token which never expires and which allows us to refresh the access_token via the oauth/access_token endpoint. Checkout the plugin documentation to learn more about access token expiration and refresh options.

Write the headers() function

Notice that in our fetch call in isLoggedIn, we are calling a headers() function (imported from headers.js). This function will return an object containing our request headers, including the token from localStorage. Let’s create this function now.

Create a new JavaScript file under client/src/security, called headers.js.

client/src/security/headers.js
export default () => { (1)
  return {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${localStorage.auth ? JSON.parse(localStorage.auth).access_token : null}`
  }
}
1 Again we’re exporting a module, but this time the module is itself a function (using the ES6 "arrow function" syntax, which is analogous to a Groovy closure). The function is anonymous, so the name will be whatever variable is used to import the module (e.g, import headers from './headers').

The function from the headers.js module returns an object with an "Authorization" header, and adds the access_token (which is obtained by parsing the localStorage.auth JSON object). We will use this function later on when it’s time to authenticate our API calls.

Install the qs package

Remember that we used the qs package in our refreshToken() function above. This is a useful utility package that performs many types of conversions. In refreshToken we are using this package to convert our request body to form-urlencoded format. Now we need to add that package to our package.json:

client/package.json
{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "16.1.1",
    "react-bootstrap": "0.31.5",
    "react-dom": "16.1.1",
    "react-scripts": "1.0.17"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}
1 Add this line to add the qs dependency to our project.

If you have yarn or npm installed locally, you may run the install command to install the new package.

~ yarn install

However, the Gradle tasks for running the client app will perform the install automatically, so you are not required to perform the above step.

7.3 Writing the Response Handlers

In order to implement our authentication we will write several "handler" functions that we can import and reuse throughout our app.

Let’s begin by creating a handlers directory under client/src.

~ cd client/src
~ mkdir handlers

Now we are ready to write our first handlers. Create the following two files:

~ touch responseHandlers.js
~ touch errorHandlers.js

7.4 Creating our Response Handlers

Our first handler will be checkResponseStatus. This function will check the HTTP status of a response, and either return the JSON of the response or throw an error. We can chain this function with our REST calls using the fetch API.

client/src/handlers/responseHandlers.js
import Auth from '../security/auth';

(1)
export const checkResponseStatus = (response) => {
    if(response.status >= 200 && response.status < 300) {
        return response.json()
    } else {
        let error = new Error(response.statusText);
        error.response = response;
        throw error;
    }
};

export const loginResponseHandler = (response, handler) => {
    Auth.logIn(response);

    if(handler) {
        handler.call();
    }
};
1 The export keyword makes this function available to any JavaScript file that imports this file.

The checkResponseStatus function takes an HTTP response and checks that the status code is in the successful range, then returns the JSON body of the response. If the HTTP status code is outside that range then an error will be thrown.

loginResponseHandler uses the Auth.login(response) function that we wrote previously. If an additional function is passed in as the second argument, it will then execute that function.

7.5 Creating a Default Error Handler

Along the same lines as the checkStatusResponse function, we’ll write a defaultErrorHandler which will take a JavaScript error object along with an custom handler (to be called after the default error handling).

client/src/handlers/errorHandlers.js
export const defaultErrorHandler = (error, handler) => {
    console.error(error);

    if(handler) {
        handler.call();
    }
};

The defaultErrorHandler function is quite simple: it logs the error to the console. If an additional handler was passed in the second argument, it will then call that function.

8 Putting it all together

Now we can finally wire these pieces together in order to get our client-side security functioning. Here’s the steps we need to take:

  • Update our App component’s state to include the user details for the Login form.

  • Check whether we’re logged in, and display the Login component if the user is not authenticated.

  • Submit the Login form details to the server and obtain (and store) the resulting token

  • Provide a way to logout (delete the token from localStorage)

  • Use the token in our REST calls to the API

For your reference the full App.js file is shown below - in the remaining sections we will go piece by piece through this component (going from top to bottom) and show how to complete the above steps.

client/src/App.js
import React, {Component} from 'react';
import Garage from './Garage';
import Auth from './security/auth';
import Login from './Login';
import {Grid} from 'react-bootstrap';
import {SERVER_URL} from './config';
import {defaultErrorHandler} from './handlers/errorHandlers';
import {checkResponseStatus, loginResponseHandler} from './handlers/responseHandlers';

class App extends Component {

  //tag::state[]
  constructor() {
    super();

    this.state = {
      userDetails: {
        username: '',
        password: ''
      },
      route: '',
      error: null
    }
  }

  reset = () => { (1)
    this.setState({
      userDetails: {
        username: '',
        password: ''
      },
      route: 'login',
      error: null
    });
  };
  //end::state[]

  //tag::lifecycle[]
  componentDidMount() {
    console.log('app mounting...');

    (async () => {
      if (await Auth.loggedIn()) {
        this.setState({route: 'garage'})
      } else {
        this.setState({route: 'login'});
      }
    })();
  }

  componentDidUpdate() {
    if (this.state.route !== 'login' && !Auth.loggedIn()) {
      this.setState({route: 'login'})
    }
  }
  //end::lifecycle[]

  //tag::inputChangeHandler[]
  inputChangeHandler = (event) => {
    let {userDetails} = this.state;
    const target = event.target;

    userDetails[target.name] = target.value; (1)

    this.setState({userDetails});
  };
  //end::inputChangeHandler[]

  //tag::login[]
  login = (e) => {
    console.log('login');
    e.preventDefault(); (1)

    fetch(`${SERVER_URL}/api/login`, { (2)
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(this.state.userDetails)
    }).then(checkResponseStatus) (3)
      .then(response => loginResponseHandler(response, this.customLoginHandler)) (4)
      .catch(error => defaultErrorHandler(error, this.customErrorHandler)); (5)
  };
  //end::login[]

  //tag::handler[]
  customLoginHandler = () => { (1)
    this.setState({route: 'garage'});
  };

  customErrorHandler = (error) => { (2)
    this.reset();
    this.setState({error: error.message});
  };
  //end::handler[]


  //tag::logout[]
  logoutHandler = () => {
    Auth.logOut();
    this.reset();
  };
  //end::logout[]


  //tag::routing[]
  contentForRoute() { (1)
    const {error, userDetails, route} = this.state;

    const loginContent = <Login error={error} (2)
                                userDetails={userDetails}
                                inputChangeHandler={this.inputChangeHandler}
                                onSubmit={this.login}/>;

    const garageContent = <Garage logoutHandler={this.logoutHandler}/>;

    switch (route) {
      case 'login':
        return loginContent;
      case 'garage':
        return garageContent;
      default:
        return <p>Loading...</p>;
    }
  };

  render() { (3)
    const content = this.contentForRoute();

    return (
      <Grid>
        {content}
      </Grid>
    );
  };
  //end::routing[]
}

export default App;

8.1 Creating our state

First thing, let’s build our App state object. This is done in the component constructor. You should recognize our user and error fields in our state as we have used them already in some of our functions. In addition, we’ll add a route variable for use later on.

client/src/App.js
constructor() {
  super();

  this.state = {
    userDetails: {
      username: '',
      password: ''
    },
    route: '',
    error: null
  }
}

reset = () => { (1)
  this.setState({
    userDetails: {
      username: '',
      password: ''
    },
    route: 'login',
    error: null
  });
};
1 We’ve added a reset function so that we can easily return to our initial state when needed:

8.2 React Lifecycle Methods

Next in our App component, we need to implement two of React’s "lifecycle" methods, which will check whether we are logged in or not.

See the React documentation for more information on the component lifecycle.
client/src/App.js
componentDidMount() {
  console.log('app mounting...');

  (async () => {
    if (await Auth.loggedIn()) {
      this.setState({route: 'garage'})
    } else {
      this.setState({route: 'login'});
    }
  })();
}

componentDidUpdate() {
  if (this.state.route !== 'login' && !Auth.loggedIn()) {
    this.setState({route: 'login'})
  }
}

In componentDidMount, we are using the Auth.isLoggedIn() function to see whether we’re logged in. Because isLoggedIn uses a fetch call, which is asynchronous, we’re using the async/await keywords to prevent our check from returning before the fetch call completes. Assuming that isLoggedIn returns true, we set our route state variable to 'garage' - otherwise, set it login.

We perform a similar check in componentDidUpdate, redirecting to the login route if we are no longer logged in.

For more information on async/await, see the documentation at https://developer.mozilla.org

8.3 Login Form Change Handler

As we discussed while we were creating the Login component (see [loginScreen]), we need to define a handler function to update our userDetails state object when the username/password values are changed.

client/src/App.js
inputChangeHandler = (event) => {
  let {userDetails} = this.state;
  const target = event.target;

  userDetails[target.name] = target.value; (1)

  this.setState({userDetails});
};
1 Note that we will use the same handler for both username and password, as the name attributes set on each form input can be used to assign the correct variable.

8.4 Handling Login

client/src/App.js
login = (e) => {
  console.log('login');
  e.preventDefault(); (1)

  fetch(`${SERVER_URL}/api/login`, { (2)
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(this.state.userDetails)
  }).then(checkResponseStatus) (3)
    .then(response => loginResponseHandler(response, this.customLoginHandler)) (4)
    .catch(error => defaultErrorHandler(error, this.customErrorHandler)); (5)
};
1 We call e.preventDefault(); to disable the Login form’s default submit event.
2 Using fetch, we make a POST request containing the credentials entered by the user via the Login form.
3 We chain the checkResponseStatus to our fetch call to validate that the request was successful.
4 Assuming success, we add loginResponseHandler to the chain to complete the login process.
5 Any errors are passed to the defaultErrorHandler function, along with our customErrorHandler.
client/src/App.js
customLoginHandler = () => { (1)
  this.setState({route: 'garage'});
};

customErrorHandler = (error) => { (2)
  this.reset();
  this.setState({error: error.message});
};

Note the use of the "custom" handler functions:

  1. customLoginHandler updates this.state.route upon a successful login.

  2. customErrorHandler`clears the `userDetails state variable, and sets an error message.

At this point you should be able to successfully login to the application but there are a few more things to do before we are done.

8.5 Handling Logout

The logout handler simply calls the Auth.logOut() function we wrote earlier. We then call reset(), which gets us back our initial state.

client/src/App.js
logoutHandler = () => {
  Auth.logOut();
  this.reset();
};

8.6 Routing

Routing, in Single Page Applications, gives the user the ability to navigate through your application as though it were made of multiple "pages". In many React apps, the React Router library is used to handle this requirement. However, we will take a simplistic approach for this guide by storing our current "route" in our state, and choosing which component to render based on that variable.

client/src/App.js
contentForRoute() { (1)
  const {error, userDetails, route} = this.state;

  const loginContent = <Login error={error} (2)
                              userDetails={userDetails}
                              inputChangeHandler={this.inputChangeHandler}
                              onSubmit={this.login}/>;

  const garageContent = <Garage logoutHandler={this.logoutHandler}/>;

  switch (route) {
    case 'login':
      return loginContent;
    case 'garage':
      return garageContent;
    default:
      return <p>Loading...</p>;
  }
};

render() { (3)
  const content = this.contentForRoute();

  return (
    <Grid>
      {content}
    </Grid>
  );
};
1 We’ve created a contentForRoute function, which will select the proper component to render based on this.state.route. If there is no route set yet, we display a "Loading…​" message.
2 Note that this is where we are passing in our inputChangeHandler, login, and logoutHandler handlers into those components as props.
3 Finally, in our render function, we render out the content that we calculated earlier based on our route.
Note how we pass our handler functions as references (onSubmit={this.login}), not function calls (onSubmit={this.login()}). This is because we want our child components to have access to these functions and call them later - we don’t want to call them when the component is rendered!

8.7 Authenticating our REST Calls

At this point our login and logout functionality is complete. The last step is to authenticate our REST calls back to our API. This takes place in the Garage component, which is shown below.

client/src/Garage.js
import React from 'react';
import Vehicles from './Vehicles';
import AddVehicleForm from './AddVehicleForm';
import { Row, Jumbotron, Button } from 'react-bootstrap';
import { SERVER_URL } from './config';
import headers from './security/headers';
import 'whatwg-fetch';

class Garage extends React.Component {

  constructor() {
    super();

    this.state = {
      vehicles: [],
      makes: [],
      models: [],
      drivers: []
    }
  }

  componentDidMount() {
      fetch(`${SERVER_URL}/api/vehicle`, {
        method: 'GET',
        headers: headers(), (1)
      })
      .then(r => r.json())
      .then(json => this.setState({vehicles: json}))
      .catch(error => console.error('Error retrieving vehicles: ' + error));

      fetch(`${SERVER_URL}/api/make`, {
        method: 'GET',
        headers: headers() (1)
      })
      .then(r => r.json())
      .then(json => this.setState({makes: json}))
      .catch(error => console.error('Error retrieving makes: ' + error));

      fetch(`${SERVER_URL}/api/model`, {
        method: 'GET',
        headers: headers() (1)
    })
      .then(r => r.json())
      .then(json => this.setState({models: json}))
      .catch(error => console.error('Error retrieving models ' + error));

    fetch(`${SERVER_URL}/api/driver`, {
        method: 'GET',
        headers: headers() (1)
    })
      .then(r => r.json())
      .then(json => this.setState({drivers: json}))
      .catch(error => console.error('Error retrieving drivers: ' + error));
  }

  submitNewVehicle = (vehicle) => {
    fetch(`${SERVER_URL}/api/vehicle`, {
      method: 'POST',
      headers: headers(), (1)
      body: JSON.stringify(vehicle)
    }).then(r => r.json())
      .then(json => {
        let vehicles = this.state.vehicles;
        vehicles.push({id: json.id, name: json.name, make: json.make, model: json.model, driver: json.driver});
        this.setState({vehicles});
      })
      .catch(ex => console.error('Unable to save vehicle', ex));
  };

  render() {
    const {vehicles, makes, models, drivers} = this.state;
    (2)
    const logoutButton = <Button bsStyle="warning" className="pull-right" onClick={this.props.logoutHandler} >Log Out</Button>;

    return <Row>
      <Jumbotron>
        <h1>Welcome to the Garage</h1>
        {logoutButton}
      </Jumbotron>
      <Row>
        <AddVehicleForm onSubmit={this.submitNewVehicle} makes={makes} models={models} drivers={drivers}/>
      </Row>
      <Row>
        <Vehicles vehicles={vehicles} />
      </Row>
    </Row>;
  }
}

export default Garage;
1 Note the use of the headers() function again to return our token-bearing request headers for all of our API calls.
2 The logout button will execute the logoutHandler function when clicked. Start up the app and verify that you can login and authenticate successfully. Congratulations! You have secured your React app with Grails and Spring Security!

9 Do you need help with Grails?

Object Computing, Inc. (OCI) sponsored the creation of this Guide. A variety of consulting and support services are available.

OCI is Home to Grails

Meet the Team