Securing an Angular application using Azure B2C

This article shows how to secure an Angular application using Azure B2C with OpenID Connect Code Flow and PKCE. The silent renew is supported using iframes.

Code: Angular Azure B2C

Setting up Azure B2C

In the Azure portal, create a new App registration in your B2C tenant. Configure a mobile and desktop application and add the Angular redirect. We do this because we use a public client and cannot keep a secret in the Angular application. This configuration supports requesting the token using the code and no secret together with PKCE (Proof Key for Code Exchange). Also add your silent renew URL.

You could just add this directly to the manifest, if you want. Now add the Graph API scopes which can be requested.

Angular OpenID Connect Code flow with PKCE

The OpenID Connect flow is supported in the Angular application by using the angular-auth-oidc-client npm package. This needs to be added to your project in the package.json file.

"angular-auth-oidc-client": "11.1.0",

The Angular application is initialized in the App.Module. You can define the Azure B2C settings as configured for your tenant. The following example uses the id_token for the user profile data, and the session is renewed using an iframe and the file silent-renew.html. The session will refresh 60 seconds before it expires. The application uses both the access token (with the expires_in property) and the id_token to check this. The response type is ‘code’ and we want to use the OIDC Code flow with PKCE.

import { HttpClientModule } from '@angular/common/http';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { AuthModule, LogLevel, OidcConfigService, OidcSecurityService } from 'angular-auth-oidc-client';
import { AppComponent } from './app.component';
import { routing } from './app.routes';
import { AuthorizationGuard } from './authorization.guard';
import { AutoLoginComponent } from './auto-login/auto-login.component';
import { ForbiddenComponent } from './forbidden/forbidden.component';
import { HomeComponent } from './home/home.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { ProtectedComponent } from './protected/protected.component';
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';

export function loadConfig(oidcConfigService: OidcConfigService) {
    return () =>
        oidcConfigService.withConfig({
            stsServer: 'https://login.microsoftonline.com/damienbod.onmicrosoft.com/v2.0',
            authWellknownEndpoint:
                'https://damienbod.b2clogin.com/damienbod.onmicrosoft.com/B2C_1_b2cpolicydamien/v2.0/.well-known/openid-configuration',
            redirectUrl: window.location.origin,
            postLogoutRedirectUri: window.location.origin,
            clientId: 'f1934a6e-958d-4198-9f36-6127cfc4cdb3',
            scope: 'openid https://damienbod.onmicrosoft.com/testapi/demo.read',
            responseType: 'code',
            silentRenew: true,
            autoUserinfo: false,
            silentRenewUrl: window.location.origin + '/silent-renew.html',
            logLevel: LogLevel.Debug,
            renewTimeBeforeTokenExpiresInSeconds: 60,
        });
}

@NgModule({
    declarations: [
        AppComponent,
        NavMenuComponent,
        HomeComponent,
        AutoLoginComponent,
        ForbiddenComponent,
        UnauthorizedComponent,
        ProtectedComponent,
    ],
    imports: [
        BrowserModule,
        HttpClientModule,
        AuthModule.forRoot(),
        FormsModule,
        routing,
    ],
    providers: [
        OidcSecurityService,
        OidcConfigService,
        {
            provide: APP_INITIALIZER,
            useFactory: loadConfig,
            deps: [OidcConfigService],
            multi: true,
        },
        AuthorizationGuard,
    ],
    bootstrap: [AppComponent],
})
export class AppModule {}

The checkAuth function, which will initialize the state of your auth is called in the app.component. This ensures that the auth will work after an F5, of if you want to implement login logic here, this is possible.

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.checkAuth().subscribe((isAuthenticated) => 
          console.log('app authenticated', isAuthenticated));
    }
}

The angular-auth-oidc-client npm package can be used anywhere in the application and a login can be sent to the STS. The isAuthenticated$ can be used to check the if you are authenticated, and the user data, profile data can be accessed using userData$.

import { Component, OnInit } from '@angular/core';
import { OidcClientNotification, OidcSecurityService, PublicConfiguration } 
    from 'angular-auth-oidc-client';
import { Observable } from 'rxjs';

@Component({
    selector: 'app-home',
    templateUrl: 'home.component.html',
})
export class HomeComponent implements OnInit {
    configuration: PublicConfiguration;
    userDataChanged$: Observable<OidcClientNotification<any>>;
    userData$: Observable<any>;
    isAuthenticated$: Observable<boolean>;

    constructor(public oidcSecurityService: OidcSecurityService) {}

    ngOnInit() {
        this.configuration = this.oidcSecurityService.configuration;
        this.userData$ = this.oidcSecurityService.userData$;
        this.isAuthenticated$ = this.oidcSecurityService.isAuthenticated$;
    }

    login() {
        this.oidcSecurityService.authorize();
    }

    logout() {
        this.oidcSecurityService.logoff();
    }
}

The Angular application uses silent renew to refresh the session with Angular B2C. At present Azure B2C does not support the APIs required to use refresh tokens a browser app in a safe way. So at present, silent renew using iframes is the only way this can be done. Or of course just login again. This is supported by adding a html file to your application.

<script>
	window.onload = function () {
		/* The parent window hosts the Angular application */
		var parent = window.parent;
		/* Send the id_token information to the oidc message handler */
		var event = new CustomEvent('oidc-silent-renew-message', 
                   { detail: window.location });
		parent.dispatchEvent(event);
	};
</script>

In Angular-CLI, the silent-renew.html file needs to be added to the angular.json, like the default index.html.

Now the application runs using Azure B2C as the token server.

Links

https://openid.net/specs/openid-connect-core-1_0.html

https://azure.microsoft.com/en-us/services/active-directory/external-identities/b2c/

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

https://www.npmjs.com/package/angular-auth-oidc-client

https://oauth.net/2/pkce/

One comment

  1. […] Securing an Angular application using Azure B2C (Damien Bowden) […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: