AzureAD / microsoft-authentication-library-for-js

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

'Remember Me' does not work with Suppressed Single sign-on configuration #7344

Closed TanyaMykhnevych closed 3 weeks ago

TanyaMykhnevych commented 1 month 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 implemented Keep Me Signed In according to Microsoft doc: https://learn.microsoft.com/en-us/azure/active-directory-b2c/session-behavior?pivots=b2c-custom-policy#enable-keep-me-signed-in-kmsi. I have Suppressed SingleSignOn in my custom Azure AD B2C policy. I also have Angular SPA,it uses msal library. But it logs me out after 24 hours. When I change scope to Tenant , it works okay.

Also I noticed that sso cookie does not exist for Suppressed scope.

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="Suppressed" 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="Suppressed" in Azure AD B2C Signup_Signin custom policy;
  2. Enable 'Remember Me' in Azure AD B2C custom policy;
  3. Implement authorization in Angular App using msal.js with code provieded;
  4. Select 'Remember Me' during Sign In.

Expected Behavior

User is not prompted to login for 30 days.

Identity Provider

Azure B2C Custom Policy

Browsers Affected (Select all that apply)

Chrome, Firefox, Edge

Regression

No response

Source

External (Customer)

TanyaMykhnevych commented 3 weeks ago

I asked Azure Microsoft Suuport. They said that if you are using SingleSignOn Scope='Suppressed' it will not modify or generate an SSO cookie, this is by design as it will not use one in this situation as it is intended to prompt for authentication when switching journeys https://learn.microsoft.com/en-us/azure/active-directory-b2c/relyingparty#singlesignon.