Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React - What is the best way to handle login and authentication?

New to react and working on an application with authentication/logging in. It currently works but feels hacked together. Right now I have my isAuthenticated state located in my routes.js like so:

class Routes extends Component {      constructor(props) {         super(props);          this.state = {             isAuthenticated: false,          }      } 

On my login page, I need to know when a user is authenticated to redirect them to the home page. What is the best design pattern to allow access and manipulation of this isAuthenticated state? How I currently have it set up is I have a function that sets the state inside the routes.js and sends the state as a prop like so:

 setAuthenticated = (isAuthenticated) => {         this.setState({isAuthenticated});     } 

and down below in the router...

<Route path="/" exact component={() =>                             <div>                                 <Login                                     isAuthenticated={this.state.isAuthenticated}                                     setAuthenticated={this.setAuthenticated}                             </div>                         } /> 

Yes, I understand this is bad design because this is changing props values which are supposed to be immutable. This is also bad because when I change this value in my login.js it causes multiple unnecessary re-renders. Should I be declaring isAuthenticated as some type of global variable? I am not using any state management by the way.

Edit: I am setting isAuthenticated based on a response from my server which confirms correct login/password combination.

like image 208
Vincent Nguyen Avatar asked Apr 13 '18 14:04

Vincent Nguyen


People also ask

How do you handle authentication in React?

There are two main things your React application needs to do to sign on a user: Get an access token from an authentication server. Send the access token to your backend server with each subsequent request.

What API calls you need to handle for an authentication setup with React?

This guide uses LoginRadius API for authenticating React apps. It provides React developers with a more straightforward way to add user authentication to react apps.


2 Answers

Handling isAuthenticated only in the state means the user will be unauthenticated every time he refreshes the page. That's not really user-friendly! :)

So instead, the Login page should store an access_token (coming from your backend) in the cookies or localStorage of the browser. An access_token proves the user is authenticated and also verifies his identity. You will usually pass this access_token to every next requests to your server, to check if this user is allowed to access the data he's requesting, or allowed to create, edit and delete the things he's trying to create, edit and delete.

Then you can check this access_token on every other pages as well and redirect the user to the Login page if he's not authenticated anymore.


A brief aside on the difference between access_token and refresh_tokenthis will help you understand the code bellow, but feel free to skip ahead if you are already familiar with it.

Your backend probably uses OAuth2, which is the most common authentication protocol nowadays. With OAuth2, your app makes a first request to the server containing the username and password of the user to authenticate. Once the user is authenticated, he receives 1) an access_token, which usually expires after an hour, and 2) a refresh_token, which expires after a very long time (hours, days). When the access_token expires, instead of asking the user for his username and password again, your app sends the refresh_token to the server to obtain a new access_token for this user.


A brief aside on the differences between cookies and localStoragefeel free to skip it too!

localStorage is the most recent technology between both. It's a simple key/value persistence system, which seems perfect to store the access_token and its value. But we also need to persist its date of expiration. We could store a second key/value pair named expires but it would be more logic to handle on our side.

On the other hand, cookies have a native expires property, which is exactly what we need! cookies are an old technology and are not very developer-friendly, so I personally use js-cookie, which is a small library to manipulate cookies. It makes it look like a simple key/value persistence system too: Cookies.set('access_token', value) then Cookies.get('access_token').

Other pro for the cookies: they are cross subdomains! If your Login app is login.mycompany.com and your Main app is app.mycompany.com, then you can create a cookie on the Login app and access it from the Main app. This is not possible with LocalStorage.


Here are some of the methods and special React components I use for authentication:

isAuthenticated()

import Cookies from 'js-cookie'  export const getAccessToken = () => Cookies.get('access_token') export const getRefreshToken = () => Cookies.get('refresh_token') export const isAuthenticated = () => !!getAccessToken() 

authenticate()

export const authenticate = async () => {   if (getRefreshToken()) {     try {       const tokens = await refreshTokens() // call an API, returns tokens        const expires = (tokens.expires_in || 60 * 60) * 1000       const inOneHour = new Date(new Date().getTime() + expires)        // you will have the exact same setters in your Login page/app too       Cookies.set('access_token', tokens.access_token, { expires: inOneHour })       Cookies.set('refresh_token', tokens.refresh_token)        return true     } catch (error) {       redirectToLogin()       return false     }   }    redirectToLogin()   return false } 

redirectToLogin()

const redirectToLogin = () => {   window.location.replace(     `${getConfig().LOGIN_URL}?next=${window.location.href}`   )   // or history.push('/login') if your Login page is inside the same app } 

AuthenticatedRoute

export const AuthenticatedRoute = ({   component: Component,   exact,   path, }) => (   <Route     exact={exact}     path={path}     render={props =>       isAuthenticated() ? (         <Component {...props} />       ) : (         <AuthenticateBeforeRender render={() => <Component {...props} />} />       )     }   /> ) 

AuthenticateBeforeRender

class AuthenticateBeforeRender extends Component {   state = {     isAuthenticated: false,   }    componentDidMount() {     authenticate().then(isAuthenticated => {       this.setState({ isAuthenticated })     })   }    render() {     return this.state.isAuthenticated ? this.props.render() : null   } } 
like image 127
GG. Avatar answered Oct 06 '22 00:10

GG.


If you are using an application where the authentication lasts only for one session, storing it in state is enough. But do note that this means, the user will lose the authenticated status on page refresh.

Here is an example using React Context, where we create context using createContext and use Consumer to access it across the application.

const AuthenticationContext = React.createContext();  const { Provider, Consumer } = AuthenticationContext;    function Login(props) {    return (      <Consumer>        {          value=>          <button onClick={value.login}>Login</button>        }      </Consumer>    );  }    function Logout() {    return (      <Consumer>        {          value=>          <button onClick={value.logout}>Logout</button>        }      </Consumer>    );  }    function AnotherComponent() {    return (      <Consumer>        {          value=>{            return value.isAuthenticated?              <p>Logged in</p>:              <p>Not Logged in</p>          }        }      </Consumer>    );  }    class App extends React.Component {    constructor(props) {      super(props);      this.login = ()=> {        this.setState({          isAuthenticated: true        });      }      this.logout = ()=> {        this.setState({          isAuthenticated: false        });      }      this.state = {        isAuthenticated: false,        login: this.login,        logout: this.logout      }    }        render() {      return (        <Provider value={this.state}>          <Login />          <Logout />          <AnotherComponent />        </Provider>      );    }  }  ReactDOM.render(<App />, document.getElementById("root"));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>  <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>  <div id="root"></div> 

https://reactjs.org/docs/context.html#reactcreatecontext

like image 32
Agney Avatar answered Oct 06 '22 01:10

Agney