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

2019-09-23 Updated to ASP.NET Core 3.0, OIDC 10.0.8
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_google_oidc",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "build:ssr": "ng run dotnet_angular_google_oidc:server:dev",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "8.0.0",
    "@angular/common": "8.0.0",
    "@angular/compiler": "8.0.0",
    "@angular/core": "8.0.0",
    "@angular/forms": "8.0.0",
    "@angular/platform-browser": "8.0.0",
    "@angular/platform-browser-dynamic": "8.0.0",
    "@angular/platform-server": "8.0.0",
    "@angular/router": "8.0.0",
    "angular-auth-oidc-client": "10.0.8",
    "@nguniversal/module-map-ngfactory-loader": "8.0.0-rc.1",
    "aspnet-prerendering": "^3.0.1",
    "bootstrap": "^4.3.1",
    "core-js": "^2.6.5",
    "jquery": "3.4.1",
    "popper.js": "^1.14.3",
    "rxjs": "^6.4.0",
    "zone.js": "~0.9.1"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^0.800.6",
    "@angular/cli": "8.0.6",
    "@angular/compiler-cli": "8.0.0",
    "@angular/language-service": "8.0.0",
    "@types/jasmine": "~3.3.9",
    "@types/jasminewd2": "~2.0.6",
    "@types/node": "~11.10.5",
    "codelyzer": "^5.0.1",
    "jasmine-core": "~3.3.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "^4.0.0",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.5",
    "karma-jasmine": "~2.0.1",
    "karma-jasmine-html-reporter": "^1.4.0",
    "typescript": "3.4.5"
  },
  "optionalDependencies": {
    "node-sass": "^4.9.3",
    "protractor": "~5.4.0",
    "ts-node": "~5.0.1",
    "tslint": "~5.9.1"
  }
}

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,
    ConfigResult,
    OidcConfigService,
    OpenIdConfiguration
} 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((configResult: ConfigResult) => {

      const config: OpenIdConfiguration = {
        stsServer: 'https://login.microsoftonline.com/ed48a4e2-9762-4f41-8a15-9bcd99965fab/v2.0/',
        redirect_url: configResult.customConfig.redirect_url,
        client_id: configResult.customConfig.client_id,
        response_type: configResult.customConfig.response_type,
        scope: configResult.customConfig.scope,
        post_logout_redirect_uri: configResult.customConfig.post_logout_redirect_uri,
        start_checksession: configResult.customConfig.start_checksession,
        silent_renew: configResult.customConfig.silent_renew,
        silent_renew_url: 'https://localhost:44311/silent-renew.html',
        post_login_route: '/home',
        forbidden_route: configResult.customConfig.forbidden_route,
        unauthorized_route: configResult.customConfig.unauthorized_route,
        log_console_warning_active: configResult.customConfig.log_console_warning_active,
        log_console_debug_active: configResult.customConfig.log_console_debug_active,
        max_id_token_iat_offset_allowed_in_seconds: configResult.customConfig.max_id_token_iat_offset_allowed_in_seconds,
        auto_userinfo: false,
        history_cleanup_off: true,
        iss_validation_off: true
        // disable_iat_offset_validation: true
      };

      this.oidcSecurityService.setupModule(config, configResult.authWellknownEndpoints);

      this.oidcSecurityService.setCustomRequestParameters(configResult.customConfig.additional_login_parameters);
      this.oidcSecurityService.setCustomRequestParameters({ response_mode: 'fragment' } );
    });

    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

18 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.

  8. Denis Tomada · · Reply

    Hi Damien, thanks for your work. I’m using your example, I deployed on Azure App Service and I actived Easy Auth in “Express” mode. All it works fine but if I try to enable “Login as Azure AD” on anonymous access, I wiil receive “Invalid reply urls for application XXX” as error. However, if I disable login for anonymous access and enable auto-login into guard, it works perfectly. Do you have any suggestions? Thanks.

  9. Victor · · Reply

    Crazy dumb question as I am not able to pull and run the code of github. Very new at Azure AD and openid-connect.

    In your github configuration is this example functional because you are running in IISExpress sending requests from your webapi uri to your client uri so code such as on the client works by using ${window.location.origin} which was your webapi uri. If you run using dotnet cli or just not using IISExpress this repo would not repo? I don’t understand if your webapi uri or your client uri is your reply uri in the Azure AD configuration? Again sorry that I don’t know exactly how this whole thing works.

  10. Hi Damien, would you please explain what’s the difference between your angular-auth-oidc-client and oidc-client (IdentityServer authors) packages? What advantages does angular-auth-oidc-client give?

    1. Hi Alex

      The 2 libs implement the same specification, one in angular, the other in Javascript. Manfred Steyer has also implemented an OIDC Implicit Flow in angular which is also certified. Any 3 of these can be used, it’s just a preference. The only difference is the different public interfaces in the npm packages, 1 exposes angular functions, the other javascript.

      Greetings Damien

  11. jawahar75 · · Reply

    Hi Damien, This one works but with below change.

    In “\dotnet-template-angular\dotnet-angular-azure-ad-oidc\Controllers\ConfigController.cs”, I have added the below line of code.

    config.silent_renew = true;

    Also, how could I renew the access token silently please.

    I tried, below changes but no luck.

    [1]
    In configController.cs, I have added the below line.

    config.silent_renew_url = $”{protocol}{Request.Host.ToUriComponent()}/silent-renew.html”;

    [2]
    I’ve added ‘silent-renew.html’ file (under src folder) with ‘Implicit Flow’ code specified in API documentation page (https://github.com/damienbod/angular-auth-oidc-client/blob/master/API_DOCUMENTATION.md).

    Am I missing anything here please?

    Thanks
    Jawahar

  12. […] this was fixed by making change in AD side (done be seperate team, dont have much details) Also modified code with help of below link https://damienbod.com/2018/01/23/using-the-dotnet-angular-template-with-azure-ad-oidc-implict-flow/ […]

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.