Unlikenesses A Backend Developer

JWTs with React and Laravel

10 August 2017

This is a tutorial on how to get JSON Web tokens working so that React can securely communicate with a back-end API. For the purposes of this tutorial I'll be using Laravel 5.4 for the back-end and React for the front-end. The finished code can be seen here.

Getting started

First thing is to create a new Laravel project - I do this in the command line to create a project called jwt:

laravel new jwt

We want a users table; we won't be using most of the Laravel auth scaffolding, but let's use it anyway to set up the users table quickly:

php artisan make:auth

For the API let's just have one resource, clients. For this we'll set up the model and migration with artisan:

php artisan make:model Client -m

In the clients migration add a few fields - I've added name, address and telephone:

public function up()
{
    Schema::create('clients', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->text('address');
        $table->string('telephone');
        $table->timestamps();
    });
}

Then migrate to set up these tables (I'm assuming you've created a database for this project & configured Laravel to use it in the .env file):

php artisan migrate

And let's seed the database. First create a ClientFactory.php file in the database/factories folder. This will use Faker to generate fake details for each client:

$factory->define(App\Client::class, function (Faker\Generator $faker) {
    static $password;

    return [
        'name' => $faker->name,
        'address' => $faker->address,
        'telephone' => $faker->phoneNumber
    ];
});

Then in seeds/DatabaseSeeder.php we can call this factory in the run method to create 50 clients:

factory(App\Client::class, 50)->create();

Now run php artisan db:seed and we have our dummy data.

Setting up JWT

We'll be using jwt-auth to get JWT authentication working with Laravel. Follow the installation instructions. The project's creating tokens page gives us a basic method to authenticate a user. For now let's test it in an API development tool like Postman. Create a new controller:

php artisan make:controller FrontEndUserController

I'm including a quick method to create a user so we can attempt logins:

public function signUp(Request $request)
{
    $user = User::create(['email' => $request->email, 'password' => bcrypt($request->password)]);
}

In our routes/api.php file we can create a route for this and another one for when the user tries to sign in:

Route::post('/signup', 'FrontEndUserController@signUp');
Route::post('/signin', 'FrontEndUserController@signIn');

