Angular SPA with an ASP.NET Core API using Azure AD Auth and user access tokens

This post shows how to authenticate an Angular SPA application using Azure AD and consume secure data from an ASP.NET Core API which is protected by Azure AD. Azure AD App registrations are used to configure and setup the authentication and authorization. The Angular application uses the OpenID Connect Code flow with PKCE and the silent renew is implemented using iframes.

Code: https://github.com/damienbod/AzureAD-Auth-MyUI-with-MyAPI

Posts in this Series

Setup the SPA APP registration

In this demo, we will create an APP registration for the Angular application, which will use the API from the first blog in this series. The SPA is a public client and so user access tokens are used. An application running in the browser cannot keep a secret and cannot use a service to service API. In your Azure AD tenant. add a new App registration and select a single page application.

The redirct URLs need to match your Angular application. For development, we use localhost. The silent renew URL is also required.

Add the API permissions which are required for the UI and the API requests. The Web API which was created in the previous blog needs to be added here, so that the SPA application can access the API which is protected by Azure AD.

The email claim is added to the access token and the id token as an optional claim. This is used in the API and the UI.

Angular application

The Angular single page application is implemented using the angular-auth-oidc-client npm package. Open ID Connect code flow with PKCE is used to authenticate. The Angular application was created using Angular-CLI.

{
  "name": "angular-oidc-oauth2",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve --ssl true -o",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "~9.1.3",
    "@angular/common": "~9.1.3",
    "@angular/compiler": "~9.1.3",
    "@angular/core": "~9.1.3",
    "@angular/forms": "~9.1.3",
    "@angular/platform-browser": "~9.1.3",
    "@angular/platform-browser-dynamic": "~9.1.3",
    "@angular/router": "~9.1.3",
    "angular-auth-oidc-client": "^11.1.3",
    "rxjs": "~6.5.4",
    "tslib": "^1.10.0",
    "zone.js": "~0.10.2"
  },

In the angular.json file, the silent renew html needs to be added to the assets, and the https configuration which uses your developer certificates are added to the serve json object. Here’s a link to an example.

angular.json silent-renew.html

angular.json certificates

The silent renew for code flow in an iframe can be created as follows:

<!doctype html>
<html>
<head>
    <base href="./">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>silent-renew</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>

  <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>
</body>
</html>

In the app.module, the OIDC Azure configuration is added. This example is for a user of a tenant. The tenant ‘7ff95b15-dc21-4ba6-bc92-824856578fc1’ is used for the token server and the authWellknownEndpoint. Code flow is configured and the silent renew is activated and the redirect is setup like configured in the App registration. The ID token is used for the user data and the user data request is not activated. The scope api://98328d53-55ec-4f14-8407-0ca5ff2f2d20/access_as_user needs to be requested to access the API.

export function configureAuth(oidcConfigService: OidcConfigService) {
  return () =>
    oidcConfigService.withConfig({
            stsServer: 'https://login.microsoftonline.com/7ff95b15-dc21-4ba6-bc92-824856578fc1/v2.0',
            authWellknownEndpoint: 'https://login.microsoftonline.com/7ff95b15-dc21-4ba6-bc92-824856578fc1/v2.0',
            redirectUrl: window.location.origin,
            clientId: 'ad6b0351-92b4-4ee9-ac8d-3e76e5fd1c67',
            scope: 'openid profile email api://98328d53-55ec-4f14-8407-0ca5ff2f2d20/access_as_user',
            responseType: 'code',
            silentRenew: true,
            maxIdTokenIatOffsetAllowedInSeconds: 600,
            issValidationOff: false, // this needs to be true if using a common endpoint in Azure
            autoUserinfo: false,
            silentRenewUrl: window.location.origin + '/silent-renew.html',
            logLevel: LogLevel.Debug
    });
}

A HttpInterceptor is used to add the access token to all requests which match a base address of the API. It is really important that the access token is only sent to APIs where the access token was intended to be used. Don’t not send the access token with every HTTP request. Localhost with port 44390 is where the API secured with Azure AD is hosted for our development.

import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { AuthService } from './auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  private secureRoutes = ['https://localhost:44390'];

  constructor(private authService: AuthService) {}

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ) {
    if (!this.secureRoutes.find((x) => request.url.startsWith(x))) {
      return next.handle(request);
    }

    const token = this.authService.token;

    if (!token) {
      return next.handle(request);
    }

    request = request.clone({
      headers: request.headers.set('Authorization', 'Bearer ' + token),
    });

    return next.handle(request);
  }
}

The home component starts the sign in for the APP and the user. If successful, the API can be called and the data is returned.

import { Component, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AuthService } from '../auth.service';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-home',
  templateUrl: 'home.component.html',
})
export class HomeComponent implements OnInit {
  userData$: Observable<any>;
  dataFromAzureProtectedApi$: Observable<any>;
  isAuthenticated$: Observable<boolean>;
  constructor(
    private authservice: AuthService,
    private httpClient: HttpClient
  ) {}

  ngOnInit() {
    this.userData$ = this.authservice.userData;
    this.isAuthenticated$ = this.authservice.signedIn;
  }

  callApi() {
    this.dataFromAzureProtectedApi$ = this.httpClient
      .get('https://localhost:44390/weatherforecast')
      .pipe(catchError((error) => of(error)));
  }
  login() {
    this.authservice.signIn();
  }

  forceRefreshSession() {
    this.authservice.forceRefreshSession().subscribe((data) => {
      console.log('Refresh completed');
    });
  }

  logout() {
    this.authservice.signOut();
  }
}

Start the API and then the Angular application. After you login, the SPA can call the API and access the API data.

Links:

https://github.com/AzureAD/microsoft-identity-web

https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2

https://jwt.io/

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

2 comments

  1. […] Angular SPA with an ASP.NET Core API using Azure AD Auth and user access tokens (Damien Bowden) […]

  2. […] Angular SPA with an ASP.NET Core API using Azure AD Auth and user access tokens – 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: