2

I am developing a React application that will authenticate with Azure Ad via ADAL. Everything works fine but i need to load the JWT tokens in the beginning of the application. I thought that I will wrap my two functions which give back a token in a Promise and then conditionally render it, however for some reason it doesn't "wait" for my promise to resolve.

Any help would be welcome. Cheers!

import React, { Component } from 'react';

import { library } from '@fortawesome/fontawesome-svg-core';
import { faIgloo, faBars } from '@fortawesome/free-solid-svg-icons';
import { initializeIcons } from '@uifabric/icons';
import { acquireToken, acquireGraphToken } from '../../adalConfig'

import Navbar from '../../common/navbar/navbar';
import Searchbar from '../../common/utils/searchBar/searchBar';
import LeftNavigation from '../../common/leftNavigation/leftNavigation';
import PageContainer from '../pageContainer/pageContainer';
import { IntlProvider } from 'react-intl';

import messages_en from '../../assets/translations/translations_en';
import messages_nl from '../../assets/translations/translations_nl';
import StylesheetLoader from '../../common/utils/stylesheetLoader/stylesheetLoader';
import ReactAI from 'react-appinsights';
import { connect } from 'react-redux';
import * as userActions from '../../store/users/actions';

initializeIcons();
library.add(faIgloo, faBars);

class App extends Component {

  state = {
    languageChoice: 'en',
    theme: 'red',
    tokensAreLoaded: false
  }

  componentWillMount() {

    let promise = new Promise((resolve, reject) => {
      const token = acquireToken();
      const graphToken = acquireGraphToken();

      if (token != '' && graphToken != '') {
        resolve(true);
      } else {
        reject(Error('promise failed'))
      }
    });

    promise.then( (value) => {
      this.setState({tokensAreLoaded: value});
    }, function (error){
      console.log('error getting promise value');
    })
  }

  componentDidMount() {
    this.props.onFetchCurrentUser();
  }    

  render() {

    if (this.state.tokensAreLoaded) {
      console.log('renderApp');
    } else {
      console.log('loading');
    }

    // Sets an interval that refreshes to get the token every 15 minutes
    // We use this because our custom API's do not automatically issue a
    // refresh token.
    setInterval(AppTokenRefresher, 900000);

    function AppTokenRefresher() {
      acquireToken();
      acquireGraphToken();
    } 

    const messages = {
        'nl': messages_nl,
        'en': messages_en
    };

    ReactAI.setAppContext({urlReferrer: document.referrer});
    // const Ai = ReactAI.ai();

    // function test() {
    //   Ai.trackEvent('Testing', { 'user': 'me' });
    // }

    const user = this.props.currentUser ? this.props.currentUser.name : 'test';

    return (
      <React.Fragment>
        <StylesheetLoader />
        {user}
        <IntlProvider locale={this.state.languageChoice} messages={messages[this.state.languageChoice]}>
          <div className="siteContainer">
            <Navbar currentUserProfile={this.props.currentUser}></Navbar>

            <div className="mobile-searchbar">
              <Searchbar />
            </div>

            <div className='page-container'>
              <aside>
                <LeftNavigation />
              </aside>

              <section className="main">
                <PageContainer />
            </section>

            </div>

          </div>
        </IntlProvider>

      </React.Fragment>
    );
  }
}

const mapStateToProps = state => {
  return {
      currentUserError: state.currentUserSlice.currentUserError,
      currentUserLoading: state.currentUserSlice.currentUserLoading,
      currentUser: state.currentUserSlice.currentUser,
      currentUserPicture: state.currentUserSlice.currentUserPicture
  }
}

const mapDispatchToProps= (dispatch) => {
  return {
      onFetchCurrentUser: () => dispatch(userActions.fetchCurrentUser()),
      onFetchCurrentUserPicture: () => dispatch(userActions.fetchCurrentUserPicture())
  }    
}

export default connect(mapStateToProps, mapDispatchToProps)(App);

Adal helper functions that I am wrapping in the promise:

import { AuthenticationContext, adalFetch } from 'react-adal';

