September 11 2018

React - Basic HTTP Authentication Tutorial & Example

Tutorial built with React 16.5

Other versions available:

In this tutorial we'll cover how to implement a simple login page with React and Basic HTTP Authentication.

The project for the tutorial is available on GitHub at https://github.com/cornflourblue/react-basic-authentication-example.

Here it is in action:(See on StackBlitz at https://stackblitz.com/edit/react-basic-authentication-example)


Running the React Basic Auth Tutorial Example Locally

  1. Install NodeJS and NPM from  https://nodejs.org/en/download/.
  2. Download or clone the project source code from https://github.com/cornflourblue/react-basic-authentication-example
  3. Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
  4. Start the application by running npm start from the command line in the project root folder, this will launch a browser displaying the application.


Running the React Basic Auth Example with a Real Backend API

The React tutorial example uses a fake / mock backend by default so it can run in the browser without a real api, to switch to a real backend api you just have to remove or comment out the 2 lines below the comment // setup fake backend located in the /src/index.jsx file.

You can build your own backend api or start with one of the below options:


React Tutorial Project Structure

All source code for the React basic authentication tutorial is located in the /src folder. Inside the src folder there is a folder per feature (App, HomePage, LoginPage) and few folders for non-feature code that can be shared across different parts of the app (_components, _helpers, _services).

I prefixed non-feature folders with an underscore "_" to group them together and make it easy to distinguish between features and non-features, it also keeps the project folder structure shallow so it's quick to see everything at a glance from the top level and to navigate around the project.

The index.js files in each folder are barrel files that group all the exported modules together so they can be imported using the folder path instead of the full module path and to enable importing multiple modules in a single import (e.g. import { serviceOne, serviceTwo } from '../_services').

Click any of the below links to jump down to a description of each file along with it's code:

 

React Tutorial Components Folder

Path: /src/_components

The _components folder contains shared React components that can be used anywhere in the application.

 

React Tutorial Private Route Component

Path: /src/_components/PrivateRoute.jsx

The react private route component renders a route component if the user is logged in, otherwise it redirects the user to the /login page.

The way it checks if the user is logged in is by checking that there is a user object in local storage. While it's possible to bypass this check by manually adding an object to local storage using browser dev tools, this would only give access to the client side component, it wouldn't give access to any real secure data from the server api because valid user credentials are required for this.

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

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

React Tutorial Helpers Folder

Path: /src/_helpers

The helpers folder contains all the bits and pieces that don't fit into other folders but don't justify having a folder of their own.

 

React Tutorial Auth Header

Path: /src/_helpers/auth-header.js

Auth header is a helper function that returns an HTTP Authorization header containing the basic authentication credentials (base64 username and password) of the currently logged in user from local storage. If the user isn't logged in an empty object is returned.

The auth header is used to make authenticated HTTP requests to the server api using basic authentication.

export function authHeader() {
    // return authorization header with basic auth credentials
    let user = JSON.parse(localStorage.getItem('user'));

    if (user && user.authdata) {
        return { 'Authorization': 'Basic ' + user.authdata };
    } else {
        return {};
    }
}
 

React Tutorial Fake / Mock Backend

Path: /src/_helpers/fake-backend.js

The fake backend is used for running the tutorial example without a server api (backend-less). It monkey patches the fetch() function to intercept certain api requests and mimic the behaviour of a real api. Any requests that aren't intercepted get passed through to the real fetch() function.

export function configureFakeBackend() {
    let users = [{ id: 1, username: 'test', password: 'test', firstName: 'Test', lastName: 'User' }];
    let realFetch = window.fetch;
    window.fetch = function (url, opts) {
        return new Promise((resolve, reject) => {
            // wrap in timeout to simulate server api call
            setTimeout(() => {

                // authenticate
                if (url.endsWith('/users/authenticate') && opts.method === 'POST') {
                    // get parameters from post request
                    let params = JSON.parse(opts.body);

                    // find if any user matches login credentials
                    let filteredUsers = users.filter(user => {
                        return user.username === params.username && user.password === params.password;
                    });

                    if (filteredUsers.length) {
                        // if login details are valid return user details
                        let user = filteredUsers[0];
                        let responseJson = {
                            id: user.id,
                            username: user.username,
                            firstName: user.firstName,
                            lastName: user.lastName
                        };
                        resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(responseJson)) });
                    } else {
                        // else return error
                        reject('Username or password is incorrect');
                    }

                    return;
                }

                // get users
                if (url.endsWith('/users') && opts.method === 'GET') {
                    // check for fake auth token in header and return users if valid, this security 
                    // is implemented server side in a real application
                    if (opts.headers && opts.headers.Authorization === `Basic ${window.btoa('test:test')}`) {
                        resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(users)) });
                    } else {
                        // return 401 not authorised if token is null or invalid
                        resolve({ status: 401, text: () => Promise.resolve() });
                    }

                    return;
                }

                // pass through any requests not handled above
                realFetch(url, opts).then(response => resolve(response));

            }, 500);
        });
    }
}
 

React Tutorial Services Folder

Path: /src/_services

