angularoauth-2.0azure-ad-b2cangular-auth-oidc-client

Can't authorize user in Azure AD B2C using angular-auth-oidc-client


I have an angular app (ver. 20.2, zoneless, standalone), running locally for now at https://localhost:4200. The app uses Angular Auth OIDC Client.

As per docs, I use autoLoginPartialRoutesGuard for app's routes:

export const routes: Routes = [
   {
      path: "register",
      component: IlgRegisterUser
   },
   {
      path: "login",
      component: IlgLogin
   },
   {
      path: "users",
      component: IlgUsersList,
      canActivate: [autoLoginPartialRoutesGuard]
   },
   {
      path: "unauthorized",
      component: IlgUnauthorized
   }
];

The app.config.ts looks like this:

export const appConfig: ApplicationConfig = {
   providers: [
      provideBrowserGlobalErrorListeners(),
      provideZonelessChangeDetection(),

      provideHttpClient(withInterceptors([authInterceptor()])),
      provideAuth(authConfig, withAppInitializerAuthCheck()),

      provideRouter(routes) // , withEnabledBlockingInitialNavigation()
   ]
};

The auth for Azure AD B2C is the following:

export const authConfig: PassedInitialConfig = {
   config: {
      authority: "https://my-domain.b2clogin.com/{TenantId}/v2.0/",
      authWellknownEndpointUrl: "https://my-domain.b2clogin.com/my-domain.onmicrosoft.com/B2C_1_si/v2.0/.well-known/openid-configuration",

      redirectUrl: "https://localhost:4200",
      postLogoutRedirectUri: window.location.origin,

      triggerAuthorizationResultEvent: false,
      unauthorizedRoute: "/unauthorized",

      clientId: "{ClientId Guid}",
      scope: "openid offline_access",
      responseType: "code",
      silentRenew: true,
      useRefreshToken: true,
      ignoreNonceAfterRefresh: true,
      maxIdTokenIatOffsetAllowedInSeconds: 600,
      issValidationOff: false, // this needs to be true if using a common endpoint in Azure
      autoUserInfo: false,
      logLevel: LogLevel.Debug,
      customParamsAuthRequest: {
         prompt: "select_account" // login, consent
      }
   }
};

The app.component.ts uses checkAuth() to check authentication status (even though I use provideAuth(authConfig, withAppInitializerAuthCheck()) in app.config.ts):

@Component({
   selector: "ilg-root",
   imports: [AsyncPipe, JsonPipe, RouterOutlet, MatSidenavModule, MatButtonModule, MatIconModule, MatDividerModule, IlgNavBar],
   templateUrl: "./app.html",
   styleUrl: "./app.scss"
})
export class App implements OnInit {
   ngOnInit(): void {
      this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated, userData, accessToken, idToken, configId }) => {
         console.log(`isAuthenticated: ${isAuthenticated}`);
      });
   }

   protected readonly oidcSecurityService = inject(OidcSecurityService);
   protected authenticated = this.oidcSecurityService.authenticated;
   protected userData$ = this.oidcSecurityService.userData$;
}

The app.component.html shows the auth info:

      <p>User is {{ oidcSecurityService.authenticated().isAuthenticated ? "Authenticated" : "NOT authenticated" }}</p>
      <br />
      UserData
      <pre>{{ userData$ | async | json }}</pre>

When I open the /users page, auth kicks in and I see the Azure AD B2C log in page. I enter my user/pass and then I get back to the https://localhost:4200, which shows me now:

  1. User is Authenticated
  2. The whole bunch of claims from my user's token

The problem is if I go to the /users/ page again, being authenticated, it redirects me to Azure B2C Auth login page! And whatever I do it works this way only!


Solution

  • I ended up using Microsoft's own MSAL.js. They have examples and their code just works. Here's part of my signal store for Auth:

    export const AuthStore = signalStore(
       { providedIn: "root" },
       withState(initialState),
    
       withMethods(
          (
             store,
             msalGuardConfig: MsalGuardConfiguration = inject<MsalGuardConfiguration>(MSAL_GUARD_CONFIG),
             authService: MsalService = inject(MsalService)
          ) => ({
             setAuth(account: AccountInfo | null): void {
                patchState(store, {
                   account,
                   idToken: account?.idToken ?? null,
                   idTokenClaims: account?.idTokenClaims as IdTokenClaims | null
                });
             },
    
             clearAuth(): void {
                patchState(store, initialState);
             },
    
             loginRedirect(): void {
                if (msalGuardConfig.authRequest) {
                   authService.loginRedirect({
                      ...msalGuardConfig.authRequest
                   } as RedirectRequest);
                } else {
                   authService.loginRedirect({ scopes: ["openid", "profile", "email"] });
                }
             },
    
             loginPopup(): void {
                if (msalGuardConfig.authRequest) {
                   authService.loginPopup({ ...msalGuardConfig.authRequest } as PopupRequest).subscribe((response: AuthenticationResult) => {
                      authService.instance.setActiveAccount(response.account);
                   });
                } else {
                   authService.loginPopup().subscribe((response: AuthenticationResult) => {
                      authService.instance.setActiveAccount(response.account);
                   });
                }
             },
    
             logout(popup?: boolean): void {
                if (popup) {
                   authService.logoutPopup({
                      mainWindowRedirectUri: "/"
                   });
                } else {
                   authService.logoutRedirect();
                }
    
                this.clearAuth();
             }
          })
       )
    );
    

    MSAL.js is supported and actively maintained. Unlike other libs. Some turn to garbage, some introduce paid licenses.