AzureAD / microsoft-authentication-library-for-js

Microsoft Authentication Library (MSAL) for JS
http://aka.ms/aadv2
MIT License
3.63k stars 2.64k forks source link

Logout does not work with Application Single sign-on configuration #7345

Open TanyaMykhnevych opened 2 days ago

TanyaMykhnevych commented 2 days ago

Core Library

MSAL.js (@azure/msal-browser)

Core Library Version

2.26.0

Wrapper Library

MSAL Angular (@azure/msal-angular)

Wrapper Library Version

2.3.2

Public or Confidential Client?

Public

Description

I have Application SingleSignOn session behavior in my custom Azure AD B2C policy. I also have Angular SPA, it uses msal library. I can not logout. When I press logout, user is redirected to main page, after that it is redirected to login page and automatic sign in is performed. When I change scope to Tenant or Suppressed , it works okay.

Error Message

No response

MSAL Logs

No response

Network Trace (Preferrably Fiddler)

MSAL Configuration

return new PublicClientApplication({
        auth: {
            authority: b2cPolicies.authorities.signUpSignIn.authority,
            clientId: environment.msalClientId,
            knownAuthorities: [environment.authorityName],
            postLogoutRedirectUri: window.location.origin,
            redirectUri: `/`,
        },
        cache: {
            cacheLocation: BrowserCacheLocation.LocalStorage,
            storeAuthStateInCookie: isIE, // set to true for IE 11
        },
    });

Relevant Code Snippets

Custom Azure AD B2C policy Signup_Signin

<UserJourneyBehaviors>
      <SingleSignOn Scope="Application" KeepAliveInDays="30"/>
      <SessionExpiryType>Absolute</SessionExpiryType>
      <SessionExpiryInSeconds>1200</SessionExpiryInSeconds>
      <JourneyInsights TelemetryEngine="ApplicationInsights" InstrumentationKey="046c8d7d-66fe-493e-9493-cc4f46cf065b" DeveloperMode="true" ClientEnabled="false" ServerEnabled="true" TelemetryVersion="1.0.0" />
      <ScriptExecution>Allow</ScriptExecution>
</UserJourneyBehaviors>

Typescript code:

import { HttpErrorResponse } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core';
import { MSAL_GUARD_CONFIG, MsalBroadcastService, MsalGuardConfiguration, MsalService } from '@azure/msal-angular';
import {
    AccountInfo,
    AuthenticationResult,
    InteractionRequiredAuthError,
    InteractionStatus,
    RedirectRequest,
    SilentRequest,
} from '@azure/msal-browser';
import { Observable, of, Subject } from 'rxjs';
import {
    catchError, delay, filter, map, switchMap, take, tap,
} from 'rxjs/operators';
import { environment } from '../../../../environments/environment';
import { AzureErrorCodes } from '../../enum';
import { apiConfig } from '../apiconfig';
import { TokenService } from './token.service';

@Injectable()
export class AuthService {
    private _authenticationTokenSubject$: Subject<void> = new Subject<void>();

    constructor(
        @Inject(MSAL_GUARD_CONFIG) private _msalGuardConfig: MsalGuardConfiguration,
        private _tokenService: TokenService,
        private _msalService: MsalService,
        private _msalBroadcastService: MsalBroadcastService,
    ) {
        this._msalBroadcastService.inProgress$.pipe(
            filter((status: InteractionStatus) => status === InteractionStatus.None),
            switchMap(() => this.getAuthenticationToken()),
        ).subscribe();
    }

    public isAuthenticated(): boolean {
        return Boolean(this._msalService.instance.getActiveAccount());
    }

    public loginRedirect(): Observable<void> {
        if (this._msalGuardConfig.authRequest) {
            return this._msalService.loginRedirect({ ...this._msalGuardConfig.authRequest } as RedirectRequest);
        }

        return this._msalService.loginRedirect();
    }

    public logout(): void {
        this._logout().subscribe();
    }

    public unauthorize(): void {
        this._msalService.logout();
        this._tokenService.clearToken();
    }

    public getAuthenticationToken(): Observable<string> {
        this.checkAndSetActiveAccount();
        const params: SilentRequest = {
            account: this._msalService.instance.getActiveAccount(),
            scopes: apiConfig.b2cScopes,
        };

        return this._msalService.acquireTokenSilent(params).pipe(
            switchMap((response: AuthenticationResult) => {
                if (new Date(response.expiresOn) > new Date()) {
                    this._tokenService.token = response.accessToken;
                    this._authenticationTokenSubject$.next();

                    return response.accessToken;
                }

                throw new Error('Token is expired');
            }),
            catchError((error: HttpErrorResponse) => {
                if (error instanceof InteractionRequiredAuthError) {
                    if (error.errorMessage.includes(AzureErrorCodes.SessionNotExist)) {
                        return this._logout().pipe(map(() => ''));
                    }

                    if (error.errorMessage.includes('interaction_required')) {
                        return this._msalService.acquireTokenRedirect(params).pipe(map(() => ''));
                    }

                    if (error.errorCode !== 'block_token_requests' &&
                        error.errorCode !== 'user_cancelled' &&
                        error.errorCode !== 'login_progress_error') {
                        throw error;
                    }
                } else if (error.message === 'Token is expired') {
                    return this.loginRedirect().pipe(map(() => ''));
                } else {
                    return this._msalService.acquireTokenRedirect(params).pipe(map(() => ''));
                }

                return of('');
            }),
        );
    }

    public checkAndSetActiveAccount(): void {
        const account = this._msalService.instance.getAllAccounts().find(
            (item: AccountInfo) => item.environment === environment.authorityName,
        );

        if (account) {
            this._msalService.instance.setActiveAccount(account);
        }
    }

    public waitForAuthenticationToken(): Observable<void> {
        return this._authenticationTokenSubject$.asObservable().pipe(take(1));
    }

    private _logout(): Observable<void> {
        const delayTime = 2000;

        return this._msalService.logout().pipe(
            tap(() => this._tokenService.clearToken()),
            delay(delayTime),
            switchMap(() => this.loginRedirect()),
        );
    }
}

Reproduction Steps

  1. Set SingleSignOn Scope="Application" in Azure AD B2C Signup_Signin custom policy;
  2. Implement authorization in Angular App using msal.js with code provided;
  3. Log In;
  4. Log Out;

Expected Behavior

User is logged out, session is ended. Log In page is displayed.

Identity Provider

Azure B2C Custom Policy

Browsers Affected (Select all that apply)

Chrome, Firefox, Edge

Regression

No response

Source

External (Customer)

TanyaMykhnevych commented 1 day ago

I noticed, that when I remove sso cookie by hands before pressing "Logout", it successfully logs me out. image