In this article, we are going to continue our IdentityServer4 series by learning how to implement Angular OAuth2 OIDC security with IdentityServer4 using the Authorization Code flow.

This article is heavily dependent on the previous articles from the series, so if you are not familiar with the IdentityServer4 concept or OAuth2 and OpenID Connect concepts, we strongly suggest reading all of our previous articles related to the IdentityServer4 series.

To download the source code for this article, you can visit the Angular OAuth2 OIDC with the IdentityServer4 repository. There, you will find the start folder for the starting projects and the end folder for the finished projects.

We have divided this article into the following sections:

So, let’s get going.

About Authorization Code Flow

Up until recently, the recommendation for securing Angular application (or any other js application) was using the Implicit flow. But with the newest standards, that’s not the case anymore. Now, it is a recommendation using the Authorization Code flow with PKCE (Proof Key for Code Exchange).

In one of the previous articles, we have been talking about protecting the MVC client using the Hybrid Flow and there we have explained how that flow works. The Authorization Code flow is similar, but it has some differences. The ResponseType property is one difference. With the Hybrid Flow, we have three combinations for the Response type: code token, code id_token, and code id_token token. But for the Authorization Code flow, we have just one response type, which is code.

We can inspect the URI to see this for our selves:

Authorization flow URI

As we can see, next to all other parameters the response type code is sent to the IDP server.

Flow explanation without PKCE (Proof Key for Code Exchange)

The flow starts with the user clicking the login button or accessing the protected page. Then, the client app sends the request to the /authorization endpoint with the parameters from the picture above (and additional ones, but the url was shortened for the better readability). The IDP replies with the code via the front-end channel. The client then calls the /token endpoint and provides the code. After the IDP verifies the code, it replies with the access token and the id token. The client application then uses that access token to access the protected API resources.

If we compare this to the Hybrid Flow, we are going to find a lot of similarities there. But, there is one addition to the Authorization Code flow, and that’s the usage of the PKCE.

Flow explanation with PKCE

Let’s take a look at the diagram for an easier understanding of the flow with PKCE:

Authorization flow with PKCE

As you can see, when a client sends a request to the /authorization endpoint, it adds the hashed code_challenge in addition to all the parameters in the URI. This code is stored at the IDP level. Later on, the client sends the code_verifier, next to the client’s credentials and code. IDP hashes the code_verifier and compares it to the stored code_challenge. If it matches, IDP replies with the id token and access token.

We can see the code_challenge in the complete URI as well:

Authorization flow URI with PKCE

As you can see, a lot is going on here and we are going to cover all of that with our articles and examples.

IdentityServer4 and Angular OAuth2 OIDC Configuration

It’s time to start with the coding part. The first step towards the Angular OAuth2 OIDC security is to create a client configuration in the IDP project. To do that, we have to add a new client configuration in the InMemoryConfig class:

new Client
{
    ClientName = "Angular-Client",
    ClientId = "angular-client",
    AllowedGrantTypes = GrantTypes.Code,
    RedirectUris = new List<string>{ "http://localhost:4200/signin-callback", "http://localhost:4200/assets/silent-callback.html" },
    RequirePkce = true,
    AllowAccessTokensViaBrowser = true,
    AllowedScopes =
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        "companyApi"
    },
    AllowedCorsOrigins = { "http://localhost:4200" },
    RequireClientSecret = false,
    PostLogoutRedirectUris = new List<string> { "http://localhost:4200/signout-callback" },
    RequireConsent = false,
    AccessTokenLifetime = 600
}

This configuration is similar to a previous MVC client configuration but here, we use the Code as our grant type that stands for the Authorization Code flow. For the RedirectUris property, we add two values and both will be applied in the client code. We require PKCE and allow access token transmission via the browser. The scopes are the same as for the previous client application. Then, we set the URI for the allowed CORS origin, set the URI for the post logout redirect, disable the consent screen, and set the access token lifetime to 10 minutes (600 seconds).

But for our IDP application, this is not enough.

Transferring New Configuration to the Database

If you remember the previous article, we have transferred the entire configuration to the database and our IDP server doesn’t use this in-memory configuration anymore. So, the easiest thing to do is to remove the entire database and just start the IDP application. Since we have a code to migrate all the configuration to the database (the MigrationManager class), as soon as we start the IDP server, the complete configuration will be transferred to the database for sure:

db configuration for IDP

That’s all it takes. The IDP configuration is in place and we can move on to the Angular OAuth2 OIDC security part.

Using OIDC-Client to Implement Angular OAuth2 OIDC Security

In the Start folder of our source code, next to the Web API and OAuth project, we can find the AngularClient project. Let’s open that project and inspect it a bit:

angular oauth2 oidc project overview

We have a basic project with a Home component, NavigationMenu component, Company module with the Companies component to fetch all the companies from the API, and the Error pages components.

Of course, don’t forget to open the terminal window (CTRL + ~) and run the npm install command.

If you followed along with the entire series, then you already have the database created for the Web API application. If not, all you have to do is to start the Web API application, modify the connection string in the appsettings.json file, and run the Update-Database command in the Package Manager Console window.

You can start the Web API application and the AngularClient application to inspect how the app works.

Installing the Required Library

Let’s navigate back to the client application, and install the oidc-client library:

npm install oidc-client --save

After the installation, we are going to create a file named constants in the shared folder and add a couple of properties inside:
export class Constants {
    public static apiRoot = "https://localhost:5001/api";
    public static clientRoot = "http://localhost:4200";
    public static idpAuthority = "https://localhost:5005"
    public static clientId = "angular-client";
}

Here, we create four properties and group them in a single class. We are going to use this file for additional properties as well. For now, we have the root address of our API project, the client application root, the URI of the IDP project, and finally the clientId property. The value of the clientId property must be the same as the ClientId property in the IDP configuration.

Creating Authentication Service

To create an authentication service file, we are going to execute another command:

ng g service shared/services/auth --skipTests

Let’s add the import statements required for our authentication logic:
import { Injectable } from '@angular/core';
import { UserManager, User, UserManagerSettings } from 'oidc-client';
import { Constants } from '../constants';

The UserManager class contains different low level abstractions for the oidc-client that save us from doing a lot of manual work. The User class contains all the properties related to the authenticated user (tokens, claims, expiration…). Finally, the UserManagerSettings class is a class meant for the OIDC configuration similar to the one we have on the IDP level.

Now, we can add our configuration:

export class AuthService {
  private _userManager: UserManager;
  private _user: User;

  private get idpSettings() : UserManagerSettings {
    return {
      authority: Constants.idpAuthority,
      client_id: Constants.clientId,
      redirect_uri: `${Constants.clientRoot}/signin-callback`,
      scope: "openid profile companyApi",
      response_type: "code",
      post_logout_redirect_uri: `${Constants.clientRoot}/signout-callback`
    }
  }

  constructor() { 
    this._userManager = new UserManager(this.idpSettings);
  }
}

The idpSettings property returns the object of the UserManagerSettings type containing all the required oidc configuration. Here, we can see all the properties that we have in the IDP configuration, except the authority, and yes these must match. Also, we have seen a similar configuration in our MVC client application.

Let’s explain these properties:

  • authority => the URI of the IDP project
  • client_id => the id of the client that consumes the IDP server
  • redirect_uri => the URI to redirect to after successful authentication
  • scope => the list of supported scopes by IDP
  • response_type => determines the flow we want to use (AllowedGrantTypes property on IDP)
  • post_logout_redirect_uri: the URI to redirect to after successful logout

Then, in the constructor, we instantiate the _userManager property passing the idpSettings object with all the required configuration.

Using the UserManager Class to Enable IDP and Angular Integration

We have explained all the steps that happen during the Authorization Code flow. But, we don’t have to manually add any of these steps. All we have to do is to ask the UserManager class to do that for us. So, to start, we are going to create a new function in the AuthService file:

public login = () => {
    return this._userManager.signinRedirect();
}

In this function, we call the signinRedirect function from the UserManager class. This function will redirect us to the authorization endpoint on the IDP server. Additionally, the UserManager stores a user result in the session storage after a successful login action and we can always retrieve that object and use all the information it contains.

That said, let’s create a new function in the AuthService file:

public isAuthenticated = (): Promise<boolean> => {
  return this._userManager.getUser()
  .then(user => {
    this._user = user;
    return this.checkUser(user);
  })
}

private checkUser = (user : User): boolean => {
  return !!user && !user.expired;
}

Here, we use the getUser function that returns a promise that produces a user object. After we fetch a user object from the storage, we assign it to the local _user property and return a result of the private checkUser function. In this function, we check if the user object is not null and not expired.

We need to do one additional thing. As soon as the user’s status changes, we want to inform any component that needs that kind of information. To do that, we are going to use an Observable:

import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private _userManager: UserManager;
  private _user: User;
  private _loginChangedSubject = new Subject<boolean>();

  public loginChanged = this._loginChangedSubject.asObservable();

And modify the isAuthenticated function:
public isAuthenticated = (): Promise<boolean> => {
  return this._userManager.getUser()
  .then(user => {
    if(this._user !== user){
      this._loginChangedSubject.next(this.checkUser(user));
    }

    this._user = user;
      
    return this.checkUser(user);
  })
}

Modifying the App Component to Check for an Authenticated User

We need to realize that after the login action on the IDP side, the redirection happens back to the Angular application causing the Angular app to refresh itself (a fresh load of the application). So, let’s modify the app.component file to check if the user is authenticated:

import { Component, OnInit } from '@angular/core';
import { AuthService } from './shared/services/auth.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'CompanyEmployees.Client.Oidc';
  public userAuthenticated = false;

  constructor(private _authService: AuthService){
    this._authService.loginChanged
    .subscribe(userAuthenticated => {
      this.userAuthenticated = userAuthenticated;
    })
  }
  
  ngOnInit(): void {
    this._authService.isAuthenticated()
    .then(userAuthenticated => {
      this.userAuthenticated = userAuthenticated;
    })
  }
}

In the ngOnInit function, we just call the isAuthenticated function from the AuthService file and populate the userAuthenticated property with true or false values. Also, we modify the constructor to react to the login change event. Now, if we need it, we can use the userAuthenticated property to modify the app.component.html page based on whether the user is authorized or not.

Testing the Connection Between the Angular Application and IDP

Before we start with the test actions, we need to provide a way to trigger the redirection to the IDP server from a client application. We are going to do that by clicking the Login button on the navigation menu.

That said, let’s modify the menu.component.ts file:

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../shared/services/auth.service';

@Component({
  selector: 'app-menu',
  templateUrl: './menu.component.html',
  styleUrls: ['./menu.component.css']
})
export class MenuComponent implements OnInit {

  constructor(private _authService: AuthService) { }

  ngOnInit(): void {
  }

  public login = () => {
    this._authService.login();
  }

}

Nothing special here, we just call the login function from the AuthService class.

After this modification, let’s modify the menu.component.html file:

<div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item">
        <a class="nav-link" [routerLink]="['/company/companies']" routerLinkActive="active" 
          [routerLinkActiveOptions]="{exact: true}"> Companies </a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#"> Privacy </a>
      </li>
    </ul>
    <section> 
      <button class="btn btn-link" style="color:gray" (click)="login()">Login</button>
    </section>
</div>

Great.

We can start the IDP application and the client application:

ng serve -o

Angular application started - Angular OAuth2 OIDC Security

Once we click the Login button, the application redirects us to the IDP server on the Login screen:

Angular OAuth2 OIDC application with IDP Login screen

If we open the Developer Tools window (F12) and navigate to the Network tab, we are going to see the URI we have been talking about with all the required parameters from the configuration:

 Angular Application Security URI with configuration

Additionally, if we inspect the console logs:

Angular OAuth2 OIDC Security console logs for the authorization server

We can see that the signinRedirect function calls the /.well-known/openid-configuration endpoint, then the /authorize endpoint was invoked and we got redirected to the Login page.

Excellent, we can see our integration works, and our Angular application successfully communicates with the IDP server.

We can use Mick’s credentials (Mick and MickPassword) but after clicking the Login button, we are going to see the 404 page. That’s because we didn’t finish the login process and we don’t have the /signin-oidc route in our angular application:

 Angular OAuth2 OIDC Security redirection to the 404 page

But this also proves that the communication between the Angular app and the IDP server works like a charm.

Conclusion

In this article, we have enabled the communication between the Angular application and the IDP server. The configuration is in place on both sides and we have a way to redirect the user to the Login page that resides on the level of IDP.

Of course, we are not going to display the 404 page after successful login action, and that’s something we are going to handle in the next article. There, we will learn about the authentication actions, how to create required routes and components, and how to execute the logout actions as well.

So, see you there.