/ elixir

API Authentication with Phoenix and React - part 2

In the first part of this post I've shown how to configure the API server to let the user authenticate, return an authentication token, and request it to access protected routes.
Now I'm going to configure a React app to consume that API and manage authentication.

The app uses React Router to manage routes and Redux for the state of the app.

Protect private routes

I'm going to define a PrivateRoute component as a wrapper around Route. The component will check the user authentication.

The router configuration will have a standard Route component for the Login page and will use PrivateRoute for the rest of the routes:

<Router>
    <Switch>
        <Route path='/login' component={Login} />
        <PrivateRoute path='/private' component={PrivateComponent}/>
    </Switch>
</Router>

The PrivateRoute component will check the isAuthenticated flag in the state and will redirect back to login if false or will render the private component otherwise:

import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';

const mapStateToProps = state => {
    return {
        isAuthenticated: state.isAuthenticated,
    };
};

class PrivateRoute extends React.Component {
    render() {
        if (!this.props.isAuthenticated) {
            return (
                <Redirect
                    to={{
                    pathname: "/login",
                    state: { from: this.props.location }
                    }}
                />
            );
        }

        return (
            <Route component={this.props.Component} {...this.props} />
        );
    }
}

export default connect(mapStateToProps)(PrivateRoute);

Sign in and receive the token from the server

The Login component will simply show a form and will manage the initial authentication, saving the token in a cookie for later use:

import React from 'react';
import { connect } from 'react-redux';

import {
    signIn,
} from '../actions';

const mapStateToProps = state => {
    return {
        isAuthenticated: state.isAuthenticated,
    };
};

const mapDispatchToProps = dispatch => {
    return {
        onSignIn: (email, password) => dispatch(signIn(email, password)),
    };
};

class Login extends React.Component {
    constructor(props) {
        super(props);
        this.onSignIn = this.onSignIn.bind(this);
        this.state = {email: "", password: ""};
    }

    render() {
        return (
            <div className="container">
                <h1 className="title">Login</h1>
                {this.props.isAuthenticated ? this.alreadyAuthenticated() : this.form()}
            </div>
        );
    }

    alreadyAuthenticated() {
        return ("You're already authenticated.")
    }

    form() {
        return (
            <form>
                <div className="field">
                    <label className="label">Email</label>
                    <div className="control">
                        <input
                            className="input"
                            type="email"
                            placeholder="Your email address"
                            value={this.state.email}
                            autoFocus={true}
                            onChange={(e) => this.setState({...this.state, email: e.target.value})}
                        />
                    </div>
                </div>

                <div className="field">
                    <label className="label">Password</label>
                    <div className="control">
                        <input
                            className="input"
                            type="password"
                            placeholder="Your password"
                            value={this.state.password}
                            onChange={(e) => this.setState({...this.state, password: e.target.value})}
                        />
                    </div>
                </div>

                <button
                    className="button is-primary"
                    onClick={this.onSignIn}
                >Sign in</button>
            </form>
        );
    }

    onSignIn() {
        this.props.onSignIn(this.state.email, this.state.password);
    }
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(Login);

The signIn action is where the "magic" happens:

export const signIn = (email, password) => ((dispatch) => {
    return fetch(`http://<server_url>/api/sessions/sign_in}`, {
        method: "POST",
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({email, password}),
      }).then(
        response => {
            if (!response.ok) {
                // Manage error
                return dispatch(errorOnFetch(response.statusText));
            }
            return response.json().then(response => dispatch(signInSuccessfull(response.data)));
        },
        error => {
            return dispatch(errorOnFetch(error))
        }
    );
});

const signInSuccessfull = (data) => {
    setAuthToken(data.token);
    return {
        type: AUTHENTICATION_SUCCEDED,
    }
};

Two main things happen in the signInSuccessfull method: the token returned by the server is passed to the setAuthToken method and the AUTHENTICATION_SUCCEEDED action is returned to the redux reducer.

The reducer sets the isAuthenticated flag to true (do you remember the check in the PrivateRoute component?):

const mainReducer = (state = initialState, action) => {
    switch (action.type) {
        case AUTHENTICATION_SUCCEDED:
            return ({...state,
                isAuthenticated: true,
            });
        case AUTHENTICATION_SIGNOUT:
            return ({...state,
                isAuthenticated: false,
            });
    }
}

The setAuthToken method saves the token in a cookie, so that it will be then available for the next requests:

const setAuthToken = (token) => {
    const cookies = new Cookies();
    cookies.set('my_auth_token', token, {
        path: '/'
    });
};

I'm using the universal-cookie package here, so we need to install it:

yarn add universal-cookie

Other useful methods will permit to get the cookie or delete it:

export const getAuthToken = () => {
    const cookies = new Cookies();
    return cookies.get('my_auth_token');
};

const removeAuthToken = () => {
    const cookies = new Cookies();
    cookies.remove('my_auth_token', {
        path: '/',
    });
};

Use the token for private routes

At this point we have a valid token saved in a cookie. We just need to use it when making a request for a private API endpoint.

I'll use a wrapper function around fetch to add the Authorization header to the requests:

const authFetch = (url, options) => (
    fetch(url, mergeAuthHeaders(options)).then(
        response => {
            // Sign out if we receive a 401!
            if (response.status === 401) {
                store.dispatch(signOut());
                throw new Error("Unauthorized");
            }
            return response;
        },
        error => error
    )
);

const mergeAuthHeaders = (baseOptions) => {
    const options = _.isUndefined(baseOptions) ? {} : baseOptions;
    if (!_.has(options, 'headers')) {
        options.headers = {};
    }
    options.headers = {
        ...options.headers,
        'Authorization': `Bearer ${getAuthToken()}`,
    };
    return options;
}

The authFetch method receives a URL to fetch and the options for the fetch method. It merges the authentication header in the options and makes the request.
If it receives a 401 response, then it makes the sign out, deleting the cookie and setting the isAuthenticated flag to false:

export const signOut = () => {
    removeAuthToken();
    return {
        type: AUTHENTICATION_SIGNOUT,
    }
};

That's it, you should probably add more logic to manage side cases and errors, but this is enough to consume the APIs we built.