The _services layer handles all http communication with backend apis for the application, each service encapsulates the api calls for a content type (e.g. users) and exposes methods for performing various operations (e.g. CRUD operations). Services can also have methods that don't wrap http calls, for example the userService.logout() method just removes an item from local storage.

I like wrapping http calls and implementation details in a services layer, it provides a clean separation of concerns and simplifies the react components that use the services.

 

React Tutorial User Service

Path: /src/_services/user.service.js

The user service encapsulates all backend api calls for performing CRUD operations on user data, as well as logging and out of the example application. The service methods are exported via the userService object at the top of the file, and the implementation of each method is located in the function declarations below.

In the handleResponse method the service checks if the http response from the api is 401 Unauthorized and automatically logs the user out. This handles if the credentials are incorrect or if the user is no longer valid for any reason.

import config from 'config';
import { authHeader } from '../_helpers';

export const userService = {
    login,
    logout,
    getAll
};

function login(username, password) {
    const requestOptions = {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
    };

    return fetch(`${config.apiUrl}/users/authenticate`, requestOptions)
        .then(handleResponse)
        .then(user => {
            // login successful if there's a user in the response
            if (user) {
                // store user details and basic auth credentials in local storage 
                // to keep user logged in between page refreshes
                user.authdata = window.btoa(username + ':' + password);
                localStorage.setItem('user', JSON.stringify(user));
            }

            return user;
        });
}

function logout() {
    // remove user from local storage to log user out
    localStorage.removeItem('user');
}

function getAll() {
    const requestOptions = {
        method: 'GET',
        headers: authHeader()
    };

    return fetch(`${config.apiUrl}/users`, requestOptions).then(handleResponse);
}

function handleResponse(response) {
    return response.text().then(text => {
        const data = text && JSON.parse(text);
        if (!response.ok) {
            if (response.status === 401) {
                // auto logout if 401 response returned from api
                logout();
                location.reload(true);
            }

            const error = (data && data.message) || response.statusText;
            return Promise.reject(error);
        }

        return data;
    });
}
 

React Tutorial App Folder

Path: /src/App

The app folder is for react components and other code that is used only by the app component in the tutorial application.

 

React Tutorial App Component

Path: /src/App/App.jsx

The app component is the root component for the react tutorial application, it contains the outer html, routes and global alert notification for the example app.

import React from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom';

import { PrivateRoute } from '../_components';
import { HomePage } from '../HomePage';
import { LoginPage } from '../LoginPage';

class App extends React.Component {
    render() {
        return (
            <div className="jumbotron">
                <div className="container">
                    <div className="col-sm-8 col-sm-offset-2">
                        <Router>
                            <div>
                                <PrivateRoute exact path="/" component={HomePage} />
                                <Route path="/login" component={LoginPage} />
                            </div>
                        </Router>
                    </div>
                </div>
            </div>
        );
    }
}

export { App };
 

React Tutorial Home Page Folder

Path: /src/HomePage

The home page folder is for react components and other code that is used only by the home page component in the tutorial application.

 

React Tutorial Home Page Component

Path: /src/HomePage/HomePage.jsx

The home page component is displayed after signing in to the application, it shows the signed in user's name plus a list of all users in the tutorial application. The users are fetched from the api by calling the userService.getAll() method from the componentDidMount() react lifecycle hook.

import React from 'react';
import { Link } from 'react-router-dom';

import { userService } from '../_services';

class HomePage extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            user: {},
            users: []
        };
    }

    componentDidMount() {
        this.setState({ 
            user: JSON.parse(localStorage.getItem('user')),
            users: { loading: true }
        });
        userService.getAll().then(users => this.setState({ users }));
    }

    render() {
        const { user, users } = this.state;
        return (
            <div className="col-md-6 col-md-offset-3">
                <h1>Hi {user.firstName}!</h1>
                <p>You're logged in with React & Basic HTTP Authentication!!</p>
                <h3>Users from secure api end point:</h3>
                {users.loading && <em>Loading users...</em>}
                {users.length &&
                    <ul>
                        {users.map((user, index) =>
                            <li key={user.id}>
                                {user.firstName + ' ' + user.lastName}
                            </li>
                        )}
                    </ul>
                }
                <p>
                    <Link to="/login">Logout</Link>
                </p>
            </div>
        );
    }
}

export { HomePage };
 

React Tutorial Login Page Folder

Path: /src/LoginPage

The login page folder is for react components and other code that is used only by the login page component in the tutorial application.

 

React Tutorial Login Page Component

Path: /src/LoginPage/LoginPage.jsx

The login page component renders a login form with username and password fields. It displays validation messages for invalid fields when the user attempts to submit the form. If the form is valid the component calls the userService.login(username, password) method, if login is successful the user is redirected back to the original page they were trying to access.

In the constructor() function the userService.logout() method is called which logs the user out if they're logged in, this enables the login page to also be used as the logout page.

import React from 'react';

import { userService } from '../_services';

