React - What is the best way to handle login and authentication?
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_token
– this 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 localStorage
– feel 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 }}
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>
you can set the access token in the local storage on login and clear it after the user logs out. the is authenticated method will then be used to check if there is a token and whether the token is valid while making an API call