Microsoft-authentication-library-for-js: handleRedirectCallback does not return a promise

Created on 23 Apr 2020  路  20Comments  路  Source: AzureAD/microsoft-authentication-library-for-js

Library

Description

In my Vue.js project I have a simple SignIn button that calls the correct login method (loginPopup or loginRedirect) depending on a config file for the app. Authentication in the app happens by triggering the signIn action from the vuex store. This works perfectly fine for the loginPopup method but for the loginRedirect method it throws the following error:

image

app\src\store\auth\actions.js

import auth from 'src/services/auth/authService'

export const signIn = ({ commit, dispatch }) => {
  commit('SET_BUTTON', { name: 'signInButton', updates: { disabled: true } })

  auth.signIn()
    .then(loginResponse => {
      console.log("id_token acquired at: " + new Date().toString())
      console.log(loginResponse);

      dispatch('setSignedInAccount')
    })
    .catch(error => {
      console.log(error);
      commit('SET_BUTTON', { name: 'signInButton', updates: { show: true, disabled: false } })
    });
}

export const setSignedInAccount = ({ commit }) => {
  let account = auth.getAccount()
  if (account) {
    commit('SET_BUTTON', { name: 'signInButton', updates: { show: false } })
    commit('SET_ACCOUNT', account)
  }
}

export const signOut = () => {
  if (auth.getAccount()) {
    auth.signOut()
  }
}

You can see that auth.signIn() expects a Promise by the use of .then and .catch. This approach works fine when msal is configured with acquireTokenPopup but not with handleRedirectCallback.

This is by design when I read the docs.:

For authentication methods with redirect flows (loginRedirect and acquireTokenRedirect), in MSAL.js 1.2.x or earlier, you will need to explicitly register a callback for success or error through handleRedirectCallback() method. This is needed since redirect flows do not return promises as the methods with a pop-up experience do. This became optional in MSAL.js version 1.3.0.

So to also make the signIn button work with the loginRedirect I would need to return a Promise which I tried to do like this:

authRedirect.js

export const authRedirectCallback = (error, response) => {
  return new Promise((resolve, reject) => {
    if (error) {reject(error)}
    else resolve(response)
  })
}
msal.handleRedirectCallback(authRedirectCallback)

export const signIn = () => msal.loginRedirect(loginRequest)

As I'm a newbie I might have gotten things wrong becuase it doesn't seem to work. Can you have a look at how I should return a promise so the action in the Vuex store with the .then and the .catch will still work?

authPopup.js

export const signIn = () => msal.loginPopup(loginRequest)

Thank you for your help.

question

Most helpful comment

@DarkLite1 The redirect flow can be confusing and we could do a better job of documenting how it works, but in the meantime I'll try to clear up the confusion. As @m-sterspace pointed out, redirecting away from the page means you are creating a whole new instance of the application when you return. This means that calling a redirect method cannot return anything. Rather what happens is, the page is redirected away, you do your signin and you are redirected back to your application with the response in the hash.

This is where things got a little muddy, pre-1.3.0. In order to process this hash and cache it, the developer needed to call handleRedirectCallback() which kicks off the processing and then calls the user defined function that was passed. However, if navigateToRequestUrl was set to true, another redirect would be kicked off while processing the hash, meaning that the user-defined function was run on a previous instance of the application, the results of which have now been lost. If you find this is happening to you, try setting navigateToRequestUrl to false. The downside of doing this is that you need to have every page that might acquire a token, registered in the app portal as a redirect uri.

In version 1.3.0 we made some changes to this behavior. Now processing of the hash is done on initialization of msal and it is done on the final page after all redirects are complete. Calling handleRedirectCallback() now just reaches into msal to return the result/error that occurred while processing the hash to the function you defined.

In the handleRedirectCallback you should do the same things you would have done in the .then clause of your popup promise if a result was returned. If an error was returned execute your .catch logic. I highly suggest upgrading to 1.3.0 as this flow becomes a little clearer. If you don't want to try the beta, we should have the mainline release coming soon.

We apologize for how confusing this is and we will certainly try to do a better job of documenting it. As there are already tickets open for documentation, however, I'm going to close this. Please do let me know if you have additional questions.

All 20 comments

@DarkLite1

I haven't used Vue so forgive me for my unfamiliarity, but I think you might be messing up the flow a little bit for the login redirect method (or maybe I am, but I did eventually manage to get my method to work in my React app lol).

First of all, the handleredirectcallback method doesn't make a whole lot of sense to me, and I've expressed why in a couple other issues. I think it's a bit of a confusing way to design an API that produces weird warnings that are puzzling to a developer that's somewhat new to authentication.

