This article shows how to use Azure AD with an Angular application implemented using the Microsoft dotnet template and the angular-auth-oidc-client npm package to implement the OpenID Implicit Flow. The Angular app uses bootstrap 4 and Angular CLI.
Code: https://github.com/damienbod/dotnet-template-angular
Setting up Azure AD
Log into https://portal.azure.com and click the Azure Active Directory button
Click App registrations and then the New application registration
Add an application name and set the URL to match the application URL. Click the create button.
Open the new application.
Click the Manifest button.
Set the oauth2AllowImplicitFlow to true.
Click the settings button and add the API Access required permissions as needed.
Now the Azure AD is ready to go. You will need to add your users which you want to login with and add them as admins if required. For example, I have add damien@damienbod.onmicrosoft.com as an owner.
dotnet Angular template from Microsoft.
Install the latest version and create a new project.
Installation:
https://docs.microsoft.com/en-gb/aspnet/core/spa/index#installation
Docs:
https://docs.microsoft.com/en-gb/aspnet/core/spa/angular?tabs=visual-studio
The dotnet template uses Angular CLI and can be found in the ClientApp folder.
Update all the npm packages including the Angular-CLI, and do a npm install, or use yarn to update the packages.
Add the angular-auth-oidc-client which implements the OIDC Implicit Flow for Angular applications.
{ "name": "dotnet_angular", "version": "0.0.0", "license": "MIT", "scripts": { "ng": "ng", "start": "ng serve --extract-css", "build": "ng build --extract-css", "build:ssr": "npm run build -- --app=ssr --output-hashing=media", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" }, "private": true, "dependencies": { "@angular-devkit/core": "0.0.28", "@angular/animations": "^5.2.1", "@angular/common": "^5.2.1", "@angular/compiler": "^5.2.1", "@angular/core": "^5.2.1", "@angular/forms": "^5.2.1", "@angular/http": "^5.2.1", "@angular/platform-browser": "^5.2.1", "@angular/platform-browser-dynamic": "^5.2.1", "@angular/platform-server": "^5.2.1", "@angular/router": "^5.2.1", "@nguniversal/module-map-ngfactory-loader": "^5.0.0-beta.5", "angular-auth-oidc-client": "4.0.0", "aspnet-prerendering": "^3.0.1", "bootstrap": "^4.0.0", "core-js": "^2.5.3", "es6-promise": "^4.2.2", "rxjs": "^5.5.6", "zone.js": "^0.8.20" }, "devDependencies": { "@angular/cli": "1.6.5", "@angular/compiler-cli": "^5.2.1", "@angular/language-service": "^5.2.1", "@types/jasmine": "~2.8.4", "@types/jasminewd2": "~2.0.3", "@types/node": "~9.3.0", "codelyzer": "^4.1.0", "jasmine-core": "~2.9.1", "jasmine-spec-reporter": "~4.2.1", "karma": "~2.0.0", "karma-chrome-launcher": "~2.2.0", "karma-cli": "~1.0.1", "karma-coverage-istanbul-reporter": "^1.3.3", "karma-jasmine": "~1.1.1", "karma-jasmine-html-reporter": "^0.2.2", "protractor": "~5.2.2", "ts-node": "~4.1.0", "tslint": "~5.9.1", "typescript": "~2.6.2" } }
Azure AD does not support CORS, so you have to GET the .well-known/openid-configuration with your tenant and add them to your application as a Json file.
https://login.microsoftonline.com/damienbod.onmicrosoft.com/.well-known/openid-configuration
Do the same for the jwt keys
https://login.microsoftonline.com/common/discovery/keys
Now change the URL in the well-known/openid-configuration json file to use the downloaded version of the keys.
{ "authorization_endpoint": "https://login.microsoftonline.com/a0958f45-195b-4036-9259-de2f7e594db6/oauth2/authorize", "token_endpoint": "https://login.microsoftonline.com/a0958f45-195b-4036-9259-de2f7e594db6/oauth2/token", "token_endpoint_auth_methods_supported": [ "client_secret_post", "private_key_jwt", "client_secret_basic" ], "jwks_uri": "https://localhost:44347/jwks.json", "response_modes_supported": [ "query", "fragment", "form_post" ], "subject_types_supported": [ "pairwise" ], "id_token_signing_alg_values_supported": [ "RS256" ], "http_logout_supported": true, "frontchannel_logout_supported": true, "end_session_endpoint": "https://login.microsoftonline.com/a0958f45-195b-4036-9259-de2f7e594db6/oauth2/logout", "response_types_supported": [ "code", "id_token", "code id_token", "token id_token", "token" ], "scopes_supported": [ "openid" ], "issuer": "https://sts.windows.net/a0958f45-195b-4036-9259-de2f7e594db6/", "claims_supported": [ "sub", "iss", "cloud_instance_name", "cloud_instance_host_name", "cloud_graph_host_name", "msgraph_host", "aud", "exp", "iat", "auth_time", "acr", "amr", "nonce", "email", "given_name", "family_name", "nickname" ], "microsoft_multi_refresh_token": true, "check_session_iframe": "https://login.microsoftonline.com/a0958f45-195b-4036-9259-de2f7e594db6/oauth2/checksession", "userinfo_endpoint": "https://login.microsoftonline.com/a0958f45-195b-4036-9259-de2f7e594db6/openid/userinfo", "tenant_region_scope": "NA", "cloud_instance_name": "microsoftonline.com", "cloud_graph_host_name": "graph.windows.net", "msgraph_host": "graph.microsoft.com" }
This can now be used in the APP_INITIALIZER of the app.module. In the OIDC configuration, set the OpenIDImplicitFlowConfiguration object to match the Azure AD application which was configured before.
import { BrowserModule } from '@angular/platform-browser'; import { NgModule, APP_INITIALIZER } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; import { NavMenuComponent } from './nav-menu/nav-menu.component'; import { HomeComponent } from './home/home.component'; import { AuthModule, OidcSecurityService, OpenIDImplicitFlowConfiguration, OidcConfigService, AuthWellKnownEndpoints } from 'angular-auth-oidc-client'; import { AutoLoginComponent } from './auto-login/auto-login.component'; import { routing } from './app.routes'; import { ForbiddenComponent } from './forbidden/forbidden.component'; import { UnauthorizedComponent } from './unauthorized/unauthorized.component'; import { ProtectedComponent } from './protected/protected.component'; import { AuthorizationGuard } from './authorization.guard'; import { environment } from '../environments/environment'; export function loadConfig(oidcConfigService: OidcConfigService) { console.log('APP_INITIALIZER STARTING'); // https://login.microsoftonline.com/damienbod.onmicrosoft.com/.well-known/openid-configuration // jwt keys: https://login.microsoftonline.com/common/discovery/keys // Azure AD does not support CORS, so you need to download the OIDC configuration, and use these from the application. // The jwt keys needs to be configured in the well-known-openid-configuration.json return () => oidcConfigService.load_using_custom_stsServer('https://localhost:44347/well-known-openid-configuration.json'); } @NgModule({ declarations: [ AppComponent, NavMenuComponent, HomeComponent, AutoLoginComponent, ForbiddenComponent, UnauthorizedComponent, ProtectedComponent ], imports: [ BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }), HttpClientModule, AuthModule.forRoot(), FormsModule, routing, ], providers: [ OidcSecurityService, OidcConfigService, { provide: APP_INITIALIZER, useFactory: loadConfig, deps: [OidcConfigService], multi: true }, AuthorizationGuard ], bootstrap: [AppComponent] }) export class AppModule { constructor( private oidcSecurityService: OidcSecurityService, private oidcConfigService: OidcConfigService, ) { this.oidcConfigService.onConfigurationLoaded.subscribe(() => { const openIDImplicitFlowConfiguration = new OpenIDImplicitFlowConfiguration(); openIDImplicitFlowConfiguration.stsServer = 'https://login.microsoftonline.com/damienbod.onmicrosoft.com'; openIDImplicitFlowConfiguration.redirect_url = 'https://localhost:44347'; openIDImplicitFlowConfiguration.client_id = 'fd87184a-00c2-4aee-bc72-c7c1dd468e8f'; openIDImplicitFlowConfiguration.response_type = 'id_token token'; openIDImplicitFlowConfiguration.scope = 'openid profile email '; openIDImplicitFlowConfiguration.post_logout_redirect_uri = 'https://localhost:44347'; openIDImplicitFlowConfiguration.post_login_route = '/home'; openIDImplicitFlowConfiguration.forbidden_route = '/home'; openIDImplicitFlowConfiguration.unauthorized_route = '/home'; openIDImplicitFlowConfiguration.auto_userinfo = false; openIDImplicitFlowConfiguration.log_console_warning_active = true; openIDImplicitFlowConfiguration.log_console_debug_active = !environment.production; openIDImplicitFlowConfiguration.max_id_token_iat_offset_allowed_in_seconds = 600; const authWellKnownEndpoints = new AuthWellKnownEndpoints(); authWellKnownEndpoints.setWellKnownEndpoints(this.oidcConfigService.wellKnownEndpoints); this.oidcSecurityService.setupModule(openIDImplicitFlowConfiguration, authWellKnownEndpoints); this.oidcSecurityService.setCustomRequestParameters({ 'prompt': 'admin_consent', 'resource': 'https://graph.windows.net'}); }); console.log('APP STARTING'); } }
Now an Auth Guard can be added to protect the protected routes.
import { Injectable } from '@angular/core'; import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { map } from 'rxjs/operators'; import { OidcSecurityService } from 'angular-auth-oidc-client'; @Injectable() export class AuthorizationGuard implements CanActivate { constructor( private router: Router, private oidcSecurityService: OidcSecurityService ) { } public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean { console.log(route + '' + state); console.log('AuthorizationGuard, canActivate'); return this.oidcSecurityService.getIsAuthorized().pipe( map((isAuthorized: boolean) => { console.log('AuthorizationGuard, canActivate isAuthorized: ' + isAuthorized); if (isAuthorized) { return true; } this.router.navigate(['/unauthorized']); return false; }) ); } }
You can then add an app.routes and protect what you require.
import { Routes, RouterModule } from '@angular/router'; import { ForbiddenComponent } from './forbidden/forbidden.component'; import { HomeComponent } from './home/home.component'; import { UnauthorizedComponent } from './unauthorized/unauthorized.component'; import { AutoLoginComponent } from './auto-login/auto-login.component'; import { ProtectedComponent } from './protected/protected.component'; import { AuthorizationGuard } from './authorization.guard'; const appRoutes: Routes = [ { path: '', component: HomeComponent, pathMatch: 'full' }, { path: 'home', component: HomeComponent }, { path: 'autologin', component: AutoLoginComponent }, { path: 'forbidden', component: ForbiddenComponent }, { path: 'unauthorized', component: UnauthorizedComponent }, { path: 'protected', component: ProtectedComponent, canActivate: [AuthorizationGuard] } ]; export const routing = RouterModule.forRoot(appRoutes);
The NavMenuComponent component is then updated to add the login, logout.
import { Component } from '@angular/core'; import { Subscription } from 'rxjs/Subscription'; import { OidcSecurityService } from 'angular-auth-oidc-client'; @Component({ selector: 'app-nav-menu', templateUrl: './nav-menu.component.html', styleUrls: ['./nav-menu.component.css'] }) export class NavMenuComponent { isExpanded = false; isAuthorizedSubscription: Subscription; isAuthorized: boolean; constructor(public oidcSecurityService: OidcSecurityService) { } ngOnInit() { this.isAuthorizedSubscription = this.oidcSecurityService.getIsAuthorized().subscribe( (isAuthorized: boolean) => { this.isAuthorized = isAuthorized; }); } ngOnDestroy(): void { this.isAuthorizedSubscription.unsubscribe(); } login() { this.oidcSecurityService.authorize(); } refreshSession() { this.oidcSecurityService.authorize(); } logout() { this.oidcSecurityService.logoff(); } collapse() { this.isExpanded = false; } toggle() { this.isExpanded = !this.isExpanded; } }
Start the application and click login
Enter your user which is defined in Azure AD
Consent page:
And you are redircted back to the application.
Notes:
If you don’t use any Microsoft API use the id_token flow, and not the id_token token flow. The resource of the API needs to be defined in both the request and also the Azure AD app definitions.
Links:
https://docs.microsoft.com/en-gb/aspnet/core/spa/angular?tabs=visual-studio