loading...

Rewrite Auth0 Example with React Hooks

terrierscript profile image terrierscript ・3 min read

Auth0 default React example don't use react hooks.

I try to rewrite this example to use React Hooks.

Full Example

You can read full example in this repository
https://github.com/terrierscript/example-auth0/tree/full-example

Details

1. Create Context

First, I create AuthContext that hold auth object and some auth result state.

// auth/AuthContext
import React, { createContext, useState, useContext } from 'react';

import { WebAuth } from 'auth0-js';
import { AUTH_CONFIG } from './auth0-variables';

const generateAuth = () =>
  new WebAuth({
    domain: AUTH_CONFIG.domain,
    clientID: AUTH_CONFIG.clientID,
    redirectUri: AUTH_CONFIG.callbackUrl,
    responseType: 'token id_token',
    scope: 'openid'
  });

const Auth0Context = createContext<ReturnType<typeof useContextValue>>(null);

const useAuthState = () => {
  return useState({
    accessToken: null,
    idToken: null,
    expiresAt: 0
  });
};

const useContextValue = () => {
  const [authState, updateAuthState] = useAuthState();
  return {
    auth0: generateAuth(),
    authState,
    updateAuthState
  };
};

export const Auth0Provider = ({ children }) => {
  const value = useContextValue();
  return (
    <Auth0Context.Provider value={value}>{children}</Auth0Context.Provider>
  );
};

export const useAuth0Context = () => {
  return useContext(Auth0Context);
};

2. Create Context

Next, generate useAuth.

Almost logics same as Auth.js

But isAuthenticated changed from function to boolean value with useMemo

// src/useAuth
import { useCallback, useMemo } from 'react';
import history from '../history'; // TODO: history may pass from props
import { useAuth0Context } from './AuthContext';

const useIsAuthenticated = expiresAt => {
  return useMemo(() => {
    return new Date().getTime() < expiresAt;
  }, [expiresAt]);
};

export const useAuth0 = () => {
  const { auth0, authState, updateAuthState } = useAuth0Context();

  const isAuthenticated = useIsAuthenticated(authState.expiresAt);

  const login = useCallback(() => {
    auth0.authorize();
  }, [auth0]);

  const logout = useCallback(() => {
    updateAuthState({
      accessToken: null,
      idToken: null,
      expiresAt: 0
    });
    localStorage.removeItem('isLoggedIn');

    auth0.logout({
      returnTo: window.location.origin
    });

    // navigate to the home route
    history.replace('/home');
  }, [auth0, updateAuthState]);

  const setSession = useCallback(
    authResult => {
      localStorage.setItem('isLoggedIn', 'true');

      let expiresAt = authResult.expiresIn * 1000 + new Date().getTime();
      updateAuthState({
        accessToken: authResult.accessToken,
        idToken: authResult.idToken,
        expiresAt: expiresAt
      });
      history.replace('/home');
    },
    [updateAuthState]
  );

  const renewSession = useCallback(() => {
    auth0.checkSession({}, (err, authResult) => {
      if (authResult && authResult.accessToken && authResult.idToken) {
        setSession(authResult);
      } else if (err) {
        logout();
        console.error(err);
        alert(
          `Could not get a new token (${err.error}: ${err.error_description}).`
        );
      }
    });
  }, []);

  const handleAuthentication = useCallback(() => {
    auth0.parseHash((err, authResult) => {
      if (authResult && authResult.accessToken && authResult.idToken) {
        setSession(authResult);
      } else if (err) {
        history.replace('/home');
        alert(`Error: ${err.error}. Check the console for further details.`);
      }
    });
  }, []);

  // retun some functions
  return {
    login,
    logout,
    handleAuthentication,
    isAuthenticated,
    renewSession
  };
};

3. fix <Callback>

In base example, handleAuthentication called in router like this.

  <Route path="/callback" render={(props) => {
    handleAuthentication(props);
    return <Callback {...props} /> 
  }}/>