But basically for the handleredirectcallback function, you shouldn't be passing a promise.

After signing in you'll be redirected back to your application's callback page. But because you left, and then came back, this is an entirely new instance of your application. So when this new instance of your application loads, your msal library is going to recognize that it's on the callback page with a hash containing a token and other information and kick into gear. It's going to parse out the token and original page information that comes after the hash, and redirect you to the page that you were on when you left. (Personally, I don't like that it does this automatically as it seems like unexpected global behaviour. In contrast, the Auth0 libraries require your callback page to call one of their functions to kick off this process which seems far more predictable to me.)

Now, some of the documentation says that what you need to do is register a callback with the handleredirectcallback method when your application first loads. I believe the thinking is that that way after the msal library is done redirecting you and parsing the token, it will execute that callback function and give you the token or the error (or in the case that it's done parsing by the time you give it a callback function, it would execute it immediately). You would design that callback function so that it would be connected to your global authentication state and would update it with the token when it executes. Right now however, you've passed a function in that returns a promise, so when the msal library goes to execute that callback, all that's going to happen is that it's going to return a promise, and since msal doesn't care about that promise, nothing is going to happen with it. f you wanted to wrap that in a promise it would have to look something like this might work but I haven't tried it:


const tryAndGetToken = (msalApp) => {
  return new Promise((resolve,reject) => {
    const callBack = (error, response) => {
      if(error) {reject(error); return;}
      if(response) {resolve(response); return;}
      reject(Error('Nothing returned from msal'));
    };
  msalApp.registerRedirectCallback(callBack);
  });
}

then when your app loads you might have:

const msal = new Msal.UserAgentApplication(config);
tryAndGetToken(msal)
  .then(token => {
    dispatch('LOGGED_IN', token);
  })
  .catch(err => {
    console.log(err);
  });

However, all of that being said, I've never found that the handleredirectcallback function will actually return a token. For me it only ever returns an error or undefined. That being said, you do still have to register this callback or it won't let you try and redirect later on.

What I do to get my token is call acquireTokenSilent on application load, and that seems to be able to get their token if they did just come back after logging in (or if they're just still signed in). So basically my app lifecycle goes:

  1. App load
    a. Register redirect callback that just checks for an error and logs it.
    b. Try to acquire token silent to see if their is currently a valid token for the user.

    • If so, update the global authentication state with their account info and token.

    • If not, do nothing and wait for them to hit the log in button (though you could also redirect them here and I believe most of the Microsoft documentation just automatically redirects them)

      ....

  2. At some point later on while the application is running and the user isn't authenticated and hits the "sign in" button
    a. Call the loginRedirect method.
    ...
  3. At some point later on while the application is running and the user is authenticated and hits the "sign out" button
    a. Update the global authentication state to clear out their account info and token
    b. Call the clearCache() function from msal. The logout function seems to completely sign them out from their Microsoft account, whereas most of the time you just want to sign them out from your application.

Thank you for the detailed clarification. I you look at my code I do this:

auth.signIn()
   .then // on success enable/disable buttons
   .catch // on failure enable/disable buttons

So as a newbie, I use this way to determine if the sign-in was successful and take the appropriate actions for the GUI. This works fine when working with the pop-up but not for redirects as it doesn't return a resolve or reject from a Promise. To be able to set my GUI buttons correctly I need a uniform way that works for both login methods (pop-up and redirect).

On a side note the login and out with both methods work flawlessly except for the error logged in the console.

With regards to the token and saving it in the global state (using Vuex in my case). I was actually planning on saving the token at first for subsequent calls. But as Microsoft advises in their example the following:

function getTokenRedirect(request, endpoint) {
  return myMSALObj.acquireTokenSilent(request, endpoint)
      .then((response) => {
          console.log(response);
          if (response.accessToken) {
              console.log('access_token acquired at: ' + new Date().toString());
              accessToken = response.accessToken;

              callMSGraph(endpoint, response.accessToken, updateUI);
              profileButton.style.display = 'none';
              mailButton.style.display = 'initial';
          }
      })
      .catch(error => {
          console.log("silent token acquisition fails. acquiring token using redirect");
          // fallback to interaction when silent call fails
          return myMSALObj.acquireTokenRedirect(request)
      });
}

function seeProfile() {
  getTokenRedirect(loginRequest, graphConfig.graphMeEndpoint);
}

I see no reason to store the token and always use the method acquireTokenSilent and/or acquireTokenRedirect to get a valid token before making the call to MsGraph for example.

Maybe I'm seeing this the wrong way, but following their js example this seems to be the way to do it. I don't think there's overhead in calling acquireTokenSilent before each GET to an API. It saves me from saving and managing a token in my Vuex store.

That's actually an interesting point about just trying to acquire the token before API calls. I hadn't thought about doing that / caught it in the documentation.

However, if I understand what you're getting at, you want the call to loginredirect to return a promise with your sign in information, the way that loginpopup does?

And if that's the case then I'm sorry to say that you may be out of luck as that is impossible because of how Single Page Applications work. Once you redirect away from the page and come back to it, that is a completely brand new instance of your application that's running with essentially no connection to the instance of the application that triggered the sign in. The only connection is that msal will redirect you to the original url, so if you put a lot of state information in your url, it can seem like you're back to the old application, but really it's a brand new one.

So if you're using the loginredirect method, you have to check for a token when your application first loads. So if you want to use both methods, you're going to have to able to sign in from two different spots, from the result of your sign in button, and when your application first loads on the callback page.

Thank you @m-sterspace , I feel less stupid because you complimented me on thinking about accessing the cached token with the acquireTokenSilent method. Feeling a little les green now behind the ears :)

But yes, that is exactly what I meant. The page reload part and all that is covered like this in my app (which is called on the lifecycle hook mounted in Vue.js:

export const setSignedInAccount = ({ dispatch }) => {
  let account = auth.getAccount()
  if (account) {
    dispatch('enableSignOut', account)
  }
  else {
    dispatch('enableSignIn')
  }
}

So the only thing I need is this:

How can I determine when a user clicks the sign-in button that the sign-in was successful or not? And the way of checking it should be consistent between popup and redirect.

Thinking about this further I believe that msal.getAccount() does just that. It's populated with data when a user is signed-in and undefined when it's not (I hope). So to check this after clicking the sign-in button I need something like this:

  auth.signIn()

  let account = auth.getAccount()

  if (account) {
    // successful sign-in
  }
  else {
    // failed sign-in
  }

The only issue is that the function signIn() when used with a popup is a Promise, that can be remediated with await auth.signIn() but that doesn't work then for the redirect, because then it doesn't return a promise and await will probably fail....

Also, catching the reason of the failure might come in handy too. This is easy with the returned Promise from the popip but not for the redirect.

Something like this would help, but it doesn't wait for the loginPopup to be finished. Getting close now...

export const signIn = async () => {
  try {
    let response = await msal.loginPopup(loginRequest)
    console.log('id_token acquired at: ' + new Date().toString())
    console.log(response)
  }
  catch (error) {
    console.log(error)
  }
}

scratchingmyheadnow

How do you determine which method is being used though? Can you not put an if statement somewhere that says "If using popup, execute this sign in function, but if using redirect, execute this one?"

Also, I don't think it should actually matter if you await msal.loginRedirect(loginRequest). When you call loginRedirect it will return undefined which the await keyword should just ignore, however, more importantly it will also redirect the user away from your application, so your application will cease to exist. It shouldn't actually matter if your application throws an error after loginRedirect gets called because your browser will destroy that instance of your application once the page changes to login.microsoft.com

Edit: You could also conditionally check what your login method returns:

const loginPromise = authProvider.SignIn();
const resp = loginPromise ? await loginPromise : null;

OMG.... You are soooo right! And here I was thinking that it all happend in my signIn() method but it isn't. I actually only added the setSignedInAccount function for page reloads, as-in a user presses F5. But what actually is happening is that this is used after a redirect too, just like you say.

You are correct, I need to look differently at it and just call the correct sign-in method and be done with hit. Thank you for the feedback. Leaving this open for someone from Microsoft so they can have a look on updating the docs about this. I was totally ignoring the fact that the page does get reloaded completely after redirect.

Have a great day.

Haha don't worry, it is a bit of a mind f* and the docs don't do a god job of explaining what is happening with the redirect method if you're not already really familiar with authentication. I was in the exact same boat and struggled through all of this like 2 months ago, and have issues both on the library repo and the docs repo about trying to get some more lifecycle explanations in the documentation.

Glad I could be of some help though!

@DarkLite1 The redirect flow can be confusing and we could do a better job of documenting how it works, but in the meantime I'll try to clear up the confusion. As @m-sterspace pointed out, redirecting away from the page means you are creating a whole new instance of the application when you return. This means that calling a redirect method cannot return anything. Rather what happens is, the page is redirected away, you do your signin and you are redirected back to your application with the response in the hash.

This is where things got a little muddy, pre-1.3.0. In order to process this hash and cache it, the developer needed to call handleRedirectCallback() which kicks off the processing and then calls the user defined function that was passed. However, if navigateToRequestUrl was set to true, another redirect would be kicked off while processing the hash, meaning that the user-defined function was run on a previous instance of the application, the results of which have now been lost. If you find this is happening to you, try setting navigateToRequestUrl to false. The downside of doing this is that you need to have every page that might acquire a token, registered in the app portal as a redirect uri.

In version 1.3.0 we made some changes to this behavior. Now processing of the hash is done on initialization of msal and it is done on the final page after all redirects are complete. Calling handleRedirectCallback() now just reaches into msal to return the result/error that occurred while processing the hash to the function you defined.

In the handleRedirectCallback you should do the same things you would have done in the .then clause of your popup promise if a result was returned. If an error was returned execute your .catch logic. I highly suggest upgrading to 1.3.0 as this flow becomes a little clearer. If you don't want to try the beta, we should have the mainline release coming soon.

We apologize for how confusing this is and we will certainly try to do a better job of documenting it. As there are already tickets open for documentation, however, I'm going to close this. Please do let me know if you have additional questions.

Totally happy with the help and feedback I got here. I felt a bit lost to be honest as I thought, as a beginner, I didn't understand enough of authentication and javaScript in general. It's always a bit of a struggle starting of, especially with complex things like authentication. But it's super important to get that right, so I'm really glad you guys (and girls and gender x) are there to help people like me.

On a side note: is it best practice to not store the token in the Vuex store but just call acquireTokenSilent and/or acquireTokenRedirect || acquireTokenPopup before every API call?

If we can do this than there's no need in maintaining this somwehere.

Please keep up the great work and have a nice day.

@DarkLite1 The tokens are cached in localstorage or sessionstorage by msal so there should not be a need to store them separately on your own. acquireTokenSilent will always look for a cached token before attempting to make a network call. Therefore the recommendation is to call acquireTokenSilent and if it throws an InteractionRequired error call acquireTokenRedirect or acquireTokenPopup

@tnorling out of curiosity, is there a security concern from keeping your token in memory as opposed to letting Msal keep it in local/session storage? The first thing my application does is acquire an actual token to get the user's application specific account information, so I've just kept that token in my application's global store (using redux / context), and just check to make sure it's not expired before attaching it to subsequent requests.

Should I be concerned that my application's running memory might be more vulnerable to attack then it's session/local storage?

@tnorling thank you for the clarification. My thinking as such was correct to have it all handled by msal in the background and don't store the token myself.

We use msal.getAccount() to figure out if a user is signed-in and in turn acquireTokenSilent will know what account to look for. The only way to clear the value returned by msal.getAccount() is by calling msal.logout(). An axpired token will not clear the value returned by msal.getAccount().

@m-sterspace if the above is correct than I see no reason to store the token yourself as you will need to maintain it (i.e. is it expired? Do I need to request a new one? Do I need to clear it from memory?). Just asking msal the required token and asking for a sign-in if it isn't valid is all we need.

@DarkLite1

I mean, that's an option, and it makes sense if acquiretokensilent can get a new token without interaction after the last one has expired, but as far as I can tell that is not the case.

As far as I can tell, since the msal 1.X library is limited to the implicit grant flow, once your token expires, it always requires interaction to refresh.

With that being the case, acquiretokensilent does nothing for me but retrieve a token from my cache, and at that point, there's no real functional difference. I can leave the token with msal and call acquiretokensilent when I make API calls, or I can leave it in memory and just check if it's expired before making API calls. If I keep it in memory though it is easier and faster to retrieve and I can regularly check it to see if it's about to expire, so that I can say, warn the user that by the time they finish with this form, they're going to have to sign back in, and lose what they entered, so they may as well do it now. That's not as big of an issue with the loginpopup method, but we exclusively use the redirect method.

I could use getAccount instead of acquiretokensilent on app load, but there's really no point in the context of my application. The login token provided is not the user's account information for the application, it just verifies who that person is. So whenever a user authenticates with Microsoft, the very next thing that happens is that the application has to send that token to my backend to retrieve their account info. Until that happens the front end does not consider them logged in. So calling get account at startup just adds unnessecary logic since the very next thing I do is always get a token.

I hear you. To be honest I was under the impression that acquiring the token silently would still be able to do so after the token has expired. In my mind msal has that password stored somewhere in the session (hashed or something) so acquireTokenSilent would succeed. It wil only fail when the password of the user in AD has changed or the user's account has been disabled or the token is older than 24 hours. @tnorling am I dreaming here?

In any case, storing it locally makes no difference. On a side note, there's no need to retrieve user details from Azure AD in the backend. With MsGraph you can easily get those details in the front-end. Unless of course your're talking about stored database details about the user, because then you're point is valid.

I was trying to avoid having to maintain a backend by using Firebase. The issue we have however is that our ticketing tool, the one I would like to query, is not publicly available but only within the LAN. On a seconde note, the database suggested by the company is an Azure Cosmos DB. Wouldn't be smart to store those credentials in the front-end either.

Still learning as I go but for now I see a need to build the backend purely to be the intermediary between the front-end and a database I need somewhere and our ticket tool with its API key/user/password combination.

@DarkLite1 msal does not store user passwords, in fact it never touches them. msal merely directs traffic between the application and AAD. Token lifetimes are 1 hour in the browser, but when you sign-in a session token is created and stored on the AAD server, as long as your session with AAD is active you can pass your sid (session id) or loginHint to your acquireTokenSilent call to renew your token, even if the token itself is expired. Once the session has expired you will need to run an interactive call to get a new token and open a new session. I believe the session has a lifetime of 24 hours and is renewed each time it is used within its validity period. Theoretically, if you call acquireTokenSilent at least once every 24 hours you can keep the session active and always silently renew the token when you need it.

@m-sterspace The idea behind msal is to do all the heavy lifting of auth for you. You should not need to maintain your own tokens and if you feel the need to do so please share with us where there may be gaps with the library so we can address them. If you need to inspect tokens for any reason you can always pull them from the cache.

@DarkLite1 @tnorling thank you very much for the discussion and explanation. In the middle of this thread I kind of realized that I must be doing something wrong since Microsoft's javascript apps do not require you to login every hour like mine do and now this is all making a lot more sense as to why.

In regards to managing the token myself, I don't think it's a lack of capability of the library so much as a documentation gap that has lead me to managing it myself. It was a pretty big struggle to get the redirect flow working at all and I wasn't completely clear on what the different parts of the api were doing in relation to that flow, what my app should be doing when, and why each part matters. Once my application was able to get a hold of an actual, verifiable, access token, I was not going to let it go for dear life.

That does all make sense for the most part now, though I'm still a little unclear on the whole sid / loginhint option ... Do I need to pass that for acquiretokensilent to last longer than the hour of the token? And where do the values for those come from? Presumably the sid would be determined by AAD once a session is created, and would then have to be stored by the application in local storage or a cookie to last longer than a single application instance... but for the loginhint option, what are the requirements there? Should the loginhint be unique to a user, or to an application, or to a session? Can I just hardcode it as something like "my-javascript-application"?

@m-sterspace Authentication is a complex subject and we definitely have plans to improve our documentation. Sorry that your experience was less than smooth ramping up.

You can read more about sid and loginHint here. The short story is that loginHint identifies the user, to differentiate between other sessions you may have open with a different account. This is usually in the form of the user's email address. This can also be used to pre-fill the email field on the login screen. sid is an optional claim that you will need to configure in the app portal and will be passed back with the id_token. Please note: you cannot use both. It is one or the other and sid can only be used for silent calls.

@tnorling so from what I gather, the preferred route to go is to use the loginhint.

So the full flow would be that when your app loads, you first try to getaccount to see if there's an account in the cache. If there is, you can get the preferred_username from that account object / idtoken, and pass that as the loginhint to acquiretokensilent?

My only question would then be what the ssoSilent method is for that is mentioned on this page, and when you would call it?

@m-sterspace Yes that sounds correct. However, you may not need to do all that. If you already have an account object, the library should auto-populate the loginHint for you. If you need to provide the sid for any reason, msal will then prefer that over the loginHint

ssoSilent is used to silently acquire a new id_token similar to how acquireTokenSilent acquires an access_token. This can be used to refresh an id_token or to silently login in a scenario where you may have an active AAD session, but msal doesn't know about it. For example, if the cache has been cleared.

@tnorling my app heavily relies on getAccount() to decide if a user is logged on or not. So I assume that when getTokenRedirect or acquireTokenPopup fail the value in getAccount() is cleared. Please correct me if I'm wrong as this a key part in my app. If that's not the case then I need to call clearCache() on failure of both methods.

One last question I have is about the identification string I can use for this specific user in my database to retrieve its preferences for the app. So if user x logs on, we want the preferences of user x coming from Azure CosmosDB. Is the field getAccount().accountIdentifier the one to use in this case?

Was this page helpful?
0 / 5 - 0 ratings