Microsoft-authentication-library-for-js: msal.js loginRedirect loop

Created on 30 Nov 2018  路  21Comments  路  Source: AzureAD/microsoft-authentication-library-for-js

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[x] Bug report  
[ ] Performance issue
[ ] Feature request
[ ] Documentation issue or request
[ ] Other... Please describe:

Browser:

  • [x] Chrome version 70
  • [ ] Firefox version XX
  • [ ] IE version XX
  • [ ] Edge version XX
  • [ ] Safari version XX

Library version


Library version: 0.2.3

## Current behavior I'm using msal.js to authenticate through AAD in a tenant. I would like to use loginRedirect func, but it's looping all the time like TENANT LOGIN > APP (with a hash fragment in the URL) > TENANT LOGIN > .... userAgentApplication has empty fields in the logs. loginPopup is working very well. ## Expected behavior loginRedirect working fine. ## Minimal reproduction of the problem with instructions I have in the configuration clientID, authority, cacheLocation - localStorage, redirectUri to the app and storeAuthStateInCookie - true. I'm using the library as follows:
const graphScopes = ['user.read'];

constructor() {
    super();

    this.userAgentApplication = new Msal.UserAgentApplication(
      applicationConfig.clientID,
      applicationConfig.authority,
      null,
      {
        cacheLocation: 'localStorage',
        redirectUri: 'link_to_the_app',
        storeAuthStateInCookie: true,
      },
    );
    this.token = '';
  }

async componentDidMount() {
    const user = this.userAgentApplication.getUser();
    if (user == null) {
      this.userAgentApplication.loginRedirect(graphScopes);
    } else {
      try {
        this.token = await this.userAgentApplication.acquireTokenSilent(graphScopes);
      } catch () {
        this.token = await this.userAgentApplication.acquireTokenPopup(graphScopes);
      }
    }
  }

The app works in Azure and it's using express server. I have imported @babel/polyfill. Maybe I'm doing something wrong?

bug

Most helpful comment

I think I have a workaround for my app, I'm not sure how well it will work for anyone else. My workaround was to set navigateToLoginRequestUrl to false in the options object when constructing the UserAgentApplication instance, ie:

msalClientApplication = new Msal.UserAgentApplication(clientID, authority, () => {}, {
    navigateToLoginRequestUrl: false
})

All 21 comments

I've been seeing this issue for a few months myself, at least on 0.2.2 and 0.2.3.

Something I've been noticing while I debug it is that if I step through the authentication flows it sometimes succeeds. I'm wondering if there's something going on where:

  1. User requests a token via acquireTokenSilent
  2. User has no cached tokens, so a login flow is initiated
  3. Login flow times out and has no valid token
  4. Control returns back to the app with no valid session credentials, loop back to 1

And I'm wondering if, while debugging, the system state is paused sufficiently to allow the login to not time out and for things to return normally?

In the cases where I am seeing the redirect loop a common thread I've noticed is that coming back from ADB2C the state doesn't have the expected value and the request type is undefined:

Mon, 10 Dec 2018 19:40:48 GMT:0.2.3-Info Processing the callback from redirect response
AuthService.ts:82 Mon, 10 Dec 2018 19:40:48 GMT:0.2.3-Info State status:false; Request type:undefined
AuthService.ts:82 Mon, 10 Dec 2018 19:40:48 GMT:0.2.3-Error State Mismatch.Expected State: 834b1642-636a-4e57-9d68-969ed32c5bb5,Actual State: 17dd38a2-c099-41b7-9bf6-0d8ff1259c4b
AuthService.ts:82 Mon, 10 Dec 2018 19:40:48 GMT:0.2.3-Info User login is required

The "undefined" request type seems wrong; for all successful attempts in our system it seems to be set to "LOGIN"

Apologies for the delay. I was able to reproduce this issue, and I am trying to find a fix. To solve your issue in the interim, have you tried using loginPopup instead of redirect? Does this cause a similar issue if you do?

@maxburke Is this something you are seeing specifically in B2C flows? I am not receiving this error, I am still able to see an idtoken in a hash as @tmszyman has stated

One of the issues we've found with using loginPopup instead of loginRedirect is when the browser blocks popups; in our user testing it hasn't been obvious that they need to enable popups.

I'm using B2C, yes.

I'm not sure if this helps @pkanher617 but I've managed to reproduce the issue I'm seeing much more frequently (nearly 100% now) by setting the async parameter to xhr.open() in XhrClient to false.

I'm wondering if there's a race condition between the openID endpoint fetch call and the rest of the logic?

Digging more ...

I've been having the same issue for the past several weeks. I'm using loginPopup.

I don't know how to fix this, my team decided to write our own plugin based on https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow. @maxburke