If we now use Postman to make a POST request to [project_path]/api/signup, with an email and password, it should create the new user (check this in the database. If it doesn't work you might need to make the username field nullable).

Now for the magic - the sign in method using JWT. We're just going to re-use the method giving in the docs mentioned above:

public function signIn(Request $request)
{
    try {
        if (! $token = JWTAuth::attempt(['email' => $request->email, 'password' => $request->password])) {
            return response()->json(['error' => 'invalid_credentials'], 401);
        }
    } catch (JWTException $e) {
        return response()->json(['error' => 'could_not_create_token'], 500);
    }

    return response()->json(compact('token'));
}

If you now POST to the signin route with the email and password you created before, you should get back a token similar to this:

Calling the API

Let's test the authentication is working. Create a ClientController:

php artisan make:controller ClientController

In that for now we'll just create an index method that return all clients (not ideal for an API but this tutorial is more about JWTs than developing APIs):

public function index()
{
    return Client::all();
}

In the api.php routes we'll create a new group that uses the built-in jwt.auth middleware:

Route::group(['middleware' => 'jwt.auth'], function() {
    Route::get('/clients', 'ClientController@index');
});

We'll need to register this middleware to the $routeMiddleware array in app/Http/Kernel.php:

'jwt.auth' => \Tymon\JWTAuth\Middleware\GetUserFromToken::class

Now if you make a GET call in Postman to [project_path]/api/clients you should get this error:

{"error":"token_not_provided"}

To remedy this, pass an authorization header, with the key of Authorization and value of Bearer [token] (copy the token you created earlier). Now run the GET call again and you should get the list of clients back as JSON. (NB. If you get a token_expired error just call the previous POST endpoint and get a new token.)

Setting up React

First let's remove the vue line from devDependencies in package.json, and install the existing npm dependencies with npm install. Then install react and react-dom:

npm install --save react react-dom

Now remove any references to Vue (this won't be necessary in Laravel 5.5). Delete Example.vue from the resources/assets/js/components folder. Then in resources/assets/js/app.js, replace all the VueJS code with this:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';


ReactDOM.render(
  <App />,
  document.getElementById('root')
);

This simply imports the React modules, imports our root component and attaches it to a div with the id root on our homepage. So now create the components/App.js file and put some temporary code there:

import React from 'react';

class App extends React.Component {
    constructor() {
        super();
    }

    render() {
        return (
            <h1>Hello</h1>
        );
    }
}

export default App;

Now we need to put that #root div in home.blade.php - put it anywhere you want. I just stuck it in the middle of the Bootstrap div-nest:

<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-body">
                    <div id="root"></div>
                </div>
            </div>
        </div>
    </div>
</div>

And modify the base route to point here (in routes/web.php):

Route::get('/', function () {
    return view('home');
});

Now to compile the React code with Laravel's Mix we need to modify the webpack.mix.js file:

mix.react('resources/assets/js/app.js', 'public/js')
   .sass('resources/assets/sass/app.scss', 'public/css');

Now run npm run watch and it'll compile the JavaScript - point your browser at [project-path] and you should see the "Hello" string on the homepage.

React Sign-In page and routing

(NB. I had to follow the instructions here to get the spread operator working with Laravel Mix.)

For this example the React app will have 3 pages: Home, Login and Clients. We'll use React Router 4 to set up the routing. We'll want Home and Login to be accessible to non-logged in users, but only logged-in users can view the Clients page. In this section I'll detail setting up the routing and the login page.

First let's install the router: npm install react-router-dom --save. Then in App.js we can import the modules we need:

import { HashRouter, Route, Switch, NavLink, Redirect } from 'react-router-dom';

Now we re-write its render method. We'll use react-router's HashRouter to contain our routes -- see their documentation for more info:

<HashRouter>
    <div>
        <Menu />
        <Switch>
            <Route exact path='/' component={Home} />
            <Route exact path='/login' render={(props) => <Login />} />
        </Switch>
    </div>
</HashRouter>

You'll notice that the Login route uses a slightly different method to render the component - this is because we'll be passing some properties to it later. There's also a new Menu component there at the top - we can add this under the App class declaration in App.js:

const Menu = (props) => (
    <ul>
        <li>
            <NavLink exact activeClassName="active" to="/">
                Home
            </NavLink>
        </li>
        <li>
            <NavLink exact activeClassName="active" to="/login">
                Login
            </NavLink>
        </li>
        <li>
            <NavLink exact activeClassName="active" to="/clients">
                Clients
            </NavLink>
        </li>
    </ul>
);

The Clients link is in there but we can ignore it for now. One last thing in App.js: import the Home and Login components:

import Home from './Home';
import Login from './Login';

Now let's create the Home component - it can really be anything you like. My one just prints the word "Home":

import React from 'react';

const Home = () => (
    <h1>Home</h1>
);

export default Home;

The Login component is more interesting. Create a basic React class. This will be what React calls a "controlled component", i.e. a component containing a form with fields whose values are controlled by the component's state. So in our component's constructor, let's set up the intial form state (there'll be 2 fields, email and password):

this.state = {
    email: '',
    password: ''
};

Then in the render method we have a simple form (it's using some Bootstrap class names for simple styling):

<form onSubmit={this.handleSubmit}>
    <div className='form-group'>
        <input
            name='email'
            type='email'
            className='form-control'
            placeholder='Email'
            value={this.state.email}
            onChange={this.handleChange} />
    </div>
    <div className='form-group'>
        <input
            name='password'
            type='password'
            className='form-control'
            placeholder='Password'
            value={this.state.password}
            onChange={this.handleChange} />
    </div>
    <div className='form-group'>
        <input type='submit' className='btn' value='Login' />
    </div>
</form>

As you can see there's an email field and a password field, both of whose values are set by the component's state. Both fields also have an onChange attribute, which calls the component's handleChange method. This will simply update the state when the user types in a given field:

handleChange(event) {
    const name = event.target.name;
    this.setState({
        [name]: event.target.value
    });
}

Because it checks for the field's name attribute we can use the same method for both input fields. Don't forget to add this method to the constructor:

this.handleChange = this.handleChange.bind(this);

We also have an onSubmit attribute for the form, which calls a method called handleSubmit. We want this to call the Laravel api/signin route we created earlier. We can use Axios for this since it's already part of Laravel's package.json:

handleSubmit(event) {
    event.preventDefault();
    axios.post('/api/signin', {
        email: this.state.email,
        password: this.state.password
    })
    .then((response) => {
        const token = response.data.token;
        console.log(token);
    })
    .catch((error) => {
        console.log(error);
    });
}

All this does is POST the component's email and password state to our endpoint, and logs the token to the console. (Bind this in the constructor: this.handleSubmit = this.handleSubmit.bind(this);.) If you try running this now and logging in (and providing you use the correct email/password) you should see a JWT token in the console.

Somewhere we'll need to keep track of whether a user is logged in or not. We can do that in a number of ways - for the purposes of this project I'll keep it simple and use the App component's state. So in its constructor, add this:

this.state = {
    isAuthenticated: false,
    token: null
};

We'll also need a method to set these properties when a login is successful (this is also in App):

authenticate(token) {
    this.setState({
        isAuthenticated: true,
        token: token
    })
}

(Bind this method in the constructor: this.authenticate = this.authenticate.bind(this);).

Now we can pass these as properties to the Login component:

<Route exact path='/login' render={(props) => <Login authenticate={this.authenticate} isAuthenticated={this.state.isAuthenticated} {...props} />} />

Now in the Login component we can modify the axios call to pass the token to the parent's authenticate method:

.then((response) => {
    const token = response.data.token;
    this.props.authenticate(token);
})

While we're here we can also check to see if a user is logged in and not display the form if so:

{this.props.isAuthenticated ?
    <p>You are already logged in.</p>
    :
    <form onSubmit={this.handleSubmit}>
        // Form stuff here
    </form>
}

Finally let's add a Logout link to the menu:

{props.isAuthenticated ?
    <li>
        <a onClick={props.logout}>
            Logout
        </a>
    </li>
    :
    null
}

This will check if isAuthenticated is true - if so, show the Logout link with an onClick handler. We'll need to pass these properties to the Menu in the router:

<Menu isAuthenticated={this.state.isAuthenticated} logout={this.logout} />

...and create the logout method in App:

logout() {
    this.setState({
        isAuthenticated: false,
        token: null
    });
}

As you can see this just resets the state to its original values. As usual, bind this method in the constructor: this.logout = this.logout.bind(this);.

Private routes

We have a link to the clients page in our menu, but we haven't set up the route or the component yet. For creating private routes (i.e. routes that can only be accessed by logged-in users) I'm following the example in the official react-router documentation. This uses a bespoke PrivateRoute component which will either display the component passed to it (in our case, Clients) or, if not logged in, it will redirect to a given route. My version is the same but with a couple of small tweaks. First let's set up the route in the App component:

<PrivateRoute exact path='/clients' component={Clients} isAuthenticated={this.state.isAuthenticated} token={this.state.token} />

And here's my version of the PrivateRoute component (which I also place in App.js):

const PrivateRoute = ({ component: Component, isAuthenticated, token, ...rest }) => (
    <Route {...rest} render={props => (
        isAuthenticated ? (
            <Component {...props} {...rest} token={token} isAuthenticated={isAuthenticated} />
        ) : (
            <Redirect to={ {
                pathname: '/login',
                state: { from: props.location }
            } } />
        )
    )} />
);

It's not the most readable of code so I'll go through it bit by bit. The first thing to note is the arguments it takes:

({ component: Component, isAuthenticated, token, ...rest })

The first three arguments correspond to the three properties we pass it in our router. (The first parameter, with a colon, renames the component parameter to Component - this is necessary [I presume] because JSX considers tags that begin with a lower-case letter to be an HTML tag rather than a React component.) We need to pass in the isAuthenticated state to know whether or not to redirect to the login page. And we need the token to give to the Clients component when we finally build it. The last parameter, ...rest uses the rest parameter syntax to gather up any remaining properties.

Moving into the body of the component itself, it's comprised of a single <Route> component. It uses the render function to allow us to programmatically decide on which component is rendered on this route. The render function takes the route props as an argument and if the user is authenticated (i.e. if isAuthenticated is true), it renders the component passed to PrivateRoute (passing it the token prop so we can make calls to the API later). If the user is not authenticated it renders a Redirect component. This uses the to object to give the location we're redirecting to (in this case the login route) and where we're coming from (using the Route component's property location). This can be useful to display messages on the login page.

Now we can add a private route pointing to the Clients component. Add this line within the Switch block:

<PrivateRoute exact path='/clients' component={Clients} isAuthenticated={this.state.isAuthenticated} token={this.state.token} />

This passes the three props - component, isAuthenticated and token - to the PrivateRoute component. Now we need to create....

The Clients component

Create a new React component called Clients. We'll need some state to hold the clients, so let's put that in the constructor:

constructor() {
    super();
    this.state = {
        clients: []
    }
}

Our render method is pretty simple - we just map over the clients array and print out each client's details:

render() {
    return (
        <div>
            <h1>Clients</h1>
            { this.state.clients.map((client, index) => {
                return (
                    <div className="client" key={index}>
                        {client.name}<br />
                        {client.address}<br />
                        {client.telephone}
                        <hr />
                    </div>
                )
            })}
        </div>
    );
}

Now we just need to make a call to the API when the component mounts to request the clients. We'll use Axios again, and pass the Authorization headers:

componentWillMount() {
    this.getClients();
}

getClients() {
    const token = this.props.token;
    axios.get('/api/clients', {
        headers: { 'Authorization': 'Bearer ' + token }
    })
    .then((response) => {
        const clients = response.data;
        this.setState({ clients });
    })
    .catch((error) => {
        console.log(error);
    });
}

As you can see we grab the token from the component's props, and if the call is successful we update the component's state. Now just import this component in App and try it out. When you first click on the Clients link in the nav you should be redirected to the Login page. After logging in and clicking the Clients link you should be able to see the list of clients returned by the API.

Cleaning up

There's more to do, but first let's tidy a few things up. First, it would be good if, on a successful login, the Login component redirected to the page the user originally tried to access. Again this code is heavily indebted to the official docs for React-Router. All we need to do is add a check at the beginning of the render method of the Login component. If we're already logged in, and if we have a location property (i.e. if we've been redirected here from another, protected, page), then just render a Redirect component pointing to that page. First import the Redirect component at the top of the Login component:

import { Redirect } from 'react-router-dom';

Then at the beginning of the render method, add this conditional:

if (this.props.isAuthenticated && this.props.location.state !== undefined) {
    return (
        <Redirect to={this.props.location.state.from} />
    );
}

Now when you click on the Clients link, then login, you should be redirected back to the Clients page.

What about if you enter false credentials? If you try to log in now with the wrong username / password you will get an error like this:

Error: Request failed with status code 401

This isn't surprising since, if you recall, earlier we set up the API signIn endpoint to do just this:

return response()->json(['error' => 'invalid_credentials'], 401);

401 is the HTTP status code that means Unauthorized. So in our axios call we could check for this code and if it's present show an appropriate error message. First let's add an error property to the state of Login:

this.state = {
    email: '',
    password: '',
    error: ''
};

Then we can change the catch part of our axios call to check for the 401 status, and update the error state accordingly:

.catch((error) => {
    const status = error.response.status
    if (status === 401) {
        this.setState({ error: 'Username or password not recognised.' });
    }
});

Finally in our render method we check to see if the error is an empty string - if not just display it in a p tag with a Bootstrap css helper class:

<h1>Login</h1>
{this.state.error !== '' ?
    <p className="text-danger">{this.state.error}</p>
    :
    null
}

Now when you try to log in with false credentials you will get this message. But we do need to reset it when login is successful otherwise the message will remain on the screen. So add a line to do this just before we authenticate:

