facebook

Blog

Stay updated

Authenticating an Angular client to access to a microservices architecture using OAuth and OpenID with Identity Server 4
Angular, Microservices and Authentication
Wednesday, January 01, 2020

I am working on a new project, for which I need to authenticate an Angular client to access to a set of microservices. The permission to access each microservices depends on the current user privileges, and I need to be independent of the authentication mechanism.

We can divide up the problem into two main subproblems: authentication and authorization. We use the existent standards and the right technologies to implement them. To manage user authentication, we can use OAuth and OpenId, which make us independent from the authority implementation. For the sake of simplicity, we can start with Microsoft Identity Server 4, an open-source project that implements OAuth 2.0 and OpenID Connect for ASP.NET Core.

Let’s start with the creation of the sample project structure, with a folder for the Identity Server project, a folder for the Angular client and two folders for two generic microservices:

We can create the Angular client with the usual command of the Angular CLI (ng new angular-client) and the two microservices with the usual command of the .NET CLI (dotnet new webapi). To create the Identity Server project, instead, we need to install the templates for Identity Server 4:

dotnet new -i IdentityServer4.Templates

This command installs various templates, and we can choose the is4inmem template, which creates a project that stores in memory all the configuration data. Using the in-memory storage, we can learn the basics of the framework without introducing the storage complexity (you can use the is4ef template if you prefer to store the configuration data using Entity Framework).

We need to introduce three concepts to understand how Identity Server works:

  1. Identity Resource: it is information like User ID, phone number, or email address that we can add to the user identity, and include them in the user token.
  2. API Resource: it defines the APIs that we can protect
  3. Client: it represents a client like our Angular client, and it wants to access to the resources protected by the system.

We can find the base configuration of the template in the Config.cs file. We decide to store this information in memory for simplicity’s sake, but you can store them in a database.

We must adapt the template code to our needs. As Identity Resource, we need only the OpenID resource that represents the unique ID for the user, that in the OpenID Connect specification is called subject ID:

public static IEnumerable<IdentityResource> Ids =>
    new IdentityResource[]
    {
         new IdentityResources.OpenId()
    }; 

As APIs, we need to define our microservices:

public static IEnumerable<ApiResource> Apis =>
    new ApiResource[]
    {
        new ApiResource("microservice1", "My Microservice #1"),
        new ApiResource("microservice2", "My Microservice #2")
    };

The most important passage is the definition of clients. In our case, we need only one client definition, but you can define all the clients you need. Our Angular client uses the data that we set to connect to the Identity Server and obtain the token to access the protected resources.

Each client must have a unique identifier, named ClientId, and optionally can have a ClientName. We can set the URI from which the client requires the connection, named ClientUri, and a password, called ClientSecrets, to authenticate it. We can choose if a secret is required or not, based on the type of flow, named Grant Type in the OpenID Connect specification. A Grant Type defines how a client can interact with the service that generates the token.

In our example, we use the Implicit Grant Type, which transmits the token via the browser. The Implicit flow is the simplest available, and it is the most used with JavaScript clients, but it does not permit advanced features like the token refresh. You can read about others Grant Types in the official documentation.

In our example, we choose that it does not have a ClientSecret (RequireClientSecret = false) because a JavaScript client runs on the user browser, then it is easily retrievable from the code. After the login and the logout operations, we can specify a list of URIs to redirect the user from which the client can choose. We can also enable CORS calls ( https://developer.mozilla.org/it/docs/Web/HTTP/CORS ) from the client URI and allow the send of the access token via the browser (AllowAccessTokenViaBrowser = true). To finish the configuration, we need to specify for which scopes the client requires the token. In our case the OpenID info (the subject identifier) and the API Resources defined:

public static IEnumerable<Client> Clients =>
    new Client[]
    {
         new Client
         {
             ClientId = "AngularClient",
             ClientName = "Angular Client",
             ClientUri = "http://localhost:4200",
             RequireClientSecret = false,
             RequireConsent = false,
             AllowedGrantTypes = GrantTypes.Implicit,
             RedirectUris = { "http://localhost:4200" },
             PostLogoutRedirectUris = { "http://localhost:4200/" },
             AllowedCorsOrigins = { "http://localhost:4200" },
             AllowAccessTokensViaBrowser = true,
             AccessTokenLifetime = 3600,
             AllowedScopes = { "openid", "microservice1", "microservice2" }
         } 
     };

We are now ready to define our user accounts (we use the memory as storage to simplify the code). We can use the class TestUser, provided by the framework, and configured in the TestUsers.cs file (under the Quickstart folder):

public static List<TestUser> Users = new List<TestUser>
{
     new TestUser{SubjectId = "1", Username = "michele", Password = "michele" },
     new TestUser{SubjectId = "2", Username = "antonio", Password = "antonio" }
};

It’s time to integrate the Angular Client, where we can use oidc-client ( https://github.com/IdentityModel/oidc-client-js ), a ready-to-use library that helps us connecting to the Identity authority to retrieve the user token and accessing to the protected APIs:

npm i oidc-client

To simplify and abstract all the operation with this library, we can create an Angular Service like this:

import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { UserManager, User } from 'oidc-client';
 
@Injectable({providedIn: 'root'})
export class AuthService {
 
    public user: User;
    private userManager: UserManager;
 
    constructor() {
        this.userManager = new UserManager({
            authority: environment.authority,
            client_id: environment.clientId,
            redirect_uri: environment.redirectUri,
            response_type: environment.responseType,
            scope: environment.scope
        });
   }
 
     login() {
         return this.userManager.signinRedirect();
     }
 
     async completeAuthentication() {
         this.user = await this.userManager.signinRedirectCallback();
     }
 
     isAuthenticated(): boolean {
         return this.user != null && !this.user.expired;
     }
 
    signout() {
        this.userManager.signoutRedirect();
    }
}

The library provides us two objects: the UserManager for managing the operations login and logout and the User for storing the token information. The login performs in two stages: a redirect to the login page, and a token recovery after the redirect. The login() method executes the first stage, the completeAuthentication() executes the second stage.

We can use the environments Angular CLI management to configure our development variables:

export const environment = {
    production: false,
    authority: 'http://localhost:5000',
    clientId: 'AngularClient',
    redirectUri: 'http://localhost:4200',
    responseType: 'id_token token',
    scope: 'openid microservice1 microservice2',
    microservice1Url: 'http://localhost:5002/WeatherForecast',
    microservice2Url: 'http://localhost:5003/WeatherForecast'
};

As we can see, we set the Authority with the address of the Identity Server application, choose the AngularClient as clientId to connect to the server, and require a token (responseType: ‘id_token token’) with the OpenID, microservice1, and microservice2 as scopes.

Now, we can use the AuthService with the APP_INITIALIZER provider ( https://angular.io/api/core/APP_INITIALIZER ). Usually, I create an AppService with a specific method to do all the needed works to startup an Angular client. In this case, the implementation is very simple:

import { Injectable } from '@angular/core';
import { AuthService } from './auth.service';
 
@Injectable({providedIn: 'root'})
export class AppService {
 
    constructor(
        private authService: AuthService) { }
 
    initApp(): Promise<any> {
        return new Promise(async (resolve, reject) => {
            if (!this.authService.isAuthenticated()) {
                if (window.location.href.indexOf('id_token') >= 0) {
                   await this.authService.completeAuthentication();
                   resolve();
                } else if (window.location.href.indexOf('error') >= 0) {
                   reject();
               } else {
                  return this.authService.login();
              }
         }
     });
   }
}

When the Angular client starts, I check if the user is already authenticated. If it is not, I have three possibilities: the user not started authenticated stage yet, I am returning from the login redirect with success, or I am returning from the login redirect with an error. I understand from the URL which is the current state:

  • f the URL contains a string ‘id_token’, I am returning from the login with success, and I can complete the login calling the completeAuthentication() method;
  • if the URL contains a string ‘error’ the login is completed with an error;
  • If the URL does not contain anything, I can call the login() method to redirect the user to the login page.

Now, we are ready to use the retrieved token when calling the application microservices. The best way to do it in Angular is an interceptor to add the token on all requests:

import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
 
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
 
    constructor(
        private authService: AuthService) {}
 
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any > > {
        const headers = req.headers
           .set('Content-Type', 'application/json')
           .set('Authorization', `${this.authService.user.token_type}  ${this.authService.user.access_token}`);
        const authReq = req.clone({ headers });
        return next.handle(authReq);
     }
}

We use the access token (that is a simple JSON Web Token or shortly a JWT Token) in the Authorization header. The token_type, in our case, is the usual ‘Bearer’.

Now we can configure all in the AppModule:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
 
import { AppComponent } from './app.component';
import { AppService } from './app.service';
import { AuthInterceptor } from './auth.interceptor';
 
export function initApp(appService: AppService) {
 return () => appService.initApp();
}
 
@NgModule({
 declarations: [ AppComponent ],
 imports: [ BrowserModule, HttpClientModule ],
 providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
    { provide: APP_INITIALIZER, useFactory: initApp, deps: [AppService], multi: true }
 ],
 bootstrap: [AppComponent]
})
export class AppModule { }

The AppComponent is just a list of calls for each available microservice, and a sign out button:

<h2>Angular Client </h2>
<h3>Available Microservices: </h3>
<ul>
 <li >Microservice 1: <button type="button" (click)="callMicroservice1()">Call</button > </li>
 <li >Microservice 2: <button type="button" (click)="callMicroservice2()">Call</button > </li>
</ul>
<button type="button" (click)="signout()">Signout </button>

This is the code of the AppComponent:

import { Component, OnInit } from '@angular/core';
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';
import { AuthService } from './auth.service';
 
@Component({
 selector: 'app-root',
 templateUrl: './app.component.html',
 providers: [Location, {provide: LocationStrategy, useClass: PathLocationStrategy}],
})
export class AppComponent {
    constructor(
        location: Location,
        private authService: AuthService,
        private httpClient: HttpClient) {
        location.replaceState('/');
     }
 
    callMicroservice1() {
        this.httpClient.get(environment.microservice1Url)
           .subscribe(
               data => alert('SUCCESS: ' + JSON.stringify(data)),
               error => alert('ERROR: ' + JSON.stringify(error)));
    }
 
    callMicroservice2() {
        this.httpClient.get(environment.microservice2Url)
           .subscribe(
               data => alert('SUCCESS: ' + JSON.stringify(data)),
               error => alert('ERROR: ' + JSON.stringify(error)));
    }
 
    signout() {
        this.authService.signout();
    }
}

Note the use of the location service: we receive the token information via the URL, so we need to clear these data and remove the call from the browser history. The location.replaceState() method replaces the top item on browser history stack and normalizes the given URL.

We are ready for the microservices configuration, where we need to configure the authentication process using JWT. We need to install the Microsoft.AspNetCore.Authentication.JwtBearer package and add to the ConfigureService() method of the Startup class, the following code:

services.AddAuthentication(options =>
 {
     options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
     options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
 }).AddJwtBearer(o =>
 {
     o.Authority = "http://localhost:5000";
     o.Audience = "microservice1";
     o.RequireHttpsMetadata = false;
});
 
services.AddAuthorization();

In the Configure() method, we need to add theUseAuthentication middleware to enable the authentication process:

app.UseAuthentication();

The ‘webapi’ template creates a WeatherForecast API with some sample data that we can protect using the Authorize attribute:

[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
    ...
}

To simplify the debug, we set the microservice1 to run on the 5002 port and the microservice2 to run on the 5003 port. To do this, we can change the launchSettings.json in the Properties folder of each microservice:

"microservice1": {
     "commandName": "Project",
     "launchBrowser": true,
     "launchUrl": "weatherforecast",
     "applicationUrl": "http://localhost:5002",
     "environmentVariables": {
         "ASPNETCORE_ENVIRONMENT": "Development"
     }
 }

We can run our four projects and open the browser at localhost:4200. If everything runs correctly, the client redirects us to the localhost:5000 for the login stage, and after the login, Identity Server redirects us to the localhost:4200 with the token_id in the URL.

If you want to see the request with the retrieved token, you can use the Network panel of the Chrome Developer Tools:

Very cool! But I want to add almost another feature: enable access to each microservice based on the current user. We have two users, Michele and Antonio, and now I want to forbid the access to the microservice2 to Antonio. I can do this in different ways, but I prefer the use of the user Claims because it enables advanced scenarios for the access granularity.

In the class TestUsers, we add some claims to the users:

public static List<TestUser> Users = new List<TestUser>
{
    new TestUser { SubjectId = "1", Username = "michele", Password = "michele", 
        Claims = 
        {
            new Claim("microservice1", "admin"),
            new Claim("microservice2", "admin"),
        }
    },
    new TestUser { SubjectId = "2", Username = "antonio", Password = "antonio", 
        Claims = 
        {
            new Claim("microservice1", "user"),
        }
     }
 };

As we can see, Michele has two claims, microservice1 and microservice2, whereas Antonio has only one claim, microservice1. We need these claims to be added to the access token, but Identity Server does not do this for us. We can execute custom code during the token creation implementing a custom ProfileService from the IProfileService interface. The IProfileService interface requires the implementation of two methods: the GetProfileDataAsync, called during the token creation to retrieve the user profile, and the IsActiveAsync method, that permits to personalize the criteria for which the user is active or not. The GetProfileDataAsync method provides us the ProfileDataRequestContext that exposes the IssuedClaims list for adding additional claims to the profile:

using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.Models;
using IdentityServer4.Quickstart.UI;
using IdentityServer4.Services;
 
namespace identityserver.Services
{
    public class ProfileService : IProfileService
    {
         public Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
             string sub = context.Subject.FindFirst("sub").Value;
             var user = TestUsers.Users.FirstOrDefault(x => x.SubjectId == sub);
             foreach (var claim in user.Claims)
            {
                 context.IssuedClaims.Add(claim);
            }
           return Task.CompletedTask;
       }
 
       public Task IsActiveAsync(IsActiveContext context)
       {
            context.IsActive = true;
            return Task.CompletedTask;
       }
   }
}

Now we need to add the custom profile service in the ConfigureService() of the Identity Server project:

builder.AddProfileService<ProfileService> ();

Thanks to these changes, the access token for the current user contains the added claims that we can use to discriminate the access to our microservices. We have to return to the ConfigureService() of the microservices and change the AddAuthorization, adding an Authorization Policy:

services.AddAuthorization(options = >
{
    options.AddPolicy("ApiUser", policy => policy.RequireClaim("microservice1"));
}); 

The Authorization Policies is a new mechanism introduced in the .NET Core for controlling the access to API through custom rules. In our case, we define a new policy called “ApiUser” that checks the presence of the claim ‘microservice1’ in the user profile. Now we can change the Authorize attribute on the WeatherForecast controller to apply the new policy:

[ApiController]
[Route("[controller]")]
[Authorize("ApiUser")]
public class WeatherForecastController : ControllerBase
{
    ...
} 

After doing the same operations for the microservice2, we can run the application and check if Antonio can access to the microservice2:

As you can imagine, we can implement very sophisticated scenarios using OAuth, OpenID, and the Authorization Policies of the .NET Core. Identity Server can help us to try different options and choose the correct flow for our application. All the code is available here: https://github.com/apomic80/angular-microservices-identityserver .

Happy Coding!