├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
├── AdminPage
│ ├── AdminPage.jsx
│ └── index.js
├── App
│ ├── App.jsx
│ └── index.js
├── HomePage
│ ├── HomePage.jsx
│ └── index.js
├── LoginPage
│ ├── LoginPage.jsx
│ └── index.js
├── _components
│ ├── PrivateRoute.jsx
│ └── index.js
├── _helpers
│ ├── auth-header.js
│ ├── fake-backend.js
│ ├── handle-response.js
│ ├── history.js
│ ├── index.js
│ └── role.js
├── _services
│ ├── authentication.service.js
│ ├── index.js
│ └── user.service.js
├── index.html
└── index.jsx
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "react",
4 | "env",
5 | "stage-0"
6 | ]
7 | }
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-role-based-authorization-example
2 |
3 | React - Role Based Authorization Tutorial & Example
4 |
5 | To see a demo and further details go to http://jasonwatmore.com/post/2019/02/01/react-role-based-authorization-tutorial-with-example
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-role-based-authorization-example",
3 | "version": "1.0.0",
4 | "repository": {
5 | "type": "git",
6 | "url": "https://github.com/cornflourblue/react-role-based-authorization-example.git"
7 | },
8 | "license": "MIT",
9 | "scripts": {
10 | "start": "webpack-dev-server --open"
11 | },
12 | "dependencies": {
13 | "formik": "^1.4.2",
14 | "history": "^4.7.2",
15 | "react": "^16.0.0",
16 | "react-dom": "^16.0.0",
17 | "react-router-dom": "^4.1.2",
18 | "rxjs": "^6.3.3",
19 | "yup": "^0.26.10"
20 | },
21 | "devDependencies": {
22 | "babel-core": "^6.26.0",
23 | "babel-loader": "^7.1.5",
24 | "babel-preset-env": "^1.6.1",
25 | "babel-preset-react": "^6.16.0",
26 | "babel-preset-stage-0": "^6.24.1",
27 | "html-webpack-plugin": "^3.2.0",
28 | "path": "^0.12.7",
29 | "webpack": "^4.15.0",
30 | "webpack-cli": "^3.0.8",
31 | "webpack-dev-server": "^3.1.3"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/AdminPage/AdminPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { userService } from '@/_services';
4 |
5 | class AdminPage extends React.Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = {
10 | users: null
11 | };
12 | }
13 |
14 | componentDidMount() {
15 | userService.getAll().then(users => this.setState({ users }));
16 | }
17 |
18 | render() {
19 | const { users } = this.state;
20 | return (
21 |
22 |
Admin
23 |
This page can only be accessed by administrators.
24 |
25 | All users from secure (admin only) api end point:
26 | {users &&
27 |
28 | {users.map(user =>
29 | - {user.firstName} {user.lastName}
30 | )}
31 |
32 | }
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | export { AdminPage };
--------------------------------------------------------------------------------
/src/AdminPage/index.js:
--------------------------------------------------------------------------------
1 | export * from './AdminPage';
--------------------------------------------------------------------------------
/src/App/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Router, Route, Link } from 'react-router-dom';
3 |
4 | import { history, Role } from '@/_helpers';
5 | import { authenticationService } from '@/_services';
6 | import { PrivateRoute } from '@/_components';
7 | import { HomePage } from '@/HomePage';
8 | import { AdminPage } from '@/AdminPage';
9 | import { LoginPage } from '@/LoginPage';
10 |
11 | class App extends React.Component {
12 | constructor(props) {
13 | super(props);
14 |
15 | this.state = {
16 | currentUser: null,
17 | isAdmin: false
18 | };
19 | }
20 |
21 | componentDidMount() {
22 | authenticationService.currentUser.subscribe(x => this.setState({
23 | currentUser: x,
24 | isAdmin: x && x.role === Role.Admin
25 | }));
26 | }
27 |
28 | logout() {
29 | authenticationService.logout();
30 | history.push('/login');
31 | }
32 |
33 | render() {
34 | const { currentUser, isAdmin } = this.state;
35 | return (
36 |
37 |
38 | {currentUser &&
39 |
46 | }
47 |
58 |
59 |
60 | );
61 | }
62 | }
63 |
64 | export { App };
--------------------------------------------------------------------------------
/src/App/index.js:
--------------------------------------------------------------------------------
1 | export * from './App';
--------------------------------------------------------------------------------
/src/HomePage/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { userService, authenticationService } from '@/_services';
4 |
5 | class HomePage extends React.Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = {
10 | currentUser: authenticationService.currentUserValue,
11 | userFromApi: null
12 | };
13 | }
14 |
15 | componentDidMount() {
16 | const { currentUser } = this.state;
17 | userService.getById(currentUser.id).then(userFromApi => this.setState({ userFromApi }));
18 | }
19 |
20 | render() {
21 | const { currentUser, userFromApi } = this.state;
22 | return (
23 |
24 |
Home
25 |
You're logged in with React & JWT!!
26 |
Your role is: {currentUser.role}.
27 |
This page can be accessed by all authenticated users.
28 |
29 | Current user from secure api end point:
30 | {userFromApi &&
31 |
32 | - {userFromApi.firstName} {userFromApi.lastName}
33 |
34 | }
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | export { HomePage };
--------------------------------------------------------------------------------
/src/HomePage/index.js:
--------------------------------------------------------------------------------
1 | export * from './HomePage';
--------------------------------------------------------------------------------
/src/LoginPage/LoginPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Formik, Field, Form, ErrorMessage } from 'formik';
3 | import * as Yup from 'yup';
4 |
5 | import { authenticationService } from '@/_services';
6 |
7 | class LoginPage extends React.Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | // redirect to home if already logged in
12 | if (authenticationService.currentUserValue) {
13 | this.props.history.push('/');
14 | }
15 | }
16 |
17 | render() {
18 | return (
19 |
20 |
21 | Normal User - U: user P: user
22 | Administrator - U: admin P: admin
23 |
24 |
Login
25 |
{
35 | setStatus();
36 | authenticationService.login(username, password)
37 | .then(
38 | user => {
39 | const { from } = this.props.location.state || { from: { pathname: "/" } };
40 | this.props.history.push(from);
41 | },
42 | error => {
43 | setSubmitting(false);
44 | setStatus(error);
45 | }
46 | );
47 | }}
48 | render={({ errors, status, touched, isSubmitting }) => (
49 |
70 | )}
71 | />
72 |
73 | )
74 | }
75 | }
76 |
77 | export { LoginPage };
--------------------------------------------------------------------------------
/src/LoginPage/index.js:
--------------------------------------------------------------------------------
1 | export * from './LoginPage';
--------------------------------------------------------------------------------
/src/_components/PrivateRoute.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Redirect } from 'react-router-dom';
3 |
4 | import { authenticationService } from '@/_services';
5 |
6 | export const PrivateRoute = ({ component: Component, roles, ...rest }) => (
7 | {
8 | const currentUser = authenticationService.currentUserValue;
9 | if (!currentUser) {
10 | // not logged in so redirect to login page with the return url
11 | return
12 | }
13 |
14 | // check if route is restricted by role
15 | if (roles && roles.indexOf(currentUser.role) === -1) {
16 | // role not authorised so redirect to home page
17 | return
18 | }
19 |
20 | // authorised so return component
21 | return
22 | }} />
23 | )
--------------------------------------------------------------------------------
/src/_components/index.js:
--------------------------------------------------------------------------------
1 | export * from './PrivateRoute';
2 |
--------------------------------------------------------------------------------
/src/_helpers/auth-header.js:
--------------------------------------------------------------------------------
1 | import { authenticationService } from '@/_services';
2 |
3 | export function authHeader() {
4 | // return authorization header with jwt token
5 | const currentUser = authenticationService.currentUserValue;
6 | if (currentUser && currentUser.token) {
7 | return { Authorization: `Bearer ${currentUser.token}` };
8 | } else {
9 | return {};
10 | }
11 | }
--------------------------------------------------------------------------------
/src/_helpers/fake-backend.js:
--------------------------------------------------------------------------------
1 | import { Role } from './'
2 |
3 | export function configureFakeBackend() {
4 | let users = [
5 | { id: 1, username: 'admin', password: 'admin', firstName: 'Admin', lastName: 'User', role: Role.Admin },
6 | { id: 2, username: 'user', password: 'user', firstName: 'Normal', lastName: 'User', role: Role.User }
7 | ];
8 | let realFetch = window.fetch;
9 | window.fetch = function (url, opts) {
10 | const authHeader = opts.headers['Authorization'];
11 | const isLoggedIn = authHeader && authHeader.startsWith('Bearer fake-jwt-token');
12 | const roleString = isLoggedIn && authHeader.split('.')[1];
13 | const role = roleString ? Role[roleString] : null;
14 |
15 | return new Promise((resolve, reject) => {
16 | // wrap in timeout to simulate server api call
17 | setTimeout(() => {
18 | // authenticate - public
19 | if (url.endsWith('/users/authenticate') && opts.method === 'POST') {
20 | const params = JSON.parse(opts.body);
21 | const user = users.find(x => x.username === params.username && x.password === params.password);
22 | if (!user) return error('Username or password is incorrect');
23 | return ok({
24 | id: user.id,
25 | username: user.username,
26 | firstName: user.firstName,
27 | lastName: user.lastName,
28 | role: user.role,
29 | token: `fake-jwt-token.${user.role}`
30 | });
31 | }
32 |
33 | // get user by id - admin or user (user can only access their own record)
34 | if (url.match(/\/users\/\d+$/) && opts.method === 'GET') {
35 | if (!isLoggedIn) return unauthorised();
36 |
37 | // get id from request url
38 | let urlParts = url.split('/');
39 | let id = parseInt(urlParts[urlParts.length - 1]);
40 |
41 | // only allow normal users access to their own record
42 | const currentUser = users.find(x => x.role === role);
43 | if (id !== currentUser.id && role !== Role.Admin) return unauthorised();
44 |
45 | const user = users.find(x => x.id === id);
46 | return ok(user);
47 | }
48 |
49 | // get all users - admin only
50 | if (url.endsWith('/users') && opts.method === 'GET') {
51 | if (role !== Role.Admin) return unauthorised();
52 | return ok(users);
53 | }
54 |
55 | // pass through any requests not handled above
56 | realFetch(url, opts).then(response => resolve(response));
57 |
58 | // private helper functions
59 |
60 | function ok(body) {
61 | resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(body)) })
62 | }
63 |
64 | function unauthorised() {
65 | resolve({ status: 401, text: () => Promise.resolve(JSON.stringify({ message: 'Unauthorised' })) })
66 | }
67 |
68 | function error(message) {
69 | resolve({ status: 400, text: () => Promise.resolve(JSON.stringify({ message })) })
70 | }
71 | }, 500);
72 | });
73 | }
74 | }
--------------------------------------------------------------------------------
/src/_helpers/handle-response.js:
--------------------------------------------------------------------------------
1 | import { authenticationService } from '@/_services';
2 |
3 | export function handleResponse(response) {
4 | return response.text().then(text => {
5 | const data = text && JSON.parse(text);
6 | if (!response.ok) {
7 | if ([401, 403].indexOf(response.status) !== -1) {
8 | // auto logout if 401 Unauthorized or 403 Forbidden response returned from api
9 | authenticationService.logout();
10 | location.reload(true);
11 | }
12 |
13 | const error = (data && data.message) || response.statusText;
14 | return Promise.reject(error);
15 | }
16 |
17 | return data;
18 | });
19 | }
--------------------------------------------------------------------------------
/src/_helpers/history.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history';
2 |
3 | export const history = createBrowserHistory();
--------------------------------------------------------------------------------
/src/_helpers/index.js:
--------------------------------------------------------------------------------
1 | export * from './auth-header';
2 | export * from './fake-backend';
3 | export * from './handle-response';
4 | export * from './history';
5 | export * from './role';
--------------------------------------------------------------------------------
/src/_helpers/role.js:
--------------------------------------------------------------------------------
1 | export const Role = {
2 | Admin: 'Admin',
3 | User: 'User'
4 | }
--------------------------------------------------------------------------------
/src/_services/authentication.service.js:
--------------------------------------------------------------------------------
1 | import { BehaviorSubject } from 'rxjs';
2 |
3 | import config from 'config';
4 | import { handleResponse } from '@/_helpers';
5 |
6 | const currentUserSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('currentUser')));
7 |
8 | export const authenticationService = {
9 | login,
10 | logout,
11 | currentUser: currentUserSubject.asObservable(),
12 | get currentUserValue () { return currentUserSubject.value }
13 | };
14 |
15 | function login(username, password) {
16 | const requestOptions = {
17 | method: 'POST',
18 | headers: { 'Content-Type': 'application/json' },
19 | body: JSON.stringify({ username, password })
20 | };
21 |
22 | return fetch(`${config.apiUrl}/users/authenticate`, requestOptions)
23 | .then(handleResponse)
24 | .then(user => {
25 | // store user details and jwt token in local storage to keep user logged in between page refreshes
26 | localStorage.setItem('currentUser', JSON.stringify(user));
27 | currentUserSubject.next(user);
28 |
29 | return user;
30 | });
31 | }
32 |
33 | function logout() {
34 | // remove user from local storage to log user out
35 | localStorage.removeItem('currentUser');
36 | currentUserSubject.next(null);
37 | }
38 |
--------------------------------------------------------------------------------
/src/_services/index.js:
--------------------------------------------------------------------------------
1 | export * from './authentication.service';
2 | export * from './user.service';
3 |
--------------------------------------------------------------------------------
/src/_services/user.service.js:
--------------------------------------------------------------------------------
1 | import config from 'config';
2 | import { authHeader, handleResponse } from '@/_helpers';
3 |
4 | export const userService = {
5 | getAll,
6 | getById
7 | };
8 |
9 | function getAll() {
10 | const requestOptions = { method: 'GET', headers: authHeader() };
11 | return fetch(`${config.apiUrl}/users`, requestOptions).then(handleResponse);
12 | }
13 |
14 | function getById(id) {
15 | const requestOptions = { method: 'GET', headers: authHeader() };
16 | return fetch(`${config.apiUrl}/users/${id}`, requestOptions).then(handleResponse);
17 | }
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React - Role Based Authorization Tutorial & Example
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
26 |
27 |
--------------------------------------------------------------------------------
/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 | );
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const path = require('path');
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 | resolve: {
18 | extensions: ['.js', '.jsx'],
19 | alias: {
20 | '@': path.resolve(__dirname, 'src/'),
21 | }
22 | },
23 | plugins: [new HtmlWebpackPlugin({
24 | template: './src/index.html'
25 | })],
26 | devServer: {
27 | historyApiFallback: true
28 | },
29 | externals: {
30 | // global app config object
31 | config: JSON.stringify({
32 | apiUrl: 'http://localhost:4000'
33 | })
34 | }
35 | }
--------------------------------------------------------------------------------