Introduction
I am currently writing a demo application for the Azure Management Library and wanted to make it secure (after all the security file could grant the same rights as the Administrator). I also wanted to make it a Single Page Application (SPA) since 1) I have been doing a lot of work with Angular2 lately and 2) I really think it is the wave of the future (no offense to MVC) as can be shown by the number of packages like Angular that are out there (with more showing up every day).
So I did my research (AKA used a search engine) to figure out the best way to do this but I could not find an example of anyone doing this before. I guarantee that someone has, but I guess they didn’t write about it. There are lot of posts talking about accessing .Net Core APIs using its own authentication engine from Angular2 and others, including this excellent one called Using ADAL with Angular2 by Vishal Saroopchand which talks about using the adal-angular package that I relied on a lot (AKA copied the code), discussing authenticating via Azure AD but it stops there. However since the application will be accessing Azure directly it makes sense to authenticate using Azure AD.
I started using the Angular2 quick start seed project that can be downloaded here from GitHub. It provides a very good starting point and I have used it quite a bit. You could also use the Command Line Interface (CLI), found here, which provides a slightly different project file but it should still work. Either one will provide a good starting point. Please note that I will assume you are already familiar with using Angular2 and .Net Core so I will not be going into step by step details on what to do.
Initial Setup
Next there are a few packages that need to be installed. You will need one to perform the Azure AD authentication along with a supporting package, and one to make the secure calls. Granted, you can write the code to do both yourself if so inclined, and there are probably others that will work as well, but I will be using these packages for this example.
npm install package adal-angular --save npm install expose-loader --save npm install @types/adal --save-dev npm install package angular2-jwt --save
The first package handles the Azure AD authentication (ADAL stands for Active Directory Authentication Library), the second package is used to expose adal-angular globally (see below), the third installs the needed types, and the last package will make the authenticated calls using JWT (JavaScript Web Tokens) which is a way to pass the credentials of the user in a secure fashion.
If you want to use the code that Vishal already wrote for his blog post, you will just need to add the last package, add the settings to the config.service.ts file as shown below, modify the oauth-callback-guard.ts file as noted below (add the line to save the token), and head to the “Authentication” section
Register your application with Azure AD
Follow the instructions in this site to register your application with Azure AD.
https://docs.microsoft.com/en-us/azure/active-directory/active-directory-app-registration
Once you have created the application Azure copy the “Application ID” for use below.
Authentication Services
Following Vishal’s post, create a “services” folder to store all the authentication related services.
Create a new file called config.service.ts to store the authentication login information. If you do not already know it, you can get the tenant ID from the “domains” link in Azure AD. It will most likely be something like <name?.onmicrosoft.com.
import { Injectable } from '@angular/core'; @Injectable() export class ConfigService { constructor() { } public get getAdalConfig(): any { return { tenant: '<your tenant ID>', clientId: '<application ID from previous step>', redirectUri: window.location.origin + '/', postLogoutRedirectUri: window.location.origin + '/' }; } }
Next create adal.service.ts which we will use to wrap the calls to the adal-angular commands.
import { ConfigService } from './config.service'; import { Injectable } from '@angular/core'; import 'expose-loader?AuthenticationContext!../../../node_modules/adal-angular/lib/adal.js'; let createAuthContextFn: adal.AuthenticationContextStatic = AuthenticationContext; @Injectable() export class AdalService { private context: adal.AuthenticationContext; constructor(private configService: ConfigService) { this.context = new createAuthContextFn(configService.getAdalConfig); } login() { this.context.login(); } logout() { this.context.logOut(); } handleCallback() { this.context.handleWindowCallback(); } public get userInfo() { return this.context.getCachedUser(); } public get accessToken() { return this.context.getCachedToken(this.configService.getAdalConfig.clientId); } public get isAuthenticated() { return this.userInfo && this.accessToken; } }
Note the use of
import 'expose-loader?AuthenticationContext!../../../node_modules/adal-angular/lib/adal.js'
This is used so that we can access the commands in adal-angular as ADAL doesn’t work with the CommonJS pattern.
Next we need to create the Angular2 guard to determine if the user has logged in or not. Create authenticated.guard.ts
import { Observable } from 'rxjs/Observable'; import { Injectable } from '@angular/core'; import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, NavigationExtras } from '@angular/router'; import { AdalService } from './../services/adal.service'; @Injectable() export class AuthenticationGuard implements CanActivate { constructor(private router: Router, private adalService: AdalService) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { let navigationExtras: NavigationExtras = { queryParams: { 'redirectUrl': route.url } }; if (!this.adalService.userInfo) { this.router.navigate(['login'], navigationExtras); } return true; } }
There is one caveat with using Azure AD authentication. It passes back the information in a URL which uses the older hash style rather than the new HTML 5 method. Because of this, there needs to be a route created that will handle the parsing of the token and saving the information and you need to make sure your application uses the older hash style as well.
I followed Vishal’s lead and created a “login-callback” folder and added oath-callback.component.ts to handle the redirection when the user is logged in
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { AdalService } from './../services/adal.service'; @Component({ template: '</p> <div>Please wait...</div> <p>' }) export class OAuthCallbackComponent implements OnInit { constructor(private router: Router, private adalService: AdalService) { } ngOnInit() { if (!this.adalService.userInfo) { this.router.navigate(['login']); } else { this.router.navigate(['home']); } } }
Then create a separate guard that will be used just for this route called oauth-callback-guard.ts.
import { Routes } from '@angular/router'; import { Injectable } from '@angular/core'; import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { AdalService } from './../services/adal.service'; @Injectable() export class OAuthCallbackHandler implements CanActivate { constructor(private router: Router, private adalService: AdalService) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { this.adalService.handleCallback(); if (this.adalService.userInfo) { localStorage.setItem("token", this.adalService.accessToken); var returnUrl = route.queryParams['returnUrl']; if (!returnUrl) { this.router.navigate(['home']); } else { this.router.navigate([returnUrl], { queryParams: route.queryParams }); } } else { this.router.navigate(['login']); } return false; } }
Notice that in line 17 we store the accessToken returned for future use. This will be used to create the authenticated calls to the .Net Core API.
Finally add a module to wrap everything together. Call it oauth-callback.module.ts
import { NgModule } from '@angular/core'; import { OAuthCallbackComponent } from './oauth-callback.component'; import { OAuthCallbackHandler } from './oauth-callback.guard'; @NgModule({ imports: [], declarations: [ OAuthCallbackComponent], providers: [OAuthCallbackHandler] }) export class OAuthHandshakeModule { }
That takes care of the basics. Now you can use the AuthenticationGuard to make sure the user is logged in before accessing secured areas. Here is an example of it in use
{ path: '', redirectTo: 'login', pathMatch: 'full' }, { path: 'home', component: HomeComponent, canActivate: [AuthenticationGuard] }, { path: 'about', component: AboutComponent, canActivate: [AuthenticationGuard] }, { path: 'login', component: LoginComponent }, { path: 'id_token', component: OAuthCallbackComponent, canActivate: [OAuthCallbackHandler] }, { path: 'contact', component: ContactComponent, canActivate: [AuthenticationGuard] }
Of course you still need your login page but you can make it look like anything you want. You just need to make sure you call the login command when needed. Here is my login.component.ts
import { Router } from '@angular/router'; import { AdalService } from './../services/adal.service'; import { Component, OnInit } from '@angular/core'; @Component({ templateUrl: './login.component.html' }) export class LoginComponent implements OnInit { constructor(private router: Router, private adalService: AdalService) { } ngOnInit() { console.log(this.adalService.userInfo); } login() { this.adalService.login(); } logout() { this.adalService.logout(); } public get isLoggedIn() { return this.adalService.isAuthenticated; } }
and my login.component.html
</p> <h3>Login</h3> <p> </p> <div *ngIf="!isLoggedIn"> <button type="button" class="btn btn-primary" (click)="login()">Login via Azure AD</button> </div> <p> </p> <div *ngIf="isLoggedIn"> <button type="button" class="btn btn-danger"(click)="logout()">Logout</button> </div> <p>
Authorization
That takes care of logging in via Azure AD. Now we need to use the token that we saved to make authenticated calls. That is where the angular2-jwt package comes into play. If you go to the package’s GitHub repository located here and scroll down, you will see that the README.md gives a lot of information on how to use the application. I used the “Advanced Configuration” section for the information that I needed.
Per the instructions, I created a auth.module.ts file
import { NgModule } from '@angular/core'; import { Http, RequestOptions } from '@angular/http'; import { AuthHttp, AuthConfig } from 'angular2-jwt'; import { AdalService } from './services/adal.service'; export function authHttpServiceFactory(http: Http, options: RequestOptions) { return new AuthHttp(new AuthConfig({ tokenName: 'token', tokenGetter: (() => localStorage.getItem("token")), globalHeaders: [{'Content-Type':'application/json'}], }), http, options); } @NgModule({ providers: [ { provide: AuthHttp, useFactory: authHttpServiceFactory, deps: [Http, RequestOptions] } ] }) export class AuthModule {}
Notice in line 9 I set the “tokenGetter” variable to the token that I stored previously.
Make sure to include this module in your main module.
Now it is simple to make your API calls using authenticated HTTP calls. The following code shows an example of how I have done it
import { Injectable } from '@angular/core'; import { Headers, Http, Response, RequestOptions } from '@angular/http'; import { Observable } from 'rxjs'; import 'rxjs/Rx'; import {AuthHttp} from 'angular2-jwt'; import { ResourceGroups } from './resourcegroups'; @Injectable() export class AboutService{ headers: Headers; options: RequestOptions; errormsg: string; constructor(private http: Http, private authhttp: AuthHttp) { this.headers = new Headers({ 'Content-Type': 'application/json' }); this.options = new RequestOptions({ headers: this.headers }); } getResourceGroups() { return this.authhttp.get("http://localhost:64564/api/resourcegroups") .map(response => <ResourceGroups[]>response.json()) .catch(this.handleError); private handleError(error: any): Promise<any> { console.error('An Error occurred', error); // for demo purposes only return Promise.reject(error.message || error); } }
Note the use of angular2-jwt’s “authhttp” rather than the typical “http” call in line 24.
That takes care of the Angular2, now onto .Net Core.
.Net Core
This is much easier than the Angular2 side. All you need to do is to add the correct information in your appsettings.json, modify your Startup.cs file to accept the JWT token, and add the Authorize attribute to your API call,
Let’s start with the appsettings.json. Just like you did with the config.service.ts file above you need to add the proper settings. Modify your file to add the “AzureAd” settings shown below
{ "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Warning" } }, "AzureAd": { "AadInstance": "https://login.microsoftonline.com/{0}", "Tenant": "<your tenant ID>", "Audience": "<application ID from above>" } }
Make sure to use the same settings that you used in the config.service.ts file. Some examples I have seen want the GUID of the for the tenant ID but that never seemed to work for me.
Now that you have the correct settings, modify the startup.cs file to use them. Edit the “Configure” method to add the following line to read and use the settings from above.
// Configure the app to use Jwt Bearer Authentication app.UseJwtBearerAuthentication(new JwtBearerOptions { AutomaticAuthenticate = true, AutomaticChallenge = true, Authority = String.Format(Configuration["AzureAd:AadInstance"], Configuration["AzureAD:Tenant"]), Audience = Configuration["AzureAd:Audience"], });
And, since you are calling this from JavaScript, you need to add Cross-Origin Resource Sharing (CORS) settings. This states that you are trusting the JavaScript that is making the calls. You can set this up in the “Configure” method as well and I highly recommend reading the Enabling Cross-Origin Requests (CORS) site to determine which method will work the best for you and for more examples.
I added the following line to the “Configure” method just under the line above. NOTE: THIS IS NOT A SECURE WAY OF DOING THIS AS IT WILL ALLOW ANY SITE TO CALL THESE APIs AND IS ONLY DONE FOR DEMONSTRATION PURPOSES. In real life you would list only those trusted orgins.
app.UseCors(builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod().AllowCredentials());
Finally, add the [Authorize] attribute to either the entire API, as I have done in the example below, or to individual API calls.
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Management.Fluent; using System.Threading.Tasks; namespace AMLDemoAPI.Controllers { [Route("api/[controller]")] [Authorize] public class ResourceGroupsController : Controller { private readonly IAzure _azure = Azure.Authenticate("c:\\code\\auth.txt").WithDefaultSubscription(); // GET api/values [HttpGet] public async Task<IActionResult> Get() { var result = await _azure.ResourceGroups.ListAsync(); return new JsonResult(result); } // GET api/values/5 [HttpGet("{id}")] public async Task<IActionResult> Get(string id) { return new JsonResult(await _azure.ResourceGroups.GetByNameAsync(id)); } // POST api/values [HttpPost] public void Post([FromBody]string value) { } // PUT api/values/5 [HttpPut("{id}")] public void Put(int id, [FromBody]string value) { } // DELETE api/values/5 [HttpDelete("{id}")] public async void Delete(string id) { await _azure.ResourceGroups.DeleteByNameAsync((id)); } } }
That is all there is to it. Hope it helps. The code for this example can be found on my GitHub repository
Angular2: https://github.com/garybushey/AuthenticationDemo
.Net Core: https://github.com/garybushey/AMLDemoAPI