'Msal Angular errors out immediately after redirect back to application
I am creating an Angular application that uses Azure AD and thus msal-angular for auth purposes. It works fine, except for this one crucial detail: I open the app and msal detects I'm not logged in, so it redirects me to the login page. I log in and am redirected back to my app. However, I immediately get an error (twice, directly after one another), going something like
ERROR Error: Uncaught (in promise): BrowserAuthError: no_account_error: No account object provided to acquireTokenSilent and no active account has been set. Please call setActiveAccount or provide an account on the request.
AuthError@http://localhost:4200/vendor.js:22968:24
BrowserAuthError@http://localhost:4200/vendor.js:14464:28
7390/BrowserAuthError</BrowserAuthError.createNoAccountError@http://localhost:4200/vendor.js:14603:16
...
The page remains largely broken, it is obvious that auth does not work. So I reload the page and that fixes it. The app obviously remembers that it has been authenticated and everyting loads just fine.
I have put logging statements into multiple locations of the app that play a role during initialization and auth, and it turns out that the error occurs very early on, probably before any of my own logic starts to play a role. Here's what I'm logging after the redirect:
Before creating PublicClientApplication // explained later
After creating PublicClientApplication
Initializing application // logged from the constructor of app.module
ERROR ...
[webpack-dev-server] Disconnected!
[webpack-dev-server] Trying to reconnect...
Before creating PublicClientApplication
After creating PublicClientApplication
Initializing application
ERROR ...
[webpack-dev-server] Live Reloading enabled.
msalSubject$ -> msal:loginSuccess // logging events from the MsalBroadcastService.msalSubject$
msalSubject$ -> msal:handleRedirectEnd
redirectObservable -> data (token and account info) // logging events from the MsalService.handleRedirectObservable()
Setting active account // PublicClientApplication.setActiveAccount()
// delay 1 s
// PublicClientApplication.acquireTokenSilent(...)
ERROR: BrowserAuthError: no_account_error: No account object provided to acquireTokenSilent and no active account has been set. Please call setActiveAccount or provide an account on the request.
So this complains about a missing account three times: Two times too early for me to do anything about it, and once later, after I have in fact set the active account.
The messages Before/After creating PublicClientApplication are logged from within app.module
let x = console.log('Before creating PublicClientApplication');
const publicClientApplication = new PublicClientApplication({
auth: {
clientId: environment.azureAD.clientID,
authority: environment.azureAD.authority, // This is your tenant ID
redirectUri: environment.azureAD.redirectURI // This is your redirect URI,
},
cache: {
cacheLocation: 'localStorage',
storeAuthStateInCookie: isIE, // Set to true for Internet Explorer 11
}
});
x = console.log('After creating PublicClientApplication');
In summary, the first two errors seem to come from somewhere "inside" the library. Has anyone had similar issues or an idea, what the problem might be? Have I made a fundamental mistake somewhere and should approach this differently? Documentation of this library is terrible and it has changed drastically in recent times. Many examples online don't apply anymore...
The third error is strange, too. Maybe the first error broke the lib? Because the same logic is used in cases when it actually works:
For reference, here is how the sequence goes after I refresh the page:
[webpack-dev-server] Disconnected!
[webpack-dev-server] Trying to reconnect...
[webpack-dev-server] Live Reloading enabled.
Before creating PublicClientApplication
After creating PublicClientApplication
msalSubject$ -> msal:acquireTokenStart
Initializing application
msalSubject$ -> msal:handleRedirectEnd
redirectObservable -> null
Angular is running in development mode. Call enableProdMode() to enable production mode.
msalSubject$ -> msal:acquireTokenSuccess // This is the event I use to infer login status, because it carries account data
Setting active account
// acquireTokenSilent(...)
... -> successful communication with backend
So that's just fine.
Here's what I do in my AuthenticationService:
// authentication.service.ts
...
@Injectable({
providedIn: 'root'
})
export class AuthenticationService {
login$: Observable<void> = merge(
this.msalBroadcastService.msalSubject$.pipe( // working part, triggered when already logged in
filter((msg: EventMessage) => msg.eventType == EventType.ACQUIRE_TOKEN_SUCCESS),
tap(() => console.log('Msal login successful')),
// @ts-ignore (even the type definitions are off, this works!)
filter(result => !!result?.payload?.account),
tap((result) => {
// @ts-ignore
console.log('Setting active account: ', result.payload.account);
// @ts-ignore
this.publicClientApplication.setActiveAccount(result.payload.account);
}),
map(() => {}),
tap(() => console.log('Logging in'))
),
this.msalService.handleRedirectObservable().pipe( // broken part, triggered after redirect
filterFalsy(), // the first thing this emits is null
tap((res) => console.log('Account info coming in: ', res, 'Setting active account')),
tap(result => this.publicClientApplication.setActiveAccount(result.account)),
delay(1000), // just in case, but didn't help
map(() => {}),
tap(() => console.log('Logging in too'))
)
);
...
}
This observable is registered in a local, Redux-like store and triggers setting a flag to true. The AuthService's isLoggedIn$() method returns an observable of this flag.
And then, in my backend service
// backend.service.ts
...
export class BackendService {
...
private readyForConnection$ = this.authenticationService.isLoggedIn$().pipe(
filterFalsy(),
take(1),
tap(() => console.log('isLoggedIn$ has fired. Has the account been set?')),
switchMapTo(fromPromise(this.publicClientApplication.acquireTokenSilent(this.accessTokenRequest))),
map(tokens => ({ accessToken: tokens.accessToken }))
);
I need this readyForConnection$ observable to authenticate a WebSocket. I can only connect once I have the accessToken.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
