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
- Login and use an ASP.NET Core API with Azure AD Auth and user access tokens
- Angular SPA with an ASP.NET Core API using Azure AD Auth and user access tokens
- Restricting access to an Azure AD protected API using Azure AD Groups
- Using Azure CLI to create Azure App Registrations
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | { "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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <! 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | export function configureAuth(oidcConfigService: OidcConfigService) { return () => oidcConfigService.withConfig({ 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http' ; import { Injectable } from '@angular/core' ; import { AuthService } from './auth.service' ; @Injectable() export class AuthInterceptor implements HttpInterceptor { 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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 .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