Implementing protected route and authentication in react-js – dev community

Add css style for react components

Open src/App.css and write some CSS code as following:

Adding our actions

Redux’ docs describe actions as “payloads of information that send data from your application to your store. They are the only source of information for the store.”

App (basic version)

At this stage, our App contains our Router and outputs our Nav on each page.

import * as React from "react";
import { Route, Router } from "react-router-dom";
import history from "./history";
import Nav from "./components/Nav";
import Pages from "./routes/Pages";
const App = () => (
<Router history={history}>
<Nav />
<Route component={Pages} />
</Router>
);
export default App;

Basic routing

Now we’ve got our base Redux setup in place, we can start implementing our route components. As we discussed at the start of the post, we want to have three route components by the end, LoggedInRoute, LoggedOutRoute, and the standard Route component.

Constants

Constants can seem a little unneccessary, and in smaller projects it’s a matter of preference as to whether they’re worth the little extra complexity they bring. At Octopus Wealth we utilise constants as they work nicely with TypeScript in catching typo’s. You can read Redux creator Dan Abramov’s thoughts on constants here.

export const AUTHENTICATE = "AUTHENTICATE";
export type AUTHENTICATE = typeof AUTHENTICATE;
export const UNAUTHENTICATE = "UNAUTHENTICATE";
export type UNAUTHENTICATE = typeof UNAUTHENTICATE;

Create react components for authentication

In src folder, create new folder named components and add several files as following:

Create services

We’re gonna create two services in src/services folder:

Creating our components: panel and signin

First of all, in our src folder, we will create a new folder which is called screens. Here we will create Panel.js and SignIn.js. I will use bootstrap to style my components faster. If you want to do the same and you don’t know how to install bootstrap, please look here.

Похожее:  ПОРТАЛ ЕДИНОГО КУРСА ВОЙДИТЕ В ЛИЧНЫЙ КАБИНЕТ

In src/screens/Panel.js:

In src/screens/SignIn.js:

Now we need to create our router. We will do it in App.js. For navigation in our app, we will be using react-router-dom. We need to install it with yarn or npm:

Now in src/App.js we will create routes for our app.

First things first,

Install all dependencies

  • npm i react-router react-router-dom
  • Add link to bootstrap CDN in ./public/index.html header

React-router will handle our routing, i.e switching from one page to another within the web application.

Note: We’ll be building ontop of the last project from Structuring redux in a web app

Further reading

Fullstack CRUD:– React Spring Boot MySQL– React Spring Boot PostgreSQL– React Spring Boot MongoDB– React Node.js Express MySQL– React Node.js Express PostgreSQL– React.js Node.js Express MongoDB– React Django

Another way to implement Form Validation:React Form Validation example with Formik and Yup

Getting started

This post is based on using create-react-app with Typescript. To get started, run:

create-react-app your-app-name --typescript
cd your-app-name
yarn
yarn run start

See the Getting Started post from React for more info on create-react-app.

History

The history file uses the history npm module to keep track of the browser history. Say history more, I hear you say?

// tslint:disable:interface-name
import { createBrowserHistory, Location } from "history";
declare global {
interface Window {
dataLayer: any;
}
}
const history = createBrowserHistory();export default history;

Home (example page)

For simplicity of this demo, our pages are functional components that return some JSX.

You can get the code for the rest of the pages in the example repo.

import * as React from "react";const Home = () => (
<p>Logged in home page</p>
);
export default Home;

Home page

This is a public page that shows public content. People don’t need to log in to view this page.

How to implement authenticated routes in react router 4?

(Using Redux for state management)

If user try to access any url, first i am going to check if access token available, if not redirect to login page,
Once user logs in using login page, we do store that in localstorage as well as in our redux state. (localstorage or cookies..we keep this topic out of context for now).
since redux state as updated and privateroutes will be rerendered. now we do have access token so we gonna redirect to home page.

Store the decoded authorization payload data as well in redux state and pass it to react context. (We dont have to use context but to access authorization in any of our nested child components it makes easy to access from context instead connecting each and every child component to redux)..

All the routes that don’t need special roles can be accessed directly after login.. If it need role like admin (we made a protected route which checks whether he had desired role if not redirects to unauthorized component)