.then((response) => {
    this.setState({ error: '' });
    const token = response.data.token;
    this.props.authenticate(token);
})

Refreshing expired tokens

By default the jwt-auth package expires its tokens after 1 hour. We need a way to check whether the current token has expired, and if so, to refresh it. For the purposes of testing it might be helpful to set the expiry time to a smaller value - this can be done in config/jwt.php. I've set the ttl value ('time to live') to 1 (i.e. one minute). Don't forget to set it back to 60 (or whatever you want) when you've finished testing.

Now, when a token has expired, if we try to make a call to the clients API endpoint we'll get a 401 error, just like when the user is not logged in. So to distinguish between these two cases, we'll check that a) the user is logged in, and b) we get a 401 response from our API. If both of these are true, then we can ask to refresh the token.

First let's set up the Laravel API side of things. We want a new endpoint, refreshToken. So in the api.php routes file, add this line under the signup and signin routes:

Route::get('/refreshToken', 'FrontEndUserController@refreshToken');

Then in FrontEndUserController we'll add the refreshToken method:

public function refreshToken()
{
    $token = JWTAuth::getToken();

    try {
        $token = JWTAuth::refresh($token);
    } catch (JWTException $e) {
        return response()->json(['error' => 'could_not_create_token'], 500);
    }

    return response()->json(compact('token'));
}

This is pretty self-explanatory - get the current token, try to refresh it and if this succeeds return it as JSON. That's it for the server-side stuff.

Now, back on the client-side, we need to update our getClients method to make that check I talked about at the start of this section. So at the end of the axios call add this catch method:

.catch((error) => {
    const status = error.response.status;
    if (status === 401 && this.props.isAuthenticated) {
        // logged in but invalid jwt
        this.props.refresh();
    }
});

This just checks that we're getting a 401 status response and that the user has been authenticated: if so, call a refresh method that sits in the parent component. Now go to the App component and add this method:

refresh() {
    return axios.get('/api/refreshToken', {
        headers: { 'Authorization': 'Bearer ' + this.state.token }
    })
    .then((response) => {
        const token = response.data.token;
        this.authenticate(token);
    })
    .catch((error) => {
        console.log('Error!', error);
    });
}

(Don't forget to bind this method in the constructor, and to pass it as a prop via the PrivateRoute: refresh={this.refresh}.) Here we're making a call to our recently-created refreshToken endpoint (passing our app's current token in the header). When we get the token back, we just update the state's token value with the authenticate method we created a while ago. Since this token value is passed as a prop to the Clients component, it will have the new token value automatically. But we still need to check for this change and re-call the clients endpoint with the new token. We can do this using the componentDidUpdate lifecycle hook.

So back in the Clients component, add this hook:

componentDidUpdate(prevProps, prevState) {
    if (prevProps.token !== this.props.token) {
        this.getClients();
    }
}

All this does is to see if the new token property is different from the previous token property: if it is, we need to get our client list again.

Persisting tokens with localStorage

As the app currently stands, if you refresh the page the token will be erased and the user will have to log in again. We need a way to persist the token between sessions - for this we'll use localStorage. We need to give the value a key name - let's call it jwt. In the authenticate method in App.js we just need to use the localStorage.setItem command to persist the token to the browser:

localStorage.setItem('jwt', token);

You can check the token's being saved using the Chrome (under Application -> Local Storage) or Firefox (under Storage) dev-tools. Now, whenever the app loads, all we need to do is to check localStorage for the jwt key, and if it exists set its value to be our app's token. As usual we can do this with the componentWillMount lifecycle hook:

componentWillMount() {
    const lsToken = localStorage.getItem('jwt');
    if (lsToken) {
        this.authenticate(lsToken);
    }
}

If there's no token in there, getItem will return null and nothing will happen. If there is one there, we call authenticate which updates the app's state with the saved token.

Finito. This has been a tutorial on setting up JWT authentication to allow a React app to communicate with a Laravel API. All the code is available here.