Using the dotnet Angular template with Azure AD OIDC Implicit Flow

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

History

2018-07-13 Removed static calls to the well known endpoints, and the jwt keys API

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",
    "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/build-angular": "^0.6.5",
    "@angular-devkit/core": "^0.6.5",
    "@angular/animations": "6.0.3",
    "@angular/common": "6.0.3",
    "@angular/compiler": "6.0.3",
    "@angular/core": "6.0.3",
    "@angular/forms": "6.0.3",
    "@angular/http": "6.0.3",
    "@angular/platform-browser": "6.0.3",
    "@angular/platform-browser-dynamic": "6.0.3",
    "@angular/platform-server": "6.0.3",
    "@angular/router": "6.0.3",
    "@nguniversal/module-map-ngfactory-loader": "^6.0.0",
    "angular-auth-oidc-client": "6.0.0",
    "aspnet-prerendering": "^3.0.1",
    "bootstrap": "^4.1.1",
    "core-js": "^2.5.7",
    "es6-promise": "^4.2.4",
    "rxjs": "^6.2.0",
    "zone.js": "^0.8.26"
  },
  "devDependencies": {
    "@angular/cli": "^6.0.5",
    "@angular/compiler-cli": "6.0.3",
    "@angular/language-service": "6.0.3",
    "@types/jasmine": "~2.8.7",
    "@types/jasminewd2": "~2.0.3",
    "@types/node": "~10.1.3",
    "codelyzer": "^4.3.0",
    "jasmine-core": "~3.1.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~2.0.2",
    "karma-chrome-launcher": "~2.2.0",
    "karma-cli": "~1.0.1",
    "typescript": "2.7.2",
    "karma-coverage-istanbul-reporter": "^2.0.1",
    "karma-jasmine": "~1.1.2",
    "karma-jasmine-html-reporter": "^1.1.0",
    "protractor": "~5.3.2",
    "ts-node": "~6.0.5",
    "tslint": "~5.10.0"
  }
}

Azure AD does not support CORS, so you have to GET the .well-known/openid-configuration with your tenant using server code, and not browser run scripts.

Michael S. Hansen added code so that the OIDC .well-known/openid-configuration json and the jwt keys can be used by calling the backend server APIs of the application which then get the data from the Azure AD endpoints.

https://blogs.msdn.microsoft.com/mihansen/2018/07/12/net-core-angular-app-with-openid-connection-implicit-flow-authentication-angular-auth-oidc-client/

.well-known/openid-configuration
https://login.microsoftonline.com/damienbod.onmicrosoft.com/.well-known/openid-configuration

jwt keys
https://login.microsoftonline.com/common/discovery/keys

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(`${window.location.origin}/api/config/configuration`);
  //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 authWellKnownEndpoints = new AuthWellKnownEndpoints();
      authWellKnownEndpoints.setWellKnownEndpoints(this.oidcConfigService.wellKnownEndpoints);

      const openIDImplicitFlowConfiguration = new OpenIDImplicitFlowConfiguration();
      openIDImplicitFlowConfiguration.stsServer = this.oidcConfigService.wellKnownEndpoints.issuer;
      openIDImplicitFlowConfiguration.redirect_url = this.oidcConfigService.clientConfiguration.redirect_url;
      // The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer
      // identified by the iss (issuer) Claim as an audience.
      // The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience,
      // or if it contains additional audiences not trusted by the Client.
      openIDImplicitFlowConfiguration.client_id = this.oidcConfigService.clientConfiguration.client_id;
      openIDImplicitFlowConfiguration.response_type = this.oidcConfigService.clientConfiguration.response_type;
      openIDImplicitFlowConfiguration.scope = this.oidcConfigService.clientConfiguration.scope;
      openIDImplicitFlowConfiguration.post_logout_redirect_uri = this.oidcConfigService.clientConfiguration.post_logout_redirect_uri;
      openIDImplicitFlowConfiguration.start_checksession = this.oidcConfigService.clientConfiguration.start_checksession;
      openIDImplicitFlowConfiguration.silent_renew = this.oidcConfigService.clientConfiguration.silent_renew;
      openIDImplicitFlowConfiguration.silent_renew_url = this.oidcConfigService.clientConfiguration.silent_renew_url;
      openIDImplicitFlowConfiguration.post_login_route = this.oidcConfigService.clientConfiguration.startup_route;
      // HTTP 403
      openIDImplicitFlowConfiguration.forbidden_route = this.oidcConfigService.clientConfiguration.forbidden_route;
      // HTTP 401
      openIDImplicitFlowConfiguration.unauthorized_route = this.oidcConfigService.clientConfiguration.unauthorized_route;
      openIDImplicitFlowConfiguration.auto_userinfo = this.oidcConfigService.clientConfiguration.auto_userinfo;
      openIDImplicitFlowConfiguration.log_console_warning_active = this.oidcConfigService.clientConfiguration.log_console_warning_active;
      openIDImplicitFlowConfiguration.log_console_debug_active = this.oidcConfigService.clientConfiguration.log_console_debug_active;
      // id_token C8: The iat Claim can be used to reject tokens that were issued too far away from the current time,
      // limiting the amount of time that nonces need to be stored to prevent attacks.The acceptable range is Client specific.
      openIDImplicitFlowConfiguration.max_id_token_iat_offset_allowed_in_seconds =
        this.oidcConfigService.clientConfiguration.max_id_token_iat_offset_allowed_in_seconds;

      this.oidcSecurityService.setupModule(openIDImplicitFlowConfiguration, authWellKnownEndpoints);

      this.oidcSecurityService.setCustomRequestParameters( this.oidcConfigService.clientConfiguration.additional_login_parameters );
    });

    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