similarly in any of your component if you have to disable button or something based on role.

simply you can do in this way

const authorization = useContext(AuthContext);
const [hasAdminRole] = checkAuth({authorization, roleType:"admin"});
const [hasLeadRole] = checkAuth({authorization, roleType:"lead"});
<Button disable={!hasAdminRole} />Admin can access</Button>
<Button disable={!hasLeadRole || !hasAdminRole} />admin or lead can access</Button>

So what if user try to insert dummy token in localstorage. As we do have access token, we will redirect to home component. My home component will make rest call to grab data, since jwt token was dummy, rest call will return unauthorized user. So i do call logout (which will clear localstorage and redirect to login page again).
If home page has static data and not making any api calls(then you should have token-verify api call in the backend so that you can check if token is REAL before loading home page)

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, Switch } from 'react-router-dom';
import history from './utils/history';


import Store from './statemanagement/store/configureStore';
import Privateroutes from './Privateroutes';
import Logout from './components/auth/Logout';

ReactDOM.render(
  <Store>
    <Router history={history}>
      <Switch>
        <Route path="/logout" exact component={Logout} />
        <Route path="/" exact component={Privateroutes} />
        <Route path="/:someParam" component={Privateroutes} />
      </Switch>
    </Router>
  </Store>,
  document.querySelector('#root')
);

History.js

import { createBrowserHistory as history } from 'history';

export default history({});

Privateroutes.js

import React, { Fragment, useContext } from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import { AuthContext, checkAuth } from './checkAuth';
import App from './components/App';
import Home from './components/home';
import Admin from './components/admin';
import Login from './components/auth/Login';
import Unauthorized from './components/Unauthorized ';
import Notfound from './components/404';

const ProtectedRoute = ({ component: Component, roleType, ...rest })=> { 
const authorization = useContext(AuthContext);
const [hasRequiredRole] = checkAuth({authorization, roleType});
return (
<Route
  {...rest}
  render={props => hasRequiredRole ? 
  <Component {...props} /> :
   <Unauthorized {...props} />  } 
/>)}; 

const Privateroutes = props => {
  const { accessToken, authorization } = props.authData;
  if (accessToken) {
    return (
      <Fragment>
       <AuthContext.Provider value={authorization}>
        <App>
          <Switch>
            <Route exact path="/" component={Home} />
            <Route path="/login" render={() => <Redirect to="/" />} />
            <Route exact path="/home" component={Home} />
            <ProtectedRoute
            exact
            path="/admin"
            component={Admin}
            roleType="admin"
          />
            <Route path="/404" component={Notfound} />
            <Route path="*" render={() => <Redirect to="/404" />} />
          </Switch>
        </App>
        </AuthContext.Provider>
      </Fragment>
    );
  } else {
    return (
      <Fragment>
        <Route exact path="/login" component={Login} />
        <Route exact path="*" render={() => <Redirect to="/login" />} />
      </Fragment>
    );
  }
};

// my user reducer sample
// const accessToken = localStorage.getItem('token')
//   ? JSON.parse(localStorage.getItem('token')).accessToken
//   : false;

// const initialState = {
//   accessToken: accessToken ? accessToken : null,
//   authorization: accessToken
//     ? jwtDecode(JSON.parse(localStorage.getItem('token')).accessToken)
//         .authorization
//     : null
// };

// export default function(state = initialState, action) {
// switch (action.type) {
// case actionTypes.FETCH_LOGIN_SUCCESS:
//   let token = {
//                  accessToken: action.payload.token
//               };
//   localStorage.setItem('token', JSON.stringify(token))
//   return {
//     ...state,
//     accessToken: action.payload.token,
//     authorization: jwtDecode(action.payload.token).authorization
//   };
//    default:
//         return state;
//    }
//    }

const mapStateToProps = state => {
  const { authData } = state.user;
  return {
    authData: authData
  };
};

export default connect(mapStateToProps)(Privateroutes);

checkAuth.js

import React from 'react';

export const AuthContext = React.createContext();

export const checkAuth = ({ authorization, roleType }) => {
  let hasRequiredRole = false;

  if (authorization.roles ) {
    let roles = authorization.roles.map(item =>
      item.toLowerCase()
    );

    hasRequiredRole = roles.includes(roleType);
  }

  return [hasRequiredRole];
};

DECODED JWT TOKEN SAMPLE

{
  "authorization": {
    "roles": [
      "admin",
      "operator"
    ]
  },
  "exp": 1591733170,
  "user_id": 1,
  "orig_iat": 1591646770,
  "email": "hemanthvrm@stackoverflow",
  "username": "hemanthvrm"
}

Index

To get our new async logIn and logOut functions working, we need to use Redux Thunk’s middleware.

Redux Thunk’s docs explain the need for this well:

With a plain basic Redux store, you can only do simple synchronous updates by dispatching an action. Middleware extend the store’s abilities, and let you write async logic that interacts with the store.

Thunks are the recommended middleware for basic Redux side effects logic, including complex synchronous logic that needs access to the store, and simple async logic like AJAX requests.

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { applyMiddleware, compose, createStore } from "redux";
import thunkMiddleware from "redux-thunk-recursion-detect";
import "./index.css";
import App from "./App";
import currentReducer from "./reducers/current";
import * as serviceWorker from "./serviceWorker";
import { ICurrent } from "./types";
let composeEnhancers;if (
process.env.NODE_ENV !== "production" &&
(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
) {
composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
} else {
composeEnhancers = compose;
}
const store = createStore<ICurrent, any, any, any>(
currentReducer,
undefined,
composeEnhancers(applyMiddleware(thunkMiddleware)),
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();

Index (basic version)

The index file is where it all gets going. Here we initialise React, enable Redux DevTools when not in production, and pass our Redux store down to any child component that wants to consume it.

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { compose, createStore } from "redux";
import "./index.css";
import App from "./App";
import currentReducer from "./reducers/current";
import * as serviceWorker from "./serviceWorker";
import { ICurrent } from "./types";
let composeEnhancers;if (
process.env.NODE_ENV !== "production" &&
(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
) {
composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
} else {
composeEnhancers = compose;
}
const store = createStore<ICurrent, any, any, any>(
currentReducer,
undefined,
composeEnhancers(),
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();

Log in page (basic version)

In this basic version of the log in page, implement the connect function from Redux, which consumes the store we’ll pass down via the Provider in index (see below) and passes through our actions as props ready to be dispatched. Clicking the log me in button will update our Redux store.

import * as React from "react";
import { connect } from "react-redux";
import { authenticate } from "../../actions/current";interface IProps {
authenticateConnect: () => void;
}
const LogIn = ({ authenticateConnect }: IProps) => (
<>
<p>Login page</p>
<button onClick={authenticateConnect}>log me in</button>
</>
);
const mapDispatchToProps = {
authenticateConnect: authenticate
};
export default connect(
null,
mapDispatchToProps,
)(LogIn);

Loggedinroute (basic version)

Here we can define any custom components that all LoggedInRoute’s should show.

Login and logout pages

Next we need to pull through our logIn and logOut functions so our LogIn and LogOut pages include the localStorage side effects.

import * as React from "react";
import { connect } from "react-redux";
import { logIn } from "../../actions/current";interface IProps {
logInConnect: () => void;
}
const LogIn = ({ logInConnect }: IProps) => (
<>
<p>Login page</p>
<button onClick={logInConnect}>log me in</button>
</>
);
const mapDispatchToProps = {
logInConnect: logIn
};
export default connect(
null,
mapDispatchToProps,
)(LogIn);

Logout when the token is expired

There are two ways. For more details, please visit:React – How to Logout when JWT Token is expired

Overview of react jwt authentication example

We will build a React application in that:

Here are the screenshots:– Signup Page:

– Form Validation Support:

– Login Page:

– Profile Page (for successful Login):

– For Moderator account login, the navigation bar will change by authorities:

– Check Browser Local Storage:

If you want to add refresh token, please visit:React Refresh Token with JWT and Axios Interceptors

Project structure

This is folders & files structure for this React application:

With the explanation in diagram above, you can understand the project structure easily.

Register page

This page is similar to Login Page.

For Form Validation, there are some more details:

We’re gonna call AuthService.register() method and show response message (successful or error).

Role-based pages

We’re gonna have 3 pages for accessing protected data:

Setup react.js project

Open cmd at the folder you want to save Project folder, run command:npx create-react-app react-jwt-auth

Signin component

In ./src/view/Authentication/Signin.js, let’s create the Signin component,
Signin Component

Simple redux store state with reducers

Redux’ official docs states that “Reducers specify how the application’s state changes in response to actions sent to the store. Remember that actions only describe what happened, but don’t describe how the application’s state changes.”

You’ll likely have a much more complex setup than this, utilising multiple reducers and combining them together, but to keep this simple, I’ve stripped it back just to what we need to get authentication working:

import { IAuthenticate, IUnauthenticate } from "../actions/current";
import { AUTHENTICATE, UNAUTHENTICATE } from "../constants";
import { ICurrent } from "../types";
export default function currentReducer(
state: ICurrent = {
uuid: null,
isAuthenticated: false,
},
action: IAuthenticate | IUnauthenticate,
): ICurrent {
switch (action.type) {
case AUTHENTICATE:
return {
...state, uuid: "placeholder-uuid", isAuthenticated: true
};
case UNAUTHENTICATE:
return { uuid: null, isAuthenticated: false }
}
return state;
}

This setup gives us our state that we’ll later provide down throughout our app.

With our reducer in place, we’ll setup our actions.

Technology

We’re gonna use these modules:

  • React 16
  • react-router-dom 5.1.2
  • axios 0.19.2
  • react-validation 3.0.7
  • Bootstrap 4
  • validator 12.2.0

Test run the application

When the application is launched, it’ll redirect to the Sign-in page,
signin

The good stuff

Now we’ve got our basics in place, let’s start wiring in the good stuff.

Types

With this change, we need to update our types as isAuthenticated will no longer always be a boolean.

export interface ICurrent {
isAuthenticated: boolean | null;
uuid: string | null;
}

Инициализация

Во-первых, нам нужно создать роутер, сага-мидлевар, стор и запустить сагу.

Компонент main

Main является компонентом контейнера без состояния, который будет перенаправлять на Login, если нет токена.

// Main.js
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { Route, Redirect } from 'react-router-dom';

const Main = ({ token }) => {
  if (!token) {
    return <Redirect to="/login" />;
  }
  return <div>Вы вошли в систему.</div>;
};

const mapStateToProps = (state) => ({
  token: state.auth.token
});

export default connect(mapStateToProps)(Main);

Редьюсер

Состояние приложения будет состоять из двух частей: одно для auth и одно для роутера. Редьюсер auth будет обрабатывать события аутентификации.

// reducer.js
import { combineReducers } from 'redux'
import { routerReducer } from 'react-router-redux';

export const AUTH_REQUEST = 'AUTH_REQUEST';
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
export const AUTH_FAILURE = 'AUTH_FAILURE';

export const authorize = (login, password) => ({
  type: AUTH_REQUEST,
  payload: { login, password }
});

const initialState = {
  token: localStorage.getItem('token'),
  error: null
};

const authReducer = (state = initialState, { type, payload }) => {
  switch (type) {
    case AUTH_SUCCESS: {
      return { ...state, token: payload };
    }
    case AUTH_FAILURE: {
      return { ...state, error: payload }
    }
    default:
      return state;
  }
};

const reducer = combineReducers({
  auth: authReducer,
  router: routerReducer
});

export default reducer;

Conclusion

Congratulation!

Today we’ve done so many interesting things. I hope you understand the overall layers of our React JWT Authentication App (without Redux) using LocalStorage, React Router, Axios, Bootstrap. Now you can apply it in your project at ease.

You should continue to check if Token is expired and logout:React – How to Logout when JWT Token is expired

Or add refresh token:React Refresh Token with JWT and Axios Interceptors

If you want to use React Hooks for this example, you can find the implementation at:React Hooks: JWT Authentication (without Redux) example

Or using Redux for state management:React Redux: JWT Authentication & Authorization example

Happy learning, see you again!

The end product

Congrats for getting this far! It’s been a bit of a marathon, wiring up Redux, React Router, TypeScript and localStorage.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *