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

History

2021-03-05 Updated Microsoft.Identity.Web to 1.7.0, switch to refresh tokens

2021-01-31 Updated Microsoft.Identity.Web to 1.5.1, Angular 11.1.1

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": "~11.2.4",
    "@angular/common": "~11.2.4",
    "@angular/compiler": "~11.2.4",
    "@angular/core": "~11.2.4",
    "@angular/forms": "~11.2.4",
    "@angular/platform-browser": "~11.2.4",
    "@angular/platform-browser-dynamic": "~11.2.4",
    "@angular/router": "~11.2.4",
    "angular-auth-oidc-client": "^11.6.2",
    "rxjs": "~6.6.6",
    "tslib": "^2.0.0",
    "zone.js": "~0.10.2"
  },

In the angular.json file, the certificates need to be added.

angular.json certificates

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 offline_access email api://98328d53-55ec-4f14-8407-0ca5ff2f2d20/access_as_user',
            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,
            customParams: {
              prompt: 'select_account', // login, consent
            },
    });
}

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

6 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 […]

  3. Thans awesome, thanks Damien !

  4. Thanks for the information!!

    1. Thanks for the head up

  5. Vijay · · Reply

    I have followed all steps but still I am getting unauthorized while authenticating my-API which is created in first post of this series, from Angular app taken from this article GitHub repository. Please help and suggest if any steps are different then what is mentioned above. If have any training video for this may help as well.

Leave a Reply to The Morning Brew - Chris Alcock » The Morning Brew #3010 Cancel 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: