0

I am fairly new to React. This question is related to storing the authentication token in the client side and reusing it until expiry.

I am using MSAL to for authentication in my React app. Below is my code.

import * as React from 'react';

import {AuthOptions, CacheOptions, SystemOptions} from './MsalConfig';
import { UserAgentApplication } from 'msal';

//export const authContext: AuthenticationContext = new AuthenticationContext(msalConfig);

export const msalInstance = new UserAgentApplication({
  auth: AuthOptions,
  cache: CacheOptions,
  system: SystemOptions
});

export const withAuth = function(WrappedComponent){
  return class Auth extends React.Component{
    constructor(props) {
      super(props);

      this.state = {
        authenticated: false,
        renewIframe: false,
        hasError: false,
        errorMessage: null,
        graphAPIToken: null
      };
    }

    componentWillMount() {
      this.checkAuthentication();
    }

    checkAuthentication = () => {
      if(this.state.graphAPIToken == null){
        // action to perform on authentication
        msalInstance.handleRedirectCallback(() => { // on success
          this.setState({
            authenticated: true
          });
        }, (authErr, accountState) => {  // on fail
          console.log("Error after graph API call " + authErr);

          this.setState({
            hasError: true,
            errorMessage: authErr.errorMessage
          });
        });

        // if we are inside renewal callback (hash contains access token), do nothing
        if (msalInstance.isCallback(window.location.hash)) {
          this.setState({
            renewIframe: true
          });
          return;
        }

        // not logged in, perform full page redirect
        if (!msalInstance.getAccount()) {
          msalInstance.loginRedirect({});
          return;
        } else {     // logged in, set authenticated state
          this.setState({
            authenticated: true
          }, () => {
            this.getAccessToken(msalInstance);
          });
        }
      }
    }

    getAccessToken = (msalInstance) => {
      console.log(msalInstance);
      // if the user is already logged in you can acquire a token
      if (msalInstance.getAccount()) {
          var tokenRequest = {
              scopes: ["user.read"]
          };
          msalInstance.acquireTokenSilent(tokenRequest)
              .then(response => {
                  // get access token from response
                  // response.accessToken
                  console.log(response.accessToken);
                  this.setState({
                    graphAPIToken: response.accessToken
                  });
                  //return response.accessToken;
              })
              .catch(err => {
                  // could also check if err instance of InteractionRequiredAuthError if you can import the class.
                  if (err.name === "InteractionRequiredAuthError") {
                      return msalInstance.acquireTokenPopup(tokenRequest)
                          .then(response => {
                              // get access token from response
                              // response.accessToken
                              console.log(response.accessToken);
                              this.setState({
                                graphAPIToken: response.accessToken
                              });
                              //return response.accessToken;
                          })
                          .catch(err => {
                              console.log("Error inside getAccessToken " + err);
                          });
                  }
                  console.log("Error inside getAccessToken " + err);
              });
      } else {
          // user is not logged in, you will need to log them in to acquire a token
          console.log("u aren't logged in");
      }
  }

  callGraphAPI = (token) => {
      //..
  }

    render(){
      if (this.state.renewIframe) {
        return <div>hidden renew iframe - not visible</div>;
      }

      if (this.state.authenticated) {
        return <WrappedComponent {...this.props} />;
      }

      if (this.state.hasError) {
        console.log("Login error: " + this.state.errorMessage);
      }

      console.log("Login error: " + this.state.errorMessage);
    }
  };
}

Now, I am using this in my React routes in my index.js:

ReactDOM.render(<MuiThemeProvider theme={theme}>
        <Router>
            <Switch>
                <Route path="/" exact component={(withAuth)(Projects)}/>
                <Route path="/signin" exact component={SignIn}/>
                <Route path="/projects" exact component={(withAuth)(Projects)}/>
                <Route path="/admin" exact component={(withAuth)(Admin)}/>
                <Route path="/projectstages/:projectNumber" exact component={(withAuth)(ProjectStages)}/>
                <Route path="/formspage" exact component={(withAuth)(FormsPage)}/>
                <Route path="/users" exact component={(withAuth)(UserManagement)}/>
            </Switch>
        </Router>
    </MuiThemeProvider>, document.getElementById('root'));

Whenever I access any URL, the componentWillMount of withAuth component will get called to check the authentication. How do I make sure that the token is generated only once and stored in the cache and reuse it until it expires? Also, I am not sure if I have implemented the authentication in correct way. Any help with this please?

Souvik Ghosh
  • 4,456
  • 13
  • 56
  • 78
  • When your component mounts, you should try to acquire a token silently from MSAL. Then if that fails, interactive authentication may be needed. – juunas Aug 29 '19 at 10:02
  • @juunas I am fetching the token in during mount of `withAuth` component. Problem is with every URL request or refresh, the mount will make a call to fetch token. Without redux, how do I reuse the existing token? – Souvik Ghosh Aug 29 '19 at 11:47
  • If you call acquireTokenSilent on MSAL, it should return you the cached token even after a refresh. – juunas Aug 29 '19 at 11:52

1 Answers1

3

You don't have to worry about caching the token and renewing it, MSAL does this all for you.

(In fact, I recommend you specifically do not do your own token caching if you're using MSAL. You risk accidentally introducing your own security vulnerability.)

As described in the documentation, every time you want to make an API request, you should call acquireTokenSilent. One of three situations will happen:

  1. If MSAL has an existing token in it's own cache that matches the parameters you give it, MSAL will provide the token right away.
  2. If a token refresh is needed (e.g. because the cached access token is expired, or because you need an access token for a different API), MSAL will attempt to do a silent token refresh. If this succeeds silently, MSAL provides the fresh access token.
  3. If MSAL doesn't have a cached valid access token, and MSAL can't silently refresh it (e.g. the user has not signed in, they've signed out, or the user needs to consent to a new permission), then MSAL will let you know and you can handle that situation as needed (e.g. use acquireTokenPopup to use a popup, or acquireTokenRedirect to use a redirect).
Philippe Signoret
  • 13,299
  • 1
  • 40
  • 58