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
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
[…] Angular SPA with an ASP.NET Core API using Azure AD Auth and user access tokens (Damien Bowden) […]
[…] Angular SPA with an ASP.NET Core API using Azure AD Auth and user access tokens – Damien Bowden […]
Thans awesome, thanks Damien !
Thanks for the information!!
Thanks for the head up
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.
Getting the same issue on my end. Did you ever figure this out? Would greatly appreciate your help if so
I tried this and don’t see what is missing from the docs. Does the scope and the API use the correct GUID as defined in your tenant?
Greetings Damien
Hi, thank you for your reply. I redid both guides again and this time did not get the unauthorized issue. I think I was originally missing something in my angular app as the access token was not getting me a valid JWT and this time it was. Please disregard my previous comment and thanks again