Sometimes Angular applications are required to authenticate against multiple identity providers. This blog post shows how to implement an Angular SPA which authenticates using Auth0 for one identity provider and also IdentityServer4 from Duende software as the second. The SPA can logout from both of the identity providers individually and also revoke the refresh token used to renew the session using the revocation endpoint. The endsession endpoint is used to logout. IdentityServer4 also supports introspection so that it is possible to revoke reference tokens on a logout as well as the refresh token. The Angular application uses OpenID Connect code flow with PKCE implemented using the npm angular-auth-oidc-client library.

Code: https://github.com/damienbod/angular-auth-oidc-client/tree/main/projects/sample-code-flow-multi-Auth0-ID4

The angular-auth-oidc-client npm package can be added to the application in the App.Module class. We used an AuthConfigModule module to configure the settings and can keep the AppModule small.

 import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; import { AuthConfigModule } from './auth-config.module'; import { HomeComponent } from './home/home.component'; import { UnauthorizedComponent } from './unauthorized/unauthorized.component';  @NgModule({   declarations: [AppComponent, HomeComponent, UnauthorizedComponent],   imports: [     BrowserModule,     RouterModule.forRoot([       { path: '', redirectTo: 'home', pathMatch: 'full' },       { path: 'home', component: HomeComponent },       { path: 'forbidden', component: UnauthorizedComponent },       { path: 'unauthorized', component: UnauthorizedComponent },     ]),     AuthConfigModule,   ],   providers: [],   bootstrap: [AppComponent], }) export class AppModule {}  

The AuthConfigModule configures the two identity providers. Both clients are step up to use the Open ID Connect code flow with PKCE (proof key for code exchange). Each secure token server (STS) has it's specifics configurations which are required to use this flow. These configurations must match the corresponding client configuration on the token service of the identity provider. All identity providers have their own flavour of the Open ID Connect specification and also support different features. Some of these are worse, some are better. When implementing SPA applications, you should validate the required features supported by the identity provider.

 import { NgModule } from '@angular/core'; import { AuthModule, LogLevel } from 'angular-auth-oidc-client';  @NgModule({   imports: [     AuthModule.forRoot({       config: [         {           authority: 'https://offeringsolutions-sts.azurewebsites.net',           redirectUrl: window.location.origin,           postLogoutRedirectUri: window.location.origin,           clientId: 'angularCodeRefreshTokens',           scope: 'openid profile email taler_api offline_access',           responseType: 'code',           silentRenew: true,           useRefreshToken: true,           logLevel: LogLevel.Debug,         },         {           authority: 'https://dev-damienbod.eu.auth0.com',           redirectUrl: window.location.origin,           postLogoutRedirectUri: window.location.origin,           clientId: 'Ujh5oSBAFr1BuilgkZPcMWEgnuREgrwU',           scope: 'openid profile offline_access auth0-user-api-spa',           responseType: 'code',           silentRenew: true,           useRefreshToken: true,           logLevel: LogLevel.Debug,           customParamsAuthRequest: {             audience: 'https://auth0-api-spa',           },         },       ],     }),   ],   exports: [AuthModule], }) export class AuthConfigModule {} 

The checkAuthMultiple function is used to initialize the authentication state and begin the flows or whatever. An SPA application requires some route when this logic can run before the guards execute. This is not a server rendered application where the security runs in the trusted backend. The initializing logic setups the state and handles callbacks form the different authentication flows. Great care should be taken when using guards. If a guard is applied to the default route, you need to ensure that the initialization logic runs first. The checkAuthMultiple function should only be called once in the application.

 import { Component, OnInit } from '@angular/core'; import { OidcSecurityService } from 'angular-auth-oidc-client';  @Component({   selector: 'app-root',   templateUrl: 'app.component.html', }) export class AppComponent implements OnInit {   constructor(public oidcSecurityService: OidcSecurityService) {}    ngOnInit() {     this.oidcSecurityService.checkAuthMultiple().subscribe(([{ isAuthenticated, userData, accessToken }]) => {       console.log('Authenticated', isAuthenticated);     });   } } 

The sign-in logout, revoke tokens can then be implemented anywhere for the different identity providers. Each client configuration has its own configuration ID which can be used to run or start the required authentication flow, logout or whatever.

 import { Component, OnInit } from '@angular/core'; import {   AuthenticatedResult,   OidcClientNotification,   OidcSecurityService,   OpenIdConfiguration,   UserDataResult, } from 'angular-auth-oidc-client'; import { Observable } from 'rxjs';  @Component({   selector: 'app-home',   templateUrl: 'home.component.html', }) export class HomeComponent implements OnInit {   configurations: OpenIdConfiguration[];   userDataChanged$: Observable<OidcClientNotification<any>>   userData$: Observable<UserDataResult>   isAuthenticated$: Observable<AuthenticatedResult>    constructor(public oidcSecurityService: OidcSecurityService) {}    ngOnInit() {     this.configurations = this.oidcSecurityService.getConfigurations();     this.userData$ = this.oidcSecurityService.userData$;     this.isAuthenticated$ = this.oidcSecurityService.isAuthenticated$;   }    login(configId: string) {     this.oidcSecurityService.authorize(configId);   }    forceRefreshSession() {     this.oidcSecurityService.forceRefreshSession().subscribe((result) => console.warn(result));   }    logout(configId: string) {     this.oidcSecurityService.logoff(configId);   }    refreshSessionId4(configId: string) {     this.oidcSecurityService.forceRefreshSession(null, configId).subscribe((result) => console.log(result));   }    refreshSessionAuth0(configId: string) {     this.oidcSecurityService       .forceRefreshSession({ scope: 'openid profile offline_access auth0-user-api-spa' }, configId)       .subscribe((result) => console.log(result));   }    logoffAndRevokeTokens(configId: string) {     this.oidcSecurityService.logoffAndRevokeTokens(configId).subscribe((result) => console.log(result));   }    revokeRefreshToken(configId: string) {     this.oidcSecurityService.revokeRefreshToken(null, configId).subscribe((result) => console.log(result));   } }  

The application can be run and you can logout or use the required configuration. Underneath the identity has not signed in with IdentityServer and the Auth0 client has be authenticated. Each identity provider is independent from the other.

The multiple identity provider support is implemented using redirects. You could also implement all of this using popups depending on you use cases.

Links:

https://github.com/damienbod/angular-auth-oidc-client

https://auth0.com/

https://github.com/IdentityServer/IdentityServer4

https://duendesoftware.com/