const adalConfig = {
    instance: 'https://login.microsoftonline.com/',
    clientId: '9e16003a',
    extraQueryParameter: 'nux=1',
    endpoints: {
        graphApi: 'https://graph.microsoft.com',
        oneApi: 'https://one365demo.onmicrosoft.com/b15a-3f1d0cf658f5'
    },
    postLogoutRedirectUri: window.location.origin,
    redirectUri: window.location.origin,
    cacheLocation: 'localStorage'
};

export const authContext = new AuthenticationContext(adalConfig);



export const adalGraphFetch = (fetch, url, options) =>
  adalFetch(authContext, adalConfig.endpoints.graphApi, fetch, url, options);

export const adalOneApiFetch = (fetch, url, options) =>
  adalFetch(authContext, adalConfig.endpoints.oneApi, fetch, url, options);

export const getToken = () => {
    return authContext.getCachedToken(authContext.config.clientId);
};

export const getGraphToken = () => {
    return authContext.getCachedToken('https://graph.microsoft.com');
};

export const acquireGraphToken = () => {  
    authContext.acquireToken(adalConfig.endpoints.graphApi, (message, token, msg) => {
        console.log('graph token', token);
        return token;
    })

    return null;
} 

export const acquireToken = () => {  
    authContext.acquireToken(adalConfig.endpoints.oneApi, (message, token, msg) => {
        console.log('the token', token);
        return token;
    })

    return null;
}
Dacre Denny
  • 29,664
  • 5
  • 45
  • 65
Rodney Wormsbecher
  • 897
  • 2
  • 17
  • 39

2 Answers2

3

It looks like AuthenticationContext#acquireToken is an asynchronous method, in that is has a callback which is invoked when the token is acquired (or when the attempt to do so fails).

Consider redefining the adal helper methods that enclose the call to acquireToken() so that they are either async, or so that they return a Promise that progresses when the callback passed to AuthenticationContext#acquireToken() is invoked:

export const acquireGraphToken = () => {  

    /* Return promise as acquireToken() is async */
    return (new Promise(resolve => {
        authContext.acquireToken(adalConfig.endpoints.graphApi, (message, token, msg) => {
                console.log('graph token', token);

                /* Resolve enclosing promise with token */
                resolve(token); 
            })
        }));    
} 

export const acquireToken = () => {  

    /* Return promise as acquireToken() is async */
    return (new Promise(resolve => {
        authContext.acquireToken(adalConfig.endpoints.oneApi, (message, token, msg) => {
            console.log('the token', token);

            /* Resolve enclosing promise with token */
            resolve(token); 
        })
    }));
}

With those changes applied in your helper module, you'd then need to update your componentWillMount() component hook to correctly integrate these methods to ensure that the component state is updated with {tokensAreLoaded: true} once both tokens are successfully acquired:

componentWillMount() {

    /* Issue request for both tokens to get aquired */
    Promise.all([
        acquireToken(),
        acquireGraphToken()
    ])
    .then(([ token, graphToken ]) => {

        /* On resolving both requests, ensure tokens are value before proceeding or reject (as before) */
        if (token != '' && graphToken != '') {
            resolve(token);
        } else {
            reject(Error('promise failed'))
        }
    })
    .then(() => {

        /* On success, update state */
        this.setState({tokensAreLoaded: true});
    })
  }

Hope this helps!

Dacre Denny
  • 29,664
  • 5
  • 45
  • 65
0

I'd recommend introducing some kind of loading notification until the token is fetched. You could await the token in componentDidMount so you don't block the stack

async componentDidMount() {
    try {
        const token = await this.getToken();
        this.setState({tokensAreLoaded: true, token});
    } catch (e) {
        throw new Error (e);
    }
}
getToken () {
    return new Promise((resolve, reject) => {
        const token = acquireToken();
        const graphToken = acquireGraphToken();

        if (token != '' && graphToken != '') {
            resolve(token);
        } else {
           reject(Error('promise failed'))
        }
    });
}

And in your render method:

render () {
    if (!this.state.tokensAreLoaded) {
        return (<p>Authorizing</p>);
    }
    return (
       // the stuff you want to display when token is ready
    )

}
Tom M
  • 2,815
  • 2
  • 20
  • 47