Any workaround for this? Getting the same issues about 75% of the time. Using Azure B2C, Sign-in policy.

I think I have a workaround for my app, I'm not sure how well it will work for anyone else. My workaround was to set navigateToLoginRequestUrl to false in the options object when constructing the UserAgentApplication instance, ie:

msalClientApplication = new Msal.UserAgentApplication(clientID, authority, () => {}, {
    navigateToLoginRequestUrl: false
})

We don't need any of the v2.0 stuff yet, so we switched to adal for now.

I'm trying to integrate MSAL with angular application. I'm using angular-cli template.

I have a requirement to show the login screen NOT as pop and need to retrieve access token as soon as the id token is retrieved.

The app that uses the new AAD v2 endpoint and MSAL to obtain tokens for both a WebAPI (using the same ClientID as the Angular app itself).

The loginRedirect is looping back again and again, also i am not able to get the user once the login is complete(sometimes the call back doesn't get invoked)

Here is the code snippet.

Authentication Guard
@Injectable()
export class AuthenticationGuard implements CanActivate, CanActivateChild {

constructor(
private router: Router,
private authService: AuthService,
private graphService: GraphService
) {}

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise {
console.log("Entered Authentication");
if (this.authService.isAuthenticated) {
/** Claim permissions for Infragraph API and User Groups */
console.log("User Authenticated");
return this.claimPermissions();

} else {
  console.log("User Not Authenticated, redirected back to login");
  this.router.navigate(['/login']);
  return this.handleResponse(false);
}

}
}

When user is not authenticated, it calls the login component
Once this is successful the claim permissions method is invoked to get acquire the token.

Login Component
export class LoginComponent implements OnInit {

public isAuthenticated: boolean;
StringConstants = Strings;

constructor(private router: Router,
private route: ActivatedRoute,
private authService: AuthService) { }

ngOnInit() {
const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
this.isAuthenticated = this.authService.isAuthenticated;
if (this.isAuthenticated) {
console.log(returnUrl);
console.log("Navigates to above URL");
this.router.navigate([returnUrl]);
}
else {
this.authService.login();
}
}

}

When the user is not authenticated it calls the auth service to make a login redirect call
constructor() {
///pass consumers/organisations..
const authority = ${'https://login.microsoftonline.com'}/organizations;
console.log(authority);
console.log("Constructor Invoked");
if (!this.app) {
this.app = new Msal.UserAgentApplication(this.applicationConfig.clientID, authority, this.authCallback.bind(this), {
redirectUri: environment.tenentUrl + '/login',
storeAuthStateInCookie : true,
cacheLocation: 'sessionStorage',
loadFrameTimeout : 35000,
validateAuthority : false
});
}
}

public get isAuthenticated(): boolean {
const user = this.app.getUser();
return user !== null;
}

public login(): void {
console.log("login redirect is invoked");
this.app.loginRedirect(this.applicationConfig.webApiScope.concat(this.applicationConfig.graphScope));

}

Please help out with the same.

The below try catch block in the acquireTokenSilent.catch fixed my issue. It seems that when there is a valid idToken in the Url and we do nothing ..the acquireTokenSilent.then success block is triggered. Don't have a more technical explanation why this works, but nothing else removed the redirect loops.

userAgentApplication
  .acquireTokenSilent(b2cScopes)
  .then(token => {
    //AcquireTokenSilent Success
    //Actions after login success here
  })
  .catch(err => {
    //acquireTokenSilent fail, redirect to login
    const idToken = (() => {
      try {
        return window.location.href.split("&id_token=")[1].length > 0; //idToken found in Url
      } catch (err) {
        return false; //token could not be found in url
      }
    })();
    if (!idToken) {
      //if no idToken found in url, redirect
      userAgentApplication.loginRedirect(b2cScopes); //redirect to login
    }
  });

const userAgentApplication = new Msal.UserAgentApplication(
  <CLIENTID>,
  <AUTHORITY>,
  null, //no callback function
  {
    cacheLocation: "localStorage",
    redirectUri: "http://localhost:3000",
    storeAuthStateInCookie: true
  }
);

Apologies for the delay on this. @maxburke why have you set the token redirect callback to null in the application config?

For anyone receiving this issue in the loginPopup methods: can you please post some code or pseudocode of how you are calling login, acquireToken and getUser?

@pkanher617 I recommend check msal.js in case of logging (method loginRedirect) after loading the page, not after clicking the button. Please check my code in the first message.

@pkanher617 setting navigateToLoginRequestUrl: false changes the flow in the UserAgentApplication.js handleAuthenticationResponse so that the id_token is saved to storage via saveTokenFromHash(requestInfo) function.

When navigateToLoginRequestUrl is true, the id_token isn't saved to local storage, then the url is changed to the request url, and getUser isn't able to load the user data because id_token is not available to it - see getUser code

I'm struggling with this issue too, and while it looks like navigateToLoginRequestUrl: false is a good option, it then means I need to determine the original url myself for calls to my api on the same domain (which changes for local / test / prod environments)

Since we are also seeing this redirect loop issue for one of our users, I'm filling in with the information we've been able to gather, to hopefully aid the debugging.

We've not been able to reproduce this issue our self's on any of our computers or phones. One of our customers however have this issue frequently.

Setup
We are running MSAL 0.2.3 against Azure AD B2C with a custom sign-in policy. Our MSAL configuration looks something like this:

export class Authentication {
  constructor(redirectUri) {
    const logger = new Msal.Logger(loggerCallback, { level: Msal.LogLevel.Verbose })

    const options = {
      logger,
      navigateToLoginRequestUrl: false,
      postLogoutRedirectUri: `${location.protocol}//${location.host}`,
      storeAuthStateInCookie: true,
      cacheLocation: 'localStorage',
    }

    this.client = new Msal.UserAgentApplication(config.clientID, config.authority, authCallback, options)

    redirectUri = redirectUri
      ? `${location.protocol}//${location.host}/${redirectUri.replace(/^\/+/g, '')}`
      : `${location.protocol}//${location.host}/redirecttarget`

    this.client._redirectUri = redirectUri
  }

  ...

}

We are using .loginRedirect() to log in, and then we use .acquireTokenSilently() to get the token.

Scenario
If the user browses in an incognito window, or if he removes the cookies set by MSAL, the login work as expected. However, after having been logged in for a while, next time he goes back to our site he ends up in this redirect loop.

Since we have turned verbose logging on, this is what we caught from his console:

[Log] Tue, 05 Feb 2019 10:17:10 GMT:74217166-0.2.3-Info Returned from redirect url (app.09b69c0c768e6c13df32.js, line 1)
[Log] Tue, 05 Feb 2019 10:17:10 GMT:74217166-0.2.3-Info State status:false; Request type:undefined (app.09b69c0c768e6c13df32.js, line 1)
[Log] Tue, 05 Feb 2019 10:17:10 GMT:74217166-0.2.3-Error State Mismatch.Expected State: 85803c1b-363f-40bf-8f3b-2d801fbf09bf,Actual State: 5ecc4e66-ea26-49c1-bf85-3901b2ea1ad1 (app.09b69c0c768e6c13df32.js, line 1)
[Error] Error during login:
Invalid_state
    handleAuthenticationResponse (vendor.64db12be1f3df8cb1886.js:1286:273411)
    t (vendor.64db12be1f3df8cb1886.js:1286:248219)
    t (app.09b69c0c768e6c13df32.js:1:296554)
    YaEn (app.09b69c0c768e6c13df32.js:1:200904)
    o (manifest.2d083d6a1b0b32bf69af.js:1:416)
    NHnr (app.09b69c0c768e6c13df32.js:1:133423)
    o (manifest.2d083d6a1b0b32bf69af.js:1:416)
    webpackJsonp (manifest.2d083d6a1b0b32bf69af.js:1:286)
    Global kod (app.09b69c0c768e6c13df32.js:1)
[Log] Tue, 05 Feb 2019 10:17:10 GMT:74217166-0.2.3-Verbose Token is not in cache for scope:https://********.onmicrosoft.com/api/user_impersonation (app.09b69c0c768e6c13df32.js, line 1)
[Log] Tue, 05 Feb 2019 10:17:10 GMT:74217166-0.2.3-Verbose renewing accesstoken (app.09b69c0c768e6c13df32.js, line 1)
[Log] Tue, 05 Feb 2019 10:17:10 GMT:74217166-0.2.3-Verbose renewToken is called for scope:https://********.onmicrosoft.com/api/user_impersonation (app.09b69c0c768e6c13df32.js, line 1)
[Log] Tue, 05 Feb 2019 10:17:10 GMT:74217166-0.2.3-Info Add msal frame to document:msalRenewFramehttps://********.onmicrosoft.com/api/user_impersonation (app.09b69c0c768e6c13df32.js, line 1)
[Log] Tue, 05 Feb 2019 10:17:10 GMT:74217166-0.2.3-Verbose Renew token Expected state: 7287e053-88be-4d31-acb2-f42af9394abd (app.09b69c0c768e6c13df32.js, line 1)
[Log] Tue, 05 Feb 2019 10:17:10 GMT:74217166-0.2.3-Verbose Set loading state to pending for: https://********.onmicrosoft.com/api/user_impersonation:7287e053-88be-4d31-acb2-f42af9394abd (app.09b69c0c768e6c13df32.js, line 1)
[Log] Tue, 05 Feb 2019 10:17:10 GMT:74217166-0.2.3-Info LoadFrame: msalRenewFramehttps://********.onmicrosoft.com/api/user_impersonation (app.09b69c0c768e6c13df32.js, line 1)
[Log] Tue, 05 Feb 2019 10:17:11 GMT:74217166-0.2.3-Info Add msal frame to document:msalRenewFramehttps://********.onmicrosoft.com/api/user_impersonation (app.09b69c0c768e6c13df32.js, line 1)
[Log] Tue, 05 Feb 2019 10:17:11 GMT:74217166-0.2.3-Info Returned from redirect url (app.09b69c0c768e6c13df32.js, line 1)
[Log] Tue, 05 Feb 2019 10:17:11 GMT:74217166-0.2.3-Info State status:true; Request type:RENEW_TOKEN (app.09b69c0c768e6c13df32.js, line 1)
[Log] Tue, 05 Feb 2019 10:17:11 GMT:74217166-0.2.3-Verbose Window is in iframe, acquiring token silently (app.09b69c0c768e6c13df32.js, line 1)

Without knowing much about the inner workings of MSAL, it seems to me as if the cookie somehow gets corrupted or includes stale information that MSAL can't handle or update properly - since it starts to work as soon as we remove the existing cookie, or it the browser has no cookie at all.

The user is seeing this issue on Safari for MacOS primarily, but has also seen it in Chrome and Firefox for MacOS.

Temporary workaround
Ask the user to always use our application in an incognito window. This works, but it isn't a long-term solution and doesn't feel very professional.

Thought I would share my solution for those of you who use SPA (Vue, React, Angular, etc...).

The standard use of this library results in a login-loop that will last forever. It can only be disrupt by fetching the token in window.onbeforeunload in the callback-function in loginRedirect. That results in loginRedirect only shooting twice but that isn't gonna work for production environment.

So to get rid of the loop I did the following:

First I created these parameters as basic js-code outside of the SPA-structure (Vue = export default {}).

var userAgentApplication = new Msal.UserAgentApplication(applicationConfig.clientID, applicationConfig.authority, authCallback, { navigateToLoginRequestUrl: false });

function authCallback(errorDesc, token, error, tokenType) { cacheLocation: "localStorage"; localStorage.setItem(applicationConfig.accessTokenKey, token); }

Later I created a function inside of my SPA-structure (straight under export default, not in methods) which looks like this. It first checks for any token in localStorage and otherwise calls for loginRedirect. Please note that this part is for Vue-only and that your gonna have to modify the structure a little depending on your framework:

`
AccessToken: function (callback) {
var self = this;
var _accessToken = localStorage.getItem(applicationConfig.accessTokenKey);

    if (_accessToken !== null) {
        callback(_accessToken);
    }

    else {
        userAgentApplication.loginRedirect(applicationConfig.b2cScopes).then(function (idToken) {
            userAgentApplication.acquireTokenPopup(applicationConfig.b2cScopes).then(function (accessToken) {
                localStorage.setItem(applicationConfig.accessTokenKey, accessToken);
                localStorage.setItem(applicationConfig.userTokenKey, idToken);
                callback(accessToken);
            })
        })
    }
},

`

Notice here how I'm using "acquireTokenPopup" instead of the usual "acquireTokenSilent" function to silently call token from the redirect-page without reloading it once more. Using it this way doesn't result in any popup and the flow feels great.

Now I can call it from my Vue-component like this:

AccessToken(function (data) { var token = data; });

Tested on two different systems with three different browsers. (Chrome, Firefox and Edge).

Hope this solves it for you SPA-users!

Hi All,

This seems to fix the login loop from an angular application. Where I'm invoking this from the AppComponent ngOnInit() method.

  msalApp = new UserAgentApplication(this.applicationConfig.clientID, '', () => { });

  public tryLogin() {
        if (!this.msalApp.getUser() && !this.msalApp.loginInProgress()) {
            return this.msalApp.loginRedirect(this.applicationConfig.graphScopes);
        }
    }

Hope this helps.

@tszymanik Apart from the above suggestion made by @ThomasJacob,

We have also recently gone through a redesign of the library's API surface to improve the redirect experience.

Please download our latest preview package or pull the dev branch and try updating your code and see if the issue still persists.

Please re-open this issue if it persists. We are now throwing error stack traces so we can understand better why your code is failing.

If you would like guidance on how to use the new version of the library, please review our wiki page here.

Getting same issue even after updating with current version. User redirect twice to the login page.

Was this page helpful?
0 / 5 - 0 ratings