├── src ├── App │ ├── index.js │ └── App.jsx ├── HomePage │ ├── index.js │ └── HomePage.jsx ├── LoginPage │ ├── index.js │ └── LoginPage.jsx ├── _services │ ├── index.js │ └── user.service.js ├── _components │ ├── index.js │ └── PrivateRoute.jsx ├── _helpers │ ├── index.js │ ├── auth-header.js │ └── fake-backend.js ├── index.jsx └── index.html ├── .babelrc ├── README.md ├── .gitignore ├── webpack.config.js ├── package.json └── LICENSE /src/App/index.js: -------------------------------------------------------------------------------- 1 | export * from './App'; -------------------------------------------------------------------------------- /src/HomePage/index.js: -------------------------------------------------------------------------------- 1 | export * from './HomePage'; -------------------------------------------------------------------------------- /src/LoginPage/index.js: -------------------------------------------------------------------------------- 1 | export * from './LoginPage'; -------------------------------------------------------------------------------- /src/_services/index.js: -------------------------------------------------------------------------------- 1 | export * from './user.service'; 2 | -------------------------------------------------------------------------------- /src/_components/index.js: -------------------------------------------------------------------------------- 1 | export * from './PrivateRoute'; 2 | -------------------------------------------------------------------------------- /src/_helpers/index.js: -------------------------------------------------------------------------------- 1 | export * from './fake-backend'; 2 | export * from './auth-header'; -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "env", 5 | "stage-0" 6 | ] 7 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-basic-authentication-example 2 | 3 | React - Basic HTTP Authentication Tutorial & Example 4 | 5 | To see a demo and further details go to http://jasonwatmore.com/post/2018/09/11/react-basic-http-authentication-tutorial-example -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import { App } from './App'; 5 | 6 | // setup fake backend 7 | import { configureFakeBackend } from './_helpers'; 8 | configureFakeBackend(); 9 | 10 | render( 11 | , 12 | document.getElementById('app') 13 | ); -------------------------------------------------------------------------------- /src/_helpers/auth-header.js: -------------------------------------------------------------------------------- 1 | export function authHeader() { 2 | // return authorization header with basic auth credentials 3 | let user = JSON.parse(localStorage.getItem('user')); 4 | 5 | if (user && user.authdata) { 6 | return { 'Authorization': 'Basic ' + user.authdata }; 7 | } else { 8 | return {}; 9 | } 10 | } -------------------------------------------------------------------------------- /src/_components/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | 4 | export const PrivateRoute = ({ component: Component, ...rest }) => ( 5 | ( 6 | localStorage.getItem('user') 7 | ? 8 | : 9 | )} /> 10 | ) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | typings 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | resolve: { 7 | extensions: ['.js', '.jsx'] 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.jsx?$/, 13 | loader: 'babel-loader' 14 | } 15 | ] 16 | }, 17 | plugins: [new HtmlWebpackPlugin({ 18 | template: './src/index.html' 19 | })], 20 | devServer: { 21 | historyApiFallback: true 22 | }, 23 | externals: { 24 | // global app config object 25 | config: JSON.stringify({ 26 | apiUrl: 'http://localhost:4000' 27 | }) 28 | } 29 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-basic-authentication-example", 3 | "version": "1.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/cornflourblue/react-basic-authentication-example.git" 7 | }, 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "webpack-dev-server --open" 11 | }, 12 | "dependencies": { 13 | "react": "^16.0.0", 14 | "react-dom": "^16.0.0", 15 | "react-router-dom": "^4.1.2" 16 | }, 17 | "devDependencies": { 18 | "babel-core": "^6.26.0", 19 | "babel-loader": "^7.1.5", 20 | "babel-preset-env": "^1.6.1", 21 | "babel-preset-react": "^6.16.0", 22 | "babel-preset-stage-0": "^6.24.1", 23 | "html-webpack-plugin": "^3.2.0", 24 | "path": "^0.12.7", 25 | "webpack": "^4.15.0", 26 | "webpack-cli": "^3.0.8", 27 | "webpack-dev-server": "^3.1.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React - Basic HTTP Authentication Example & Tutorial 6 | 7 | 11 | 12 | 13 |
14 | 15 | 16 | 24 | 25 | -------------------------------------------------------------------------------- /src/App/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 3 | 4 | import { PrivateRoute } from '../_components'; 5 | import { HomePage } from '../HomePage'; 6 | import { LoginPage } from '../LoginPage'; 7 | 8 | class App extends React.Component { 9 | render() { 10 | return ( 11 |
12 |
13 |
14 | 15 |
16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 | ); 24 | } 25 | } 26 | 27 | export { App }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jason Watmore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/HomePage/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { userService } from '../_services'; 5 | 6 | class HomePage extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | user: {}, 12 | users: [] 13 | }; 14 | } 15 | 16 | componentDidMount() { 17 | this.setState({ 18 | user: JSON.parse(localStorage.getItem('user')), 19 | users: { loading: true } 20 | }); 21 | userService.getAll().then(users => this.setState({ users })); 22 | } 23 | 24 | render() { 25 | const { user, users } = this.state; 26 | return ( 27 |
28 |

Hi {user.firstName}!

29 |

You're logged in with React & Basic HTTP Authentication!!

30 |

Users from secure api end point:

31 | {users.loading && Loading users...} 32 | {users.length && 33 |
    34 | {users.map((user, index) => 35 |
  • 36 | {user.firstName + ' ' + user.lastName} 37 |
  • 38 | )} 39 |
40 | } 41 |

42 | Logout 43 |

44 |
45 | ); 46 | } 47 | } 48 | 49 | export { HomePage }; -------------------------------------------------------------------------------- /src/_services/user.service.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import { authHeader } from '../_helpers'; 3 | 4 | export const userService = { 5 | login, 6 | logout, 7 | getAll 8 | }; 9 | 10 | function login(username, password) { 11 | const requestOptions = { 12 | method: 'POST', 13 | headers: { 'Content-Type': 'application/json' }, 14 | body: JSON.stringify({ username, password }) 15 | }; 16 | 17 | return fetch(`${config.apiUrl}/users/authenticate`, requestOptions) 18 | .then(handleResponse) 19 | .then(user => { 20 | // login successful if there's a user in the response 21 | if (user) { 22 | // store user details and basic auth credentials in local storage 23 | // to keep user logged in between page refreshes 24 | user.authdata = window.btoa(username + ':' + password); 25 | localStorage.setItem('user', JSON.stringify(user)); 26 | } 27 | 28 | return user; 29 | }); 30 | } 31 | 32 | function logout() { 33 | // remove user from local storage to log user out 34 | localStorage.removeItem('user'); 35 | } 36 | 37 | function getAll() { 38 | const requestOptions = { 39 | method: 'GET', 40 | headers: authHeader() 41 | }; 42 | 43 | return fetch(`${config.apiUrl}/users`, requestOptions).then(handleResponse); 44 | } 45 | 46 | function handleResponse(response) { 47 | return response.text().then(text => { 48 | const data = text && JSON.parse(text); 49 | if (!response.ok) { 50 | if (response.status === 401) { 51 | // auto logout if 401 response returned from api 52 | logout(); 53 | location.reload(true); 54 | } 55 | 56 | const error = (data && data.message) || response.statusText; 57 | return Promise.reject(error); 58 | } 59 | 60 | return data; 61 | }); 62 | } -------------------------------------------------------------------------------- /src/_helpers/fake-backend.js: -------------------------------------------------------------------------------- 1 | export function configureFakeBackend() { 2 | let users = [{ id: 1, username: 'test', password: 'test', firstName: 'Test', lastName: 'User' }]; 3 | let realFetch = window.fetch; 4 | window.fetch = function (url, opts) { 5 | return new Promise((resolve, reject) => { 6 | // wrap in timeout to simulate server api call 7 | setTimeout(() => { 8 | 9 | // authenticate 10 | if (url.endsWith('/users/authenticate') && opts.method === 'POST') { 11 | // get parameters from post request 12 | let params = JSON.parse(opts.body); 13 | 14 | // find if any user matches login credentials 15 | let filteredUsers = users.filter(user => { 16 | return user.username === params.username && user.password === params.password; 17 | }); 18 | 19 | if (filteredUsers.length) { 20 | // if login details are valid return user details 21 | let user = filteredUsers[0]; 22 | let responseJson = { 23 | id: user.id, 24 | username: user.username, 25 | firstName: user.firstName, 26 | lastName: user.lastName 27 | }; 28 | resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(responseJson)) }); 29 | } else { 30 | // else return error 31 | reject('Username or password is incorrect'); 32 | } 33 | 34 | return; 35 | } 36 | 37 | // get users 38 | if (url.endsWith('/users') && opts.method === 'GET') { 39 | // check for fake auth token in header and return users if valid, this security 40 | // is implemented server side in a real application 41 | if (opts.headers && opts.headers.Authorization === `Basic ${window.btoa('test:test')}`) { 42 | resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(users)) }); 43 | } else { 44 | // return 401 not authorised if token is null or invalid 45 | resolve({ status: 401, text: () => Promise.resolve() }); 46 | } 47 | 48 | return; 49 | } 50 | 51 | // pass through any requests not handled above 52 | realFetch(url, opts).then(response => resolve(response)); 53 | 54 | }, 500); 55 | }); 56 | } 57 | } -------------------------------------------------------------------------------- /src/LoginPage/LoginPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { userService } from '../_services'; 4 | 5 | class LoginPage extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | userService.logout(); 10 | 11 | this.state = { 12 | username: '', 13 | password: '', 14 | submitted: false, 15 | loading: false, 16 | error: '' 17 | }; 18 | 19 | this.handleChange = this.handleChange.bind(this); 20 | this.handleSubmit = this.handleSubmit.bind(this); 21 | } 22 | 23 | handleChange(e) { 24 | const { name, value } = e.target; 25 | this.setState({ [name]: value }); 26 | } 27 | 28 | handleSubmit(e) { 29 | e.preventDefault(); 30 | 31 | this.setState({ submitted: true }); 32 | const { username, password, returnUrl } = this.state; 33 | 34 | // stop here if form is invalid 35 | if (!(username && password)) { 36 | return; 37 | } 38 | 39 | this.setState({ loading: true }); 40 | userService.login(username, password) 41 | .then( 42 | user => { 43 | const { from } = this.props.location.state || { from: { pathname: "/" } }; 44 | this.props.history.push(from); 45 | }, 46 | error => this.setState({ error, loading: false }) 47 | ); 48 | } 49 | 50 | render() { 51 | const { username, password, submitted, loading, error } = this.state; 52 | return ( 53 |
54 |
55 | Username: test
56 | Password: test 57 |
58 |

Login

59 |
60 |
61 | 62 | 63 | {submitted && !username && 64 |
Username is required
65 | } 66 |
67 |
68 | 69 | 70 | {submitted && !password && 71 |
Password is required
72 | } 73 |
74 |
75 | 76 | {loading && 77 | 78 | } 79 |
80 | {error && 81 |
{error}
82 | } 83 |
84 |
85 | ); 86 | } 87 | } 88 | 89 | export { LoginPage }; --------------------------------------------------------------------------------