class LoginPage extends React.Component {
    constructor(props) {
        super(props);

        userService.logout();

        this.state = {
            username: '',
            password: '',
            submitted: false,
            loading: false,
            error: ''
        };

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

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

    handleSubmit(e) {
        e.preventDefault();

        this.setState({ submitted: true });
        const { username, password, returnUrl } = this.state;

        // stop here if form is invalid
        if (!(username && password)) {
            return;
        }

        this.setState({ loading: true });
        userService.login(username, password)
            .then(
                user => {
                    const { from } = this.props.location.state || { from: { pathname: "/" } };
                    this.props.history.push(from);
                },
                error => this.setState({ error, loading: false })
            );
    }

    render() {
        const { username, password, submitted, loading, error } = this.state;
        return (
            <div className="col-md-6 col-md-offset-3">
                <div className="alert alert-info">
                    Username: test<br />
                    Password: test
                </div>
                <h2>Login</h2>
                <form name="form" onSubmit={this.handleSubmit}>
                    <div className={'form-group' + (submitted && !username ? ' has-error' : '')}>
                        <label htmlFor="username">Username</label>
                        <input type="text" className="form-control" name="username" value={username} onChange={this.handleChange} />
                        {submitted && !username &&
                            <div className="help-block">Username is required</div>
                        }
                    </div>
                    <div className={'form-group' + (submitted && !password ? ' has-error' : '')}>
                        <label htmlFor="password">Password</label>
                        <input type="password" className="form-control" name="password" value={password} onChange={this.handleChange} />
                        {submitted && !password &&
                            <div className="help-block">Password is required</div>
                        }
                    </div>
                    <div className="form-group">
                        <button className="btn btn-primary" disabled={loading}>Login</button>
                        {loading &&
                            <img src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" />
                        }
                    </div>
                    {error &&
                        <div className={'alert alert-danger'}>{error}</div>
                    }
                </form>
            </div>
        );
    }
}

export { LoginPage };
 

React Tutorial Index HTML File

Path: /src/index.html

The base index html file contains the outer html for the whole tutorial application. When the app is started with npm start, Webpack bundles up all of the react code into a single javascript file and injects it into the body of the page.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>React - Basic HTTP Authentication Example & Tutorial</title>
    <link rel="stylesheet" href="https://netdna.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
    <style>
        a { cursor: pointer; }
        .help-block { font-size: 12px; }
    </style>
</head>
<body>
    <div id="app"></div>
</body>
</html>
 

React Tutorial Main Entry File

Path: /src/index.jsx

The root index.jsx file bootstraps the react tutorial application by rendering the App component into the app div element defined in the base index html file above.

The boilerplate application uses a fake / mock backend that stores data in browser local storage, to switch to a real backend api simply remove the fake backend code below the comment // setup fake backend.

import React from 'react';
import { render } from 'react-dom';

import { App } from './App';

// setup fake backend
import { configureFakeBackend } from './_helpers';
configureFakeBackend();

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

React Tutorial Babel RC (Run Commands)

Path: /.babelrc

The babel config file defines the presets used by babel to transpile the React and ES6 code. The babel transpiler is run by webpack via the babel-loader module configured in the webpack.config.js file below.

{
  "presets": [
      "react",
      "env",
      "stage-0"
  ]
}
 

React Tutorial Package.json

Path: /package.json

The package.json file contains project configuration information including package dependencies which get installed when you run npm install. Full documentation is available on the npm docs website.

{
  "name": "react-basic-authentication-example",
  "version": "1.0.0",
  "repository": {
    "type": "git",
    "url": "https://github.com/cornflourblue/react-basic-authentication-example.git"
  },
  "license": "MIT",
  "scripts": {
    "start": "webpack-dev-server --open"
  },
  "dependencies": {
    "react": "^16.0.0",
    "react-dom": "^16.0.0",
    "react-router-dom": "^4.1.2"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.5",
    "babel-preset-env": "^1.6.1",
    "babel-preset-react": "^6.16.0",
    "babel-preset-stage-0": "^6.24.1",
    "html-webpack-plugin": "^3.2.0",
    "path": "^0.12.7",
    "webpack": "^4.15.0",
    "webpack-cli": "^3.0.8",
    "webpack-dev-server": "^3.1.3"
  }
}
 

React Tutorial Webpack Config

Path: /webpack.config.js

Webpack is used to compile and bundle all the project files so they're ready to be loaded into a browser, it does this with the help of loaders and plugins that are configured in the webpack.config.js file. For more info about webpack check out the webpack docs.

The webpack config file also defines a global config object for the application using the externals property, you can also use this to define different config variables for your development and production environments.

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    resolve: {
        extensions: ['.js', '.jsx']
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                loader: 'babel-loader'
            }
        ]
    },
    plugins: [new HtmlWebpackPlugin({
        template: './src/index.html'
    })],
    devServer: {
        historyApiFallback: true
    },
    externals: {
        // global app config object
        config: JSON.stringify({
            apiUrl: 'http://localhost:4000'
        })
    }
}

 

Web Development Sydney

Feel free to contact me if you're looking for a web developer in Sydney, I also provide remote contracting services for clients outside Sydney.

 


Sponsored by