I feel it's so tricky.
But when use hooks, we call that with useEffect

// Callback/Callback

import React, { useEffect } from 'react';
import loading from './loading.svg';
import { useAuth0 } from '../Auth/useAuth';

export const Callback = props => {
  const { handleAuthentication } = useAuth0();
  const { location } = props;
  useEffect(() => {
    if (/access_token|id_token|error/.test(location.hash)) {
      handleAuthentication();
    }
  }, [handleAuthentication, location]);

  const style = {
  //....
  };

  return (
    <div style={style}>
      <img src={loading} alt="loading" />
    </div>
  );
};

4. fix <App> and <Home>

<App> and <Home> rewrited too.

<App> call renewSession with useEffect

// App

import React, { useCallback, useEffect, useMemo } from 'react';
import { Navbar, Button } from 'react-bootstrap';
import './App.css';
import { useAuth0 } from './Auth/useAuth';

const useGoToHandler = history => {
  return useCallback(route => () => history.replace(`/${route}`), [history]);
};

export const App = ({ history }) => {
  const { login, logout, isAuthenticated, renewSession } = useAuth0();

  const goToHandler = useGoToHandler(history);
  useEffect(() => {
    if (localStorage.getItem('isLoggedIn') === 'true') {
      renewSession();
    }
  }, [renewSession]);

  return (
    <div>
      <Navbar fluid>
        <Navbar.Header>
          <Navbar.Brand>
            <a href="#">Auth0 - React</a>
          </Navbar.Brand>
          <Button
            bsStyle="primary"
            className="btn-margin"
            onClick={goToHandler('home')}
          >
            Home
          </Button>
          {!isAuthenticated && (
            <Button
              id="qsLoginBtn"
              bsStyle="primary"
              className="btn-margin"
              onClick={login}
            >
              Log In
            </Button>
          )}
          {isAuthenticated && (
            <Button
              id="qsLogoutBtn"
              bsStyle="primary"
              className="btn-margin"
              onClick={logout}
            >
              Log Out
            </Button>
          )}
        </Navbar.Header>
      </Navbar>
    </div>
  );
};
// Home/Home

import React from 'react';
import { useAuth0 } from '../Auth/useAuth';

export const Home = () => {
  const { login, isAuthenticated: isAuthenticated } = useAuth0();
  return (
    <div className="container">
      {isAuthenticated && <h4>You are logged in!</h4>}
      {!isAuthenticated && (
        <h4>
          You are not logged in! Please
          <a style={{ cursor: 'pointer' }} onClick={login}>
            Log In
          </a>
          to continue.
        </h4>
      )}
    </div>
  );
};

5. fix router

Rewrite router to this.

  • Routers wrapped <Auth0Provider>.
  • Callback logic moved that component.
  • (trivial) Use react-router <Switch>.
// roter
import React from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import { App } from './App';
import { Home } from './Home/Home';
import { Callback } from './Callback/Callback';
import history from './history';
import { Auth0Provider } from './Auth/AuthContext';

const Routes = () => {
  return (
    <Router history={history}>
      <Route path="/" render={props => <App {...props} />} />
      <Switch>
        <Route path="/home" render={props => <Home {...props} />} />
        <Route path="/callback" render={props => <Callback {...props} />} />
      </Switch>
    </Router>
  );
};

export const makeMainRoutes = () => {
  return (
    <Auth0Provider>
      <Routes />
    </Auth0Provider>
  );
};

6. Setup Auth0 and npm start

That all !

Discussion

markdown guide
 

With this code, can I use redux for storing the token?

 

Sorry for late.

I think not need store to redux but it's enable with useEffect

maybe like this:

const TokenSync => ({updateTokenAction]){
  const { token } =  useAuth0Context() // append token 
  useEffect( () => updateTokenAction(token), [token])
  return null
}

// usage: <TokenSync />