https://blogs.msdn.microsoft.com/mihansen/2018/07/12/net-core-angular-app-with-openid-connection-implicit-flow-authentication-angular-auth-oidc-client/

https://portal.azure.com

Advertisements

12 comments

  1. […] Using the dotnet Angular template with Azure AD OIDC Implicit Flow – Damien Bowden […]

  2. EricC · · Reply

    Hi Damien, thank you for this. Can you also include the code to protect the API in Startup.cs?

  3. AdamB · · Reply

    Hi Damien, thanks for all the hard work. Azure AD does not allow requests for a token with more than one audience. If I have 2 APIs I need to access from my UI, I have to request a different access token for each one. MSAL seems to guide one down the route of requesting an access token in the service that is calling the API, therefore solving that problem. Is there a simple way to do this with angular-auth-oidc-client, or is this the wrong approach?

    1. Hi Adam

      thanks. Could you not use the same token for both APIs? The access token should have 2 scope cliams ScopeA for API A and ScopeB for API B. You could then validate the scopes in each API.

      Greetings Damien

      1. abezverkov · ·

        So each API is defined as an application in AAD which means each one gets its own clientId/audience and its own set of scopes. AAD does not allow you to request scopes from multiple client (throws a very specific error). So I can request scope xxx.onmicrosoft.com/api1/readvalues and get an access token for audience1, or I can request scope xxx.onmicrosoft.com/api2/readvalues and get an access token for audience2, but I cannot request both at the same time. I dont like the idea of having to define one audience (application in AAD) for all of our api’s, as that would add some interlinking/dependency I dont want.

        PS – apologies for the double post, that was a wordpress login snafu. It wont let me remove it.

  4. abezverkov · · Reply

    Hi Damien, thanks for all the hard work. Azure AD does not allow requests for a token with more than one audience. If I have 2 APIs I need to access from my UI, I have to request a different access token for each one. MSAL seems to guide one down the route of requesting an access token in the service that is calling the API, therefore solving that problem. Is there a simple way to do this with angular-auth-oidc-client, or is this the wrong approach?

    1. Hi abezverkov

      thanks.

      I think you should use Azure AD B2C and use this to connect to Azure AD or any other system. The reason is that you can control the claims in the tokens better, and the main reason, Azure AD does not support CORS, so when the jwts keys are updated on the server, your app will stop working until you update your configuration. I would define a separate scope claim for each API and return both in the token if the identity is allowed to to use the API. 1 access token for both APIs

      Greetings Damien

  5. Bharani Chakravarthy · · Reply

    Currently i am using this template from github. i see visual studio getting hung all the time while adding files or saving. is this an existing issue ?

  6. i am using this template from github. I am facing VS 2017 hanging all the time when i add files or folders and even saving files. Is this an existing issue.

    1. not that I know of

      I’ll have a look.

      Greetings Damien

  7. Hi Damien, thanks for your work on this. Really nice examples. I was playing with your example and “re-implementing” to make sure I understood the steps. I made some changes to let the backend pull the openid-configuration instead of storing a static file: https://github.com/hansenms/dotnet-angular-oidc. I put up an issue on the GitHub repo. Let me know if that is something you would consider merging in and I could make a PR.

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 )

w

Connecting to %s

%d bloggers like this: