OpenID Connect with Angular 8 (OIDC Part 7)

Share on facebook
Share on google
Share on twitter
Share on linkedin
Now we are getting somewhere, huh!? So far we have played around with the authorization code, hybrid and client credentials flow to get a grasp of when to use what. We have made the authorization server interactive and dynamic by saving user data and IdentityServer data in the database. In this part, we are going to implement the Angular app on the client app to use an authorization code client with PKCE for authenticating and calling an authorized endpoint on the resource API. As mentioned in the first part, we are using code flow with PKCE here because we are using an Angular app, which runs in the browser and can’t keep a client secret so we use PKCE to generate a dynamic secret. In the first part, I wrote about the security concerns of OpenId Connect/OAuth2 and how to mitigate them, and Angular is beneficial for this purpose in the following ways:

  • Built-in XSS protection (if not doing manual dom manipulation, which is an Angular anti-pattern anyway).
  • Official OpenID connect approved implementations of the specification. It is more error-prone to implement the OpenID connect standard ourselves, with stuff like token validation, implementing validation rules etc.

The whole solution for this part can be found on my Github here.

The OpenID connect with IdentityServer4 and Angular series

This series is learning you OpenID connect with Angular in these parts:

Authorization server

For this part, the authorization server needs a code flow client with PKCE for the Angular application.

Setup code flow client with PKCE on the Authorization server

We go to the Config.cs file and add the following client to the Authorization server’s Config.cs:

new Client
{
    ClientId = "spaCodeClient",
    ClientName = "SPA Code Client",
    AccessTokenType = AccessTokenType.Jwt,
    AccessTokenLifetime = 330,// 330 seconds, default 60 minutes
    IdentityTokenLifetime = 30,

    RequireClientSecret = false,
    AllowedGrantTypes = GrantTypes.Code,
    RequirePkce = true,

    AllowAccessTokensViaBrowser = true,
    RedirectUris = new List<string>
    {
        $"{spaClientUrl}/callback",
        $"{spaClientUrl}/silent-renew.html",
        "https://localhost:4200",
        "https://localhost:4200/silent-renew.html"
    },
    PostLogoutRedirectUris = new List<string>
    {
        $"{spaClientUrl}/unauthorized",
        $"{spaClientUrl}",
        "https://localhost:4200/unauthorized",
        "https://localhost:4200"
    },
    AllowedCorsOrigins = new List<string>
    {
        $"{spaClientUrl}",
        "https://localhost:4200"
    },
    AllowedScopes = new List<string>
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        "resourceApi"
    }
},

Here we are creating a client for single-page applications (SPAs) like Angular. This is using code flow grant type and will validate the requesters code_verifier and authorization code before returning the requested tokens. As usual, it also specifies the redirect URLs and the scopes contained in the access token. Note, that the RequirePkce is set to enable PKCE and that the access token and ID token is very shortlived because we are going to be using silent renewal to periodically renew these tokens in the background.

Remember, in the previous part we set up dynamic configuration with Entity Framework, containing a seed method reading this Config.cs file for populating the database if not already populated. If your database is already populated from the previous part, remember to delete it to get it seeded with this new client.

Resource API

Setup an authorized controller with a method providing weather data to the client application.

ResourceApi/Controllers/SampleDataController.cs

[Route("api/[controller]")]
[Authorize]
public class SampleDataController : Controller
{
   private static string[] Summaries = new[]
   {
       "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
   };

   [HttpGet("WeatherForecasts")]
   public IEnumerable<WeatherForecast> WeatherForecasts()
   {
       var rng = new Random();
       return Enumerable.Range(1, 5).Select(index => new WeatherForecast
       {
           DateFormatted = DateTime.Now.AddDays(index).ToString("d"),
           TemperatureC = rng.Next(-20, 55),
           Summary = Summaries[rng.Next(Summaries.Length)]
       });
   }

   public class WeatherForecast
   {
       public string DateFormatted { get; set; }
       public int TemperatureC { get; set; }
       public string Summary { get; set; }

       public int TemperatureF
       {
           get
           {
               return 32 + (int)(TemperatureC / 0.5556);
           }
       }
   }
}

This controller is created by simply copying the existing weather endpoint from the app client and adding the authorized attribute.

Configure Startup.cs for a browser client

The client app is doing a cross-origin request when requesting the resource API, so we need to enable CORS in Startup.cs with the CORS middleware:

ResourceApi/Startup.cs

services.AddCors(options =>
{
    options.AddPolicy("default", policy =>
    {
        policy.WithOrigins(Configuration["clientUrl"])
            .AllowAnyHeader()
            .AllowAnyMethod();
    });
});

Now we are ready to connect our app to this endpoint.

ClientApp

For the client app we need to first update to Angular 8 and hereafter create an app showing weather data by calling the authorized endpoint on the resource API.

Setup an Angular app with Angular 8 hosted on a DotNet Core 2 server

ASP.NET Core has built-in support for Angular apps. In part 2 we scaffolded ClientApp as an ASP.NET application with Angular, setting it up with Angular 6. Angular 6 is the version been scaffolded with DotNet Core 2 so we want to upgrade that to Angular 8 by doing a few changes:

  1. Rename the existing AppClient and use the newest version of Angular CLI to generate a new Angular CLI app
  2. Move the look from the original Angular app over to the new Angular 8 app
  3. Setup OpenID Connect in the Angular app

Step 1: Update to Angular 8

First, we rename the old ClientApp inside of the ClientApp to ClientApp2 (we will soon delete it, don’t sweat the name).

Then, to generate a new Angular CLI project we run:

npm i -g @angular/cli // ensure we have the newest Angular CLI
ng new ClientApp // generate new Angular CLI app

After running this we should have the brand new Angular CLI app setup.

Step 2: Setup the look of the app

Install bootstrap for the styling:
npm i bootstrap@^4.3.1
We already had a decent design setup in the DotNet Core auto-generated Angular app. We are going to replace everything inside of the ClientApp/ClientApp/src/app with the what is in ClientApp/ClientApp2/src/app.
We now try to run the site with:
Selecting the ClientApp, in Visual Studio, as start project and run it. We should now see something like this:
Good so far! Let’s proceed.

Setting up the Angular app for OpenID Connect with PKCE

We set up OpenID connect in the Angular with the specification approved library called angular-auth-oidc-client. This gives us an easy abstraction to use in our Angular application that implements the validation rules according to the OpenID Connect specification. Install the Angular OIDC client:

npm i angular-auth-oidc-client

Setup Auth Service

We create a new service called Auth for wrapping the authentication and authorization logic, including the oidc client library. I’m following the Angular style guide which puts global singleton services into a folder named “Core”.

We create a new folder named core and create an auth.service.ts file.

The whole folder structure should look like this:

We are going to create the auth service and configure the OIDC client so it:

  1. Requests the right tokens and scopes
  2. Does the right redirects
  3. Knows how to locate the endpoints on the authorization server
  4. Is doing silent renew of tokens

Configuring the OIDC client

ClientApp/ClientApp/app/components/core/auth.service.ts

public initAuth() {
    const openIdConfiguration: OpenIdConfiguration = {
        stsServer: this.authUrl,
        redirect_url: this.originUrl + 'callback',
        client_id: 'spaCodeClient',
        response_type: 'code',
        scope: 'openid profile resourceApi',
        post_logout_redirect_uri: this.originUrl,
        forbidden_route: '/forbidden',
        unauthorized_route: '/unauthorized',
        silent_renew: true,
        silent_renew_url: this.originUrl + '/silent-renew.html',
        history_cleanup_off: true,
        auto_userinfo: true,
        log_console_warning_active: true,
        log_console_debug_active: true,
        max_id_token_iat_offset_allowed_in_seconds: 10,
    };

    const authWellKnownEndpoints: AuthWellKnownEndpoints = {
        issuer: this.authUrl,
        jwks_uri: this.authUrl + '/.well-known/openid-configuration/jwks',
        authorization_endpoint: this.authUrl + '/connect/authorize',
        token_endpoint: this.authUrl + '/connect/token',
        userinfo_endpoint: this.authUrl + '/connect/userinfo',
        end_session_endpoint: this.authUrl + '/connect/endsession',
        check_session_iframe: this.authUrl + '/connect/checksession',
        revocation_endpoint: this.authUrl + '/connect/revocation',
        introspection_endpoint: this.authUrl + '/connect/introspect',
    };

    this.oidcSecurityService.setupModule(openIdConfiguration, authWellKnownEndpoints);
    ...

Create HTTP helpers for authorized requests

We create methods for doing HTTP calls that set the Authorization header with the bearer token:

ClientApp/ClientApp/app/components/core/auth.service.ts

get(url: string): Observable<any> {
    return this.http.get(url, { headers: this.getHeaders() })
    .pipe(catchError((error) => {
        this.oidcSecurityService.handleError(error);
        return throwError(error);
    }));
}

put(url: string, data: any): Observable<any> {
    const body = JSON.stringify(data);
    return this.http.put(url, body, { headers: this.getHeaders() })
    .pipe(catchError((error) => {
        this.oidcSecurityService.handleError(error);
        return throwError(error);
    }));
}

delete(url: string): Observable<any> {
    return this.http.delete(url, { headers: this.getHeaders() })
    .pipe(catchError((error) => {
        this.oidcSecurityService.handleError(error);
        return throwError(error);
    }));
}

post(url: string, data: any): Observable<any> {
    const body = JSON.stringify(data);
    return this.http.post(url, body, { headers: this.getHeaders() })
    .pipe(catchError((error) => {
        this.oidcSecurityService.handleError(error);
        return throwError(error);
    }));
}

Note that these HTTP methods make sure to append the access token as a bearer header and will handle HTTP errors with respect to how we set up the unauthorized and forbidden redirects.

Adding a login/logout button

We create login and logout methods:ClientApp/ClientApp/app/components/core/auth.service.ts

login() {
    console.log('start login');
    this.oidcSecurityService.authorize();
}

logout() {
    console.log('start logoff');
    this.oidcSecurityService.logoff();
}

These methods are used by the navigation component to login and logout the user.

We add an extra nav item to call these methods:

ClientApp/ClientApp/src/app/nav-menu/nav-menu.component.html

<li *ngIf="!isAuthorized" class="nav-item"  [routerLinkActive]="['link-active']">
  <a class="nav-link text-dark"  (click)="login()" [routerLink]="['/login']">Login</a>
</li>
<li *ngIf="isAuthorized" class="nav-item"  [routerLinkActive]="['link-active']">
    <a (click)="logout()" class="nav-link text-dark" href="#">Logout</a>
</li>

Clicking on these are then handled in the navbar component with:

ClientApp/ClientApp/src/app/nav-menu/nav-menu.component.ts

  public isAuthorized = false;

  /**
   *
   */
  constructor(private authService: AuthService) {
    this.isAuthorizedSubscription = this.authService.getIsAuthorized().subscribe(
      (isAuthorized: boolean) => {
        this.isAuthorized = isAuthorized;
      });
  }
  public login() {
    this.authService.login();
  }

  public logout() {
    this.authService.logout();
  }

Setting up silent renew

Notice, how we set the silent_renew and silent_renew_url for the OIDC client configuration.
This creating a hidden iframe in the DOM, which will update the tokens when the ID token has expired. That is why we made that shorter than the access token in the Authorization server configuration for the PKCE client.

We set the URL of the renew IFrame like this:

silent_renew_url: this.originUrl + '/silent-renew.html',

Now we just need to make sure we have an HTML page with a callback handler for handling silent renew.

We create a new file in wwwroot called silent-renew.html and copy this in:

<!doctype html>
<html>
<head>
    <base href="./">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>silent-renew</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>

    <script>
        window.onload = function () {
            /* The parent window hosts the Angular application */
            var parent = window.parent;
            /* Send the id_token information to the oidc message handler */
            var event = new CustomEvent('oidc-silent-renew-message', { detail: window.location });
            parent.dispatchEvent(event);
        };
    </script>
</body>
</html>

Our wwwroot should now look like this:

 

 

Request authorized weather data

The weather data is fetched from the Resource API by calling its endpoint with the authService get method:

ClientApp/ClientApp/app/components/fetchdata/fetchdata.component.ts

ngOnInit(): void {
    this.authService.get(this.apiUrl + '/api/SampleData/WeatherForecasts').subscribe(result => {
        this.forecasts = result as WeatherForecast[];
    }, (error) => {
        console.error(error);
    });
}

Running the app

As usual, we can select the AuthorizationServer, AppClient, and ResourceApi and make them run together. From here we can click login, register a user, login, give consent and get access to the authorized weather forecast data from ResourceAPI:

The whole solution for this part can be found on my Github here.

Conclusion

In this part, the last part of the series, we got our system set up with an Angular client using a code flow with PKCE client. We updated to Angular 8 and used an Angular library, called angular-auth-oidc-client, approved by the OpenID connect standard for easily plugging the Angular app into the OpenID connect setup. This made the Angular app able to authenticate and be authorized to request an authorized resource on the resource API.

This is concluding the OpenID connect with Angular series. I hope you learned a lot and it gave you a grasp of the different variations of the OpenID connect implementation, including the different flow types, scopes and hands-on implementations of this. OpenID connect is extremely relevant today as it is the most common standard for authentication and authorization, implemented in almost all big companies in one variation or another. You really got an advantage knowing this stuff now on a practical level as many people talk about this in vague terms because they don’t really know what’s going on under the helmet. You do know now; no more buzzword bingo!

References

Securing Angular applications using the OpenID Connect Code Flow with PKCE

Do you want to become an Angular architect? Check out Angular Architect Accelerator.

Related Posts and Comments

Error, loading, content…? Use this page pattern for your Angular apps

When developing Angular applications, it’s common for pages to transition through three key states: error, loading, and show content. Every time you fetch data from an API, your page will likely show a loading indicator first, and then either render the content successfully or display an error message if something goes wrong. This pattern is

Read More »

How to do Cypress component testing for Angular apps with MSW

In this post, we will cover how to do Cypress Component testing with MSW (mock service worker) and why it’s beneficial to have a mock environment with MSW. The mock environment My recommendation for most enterprise projects is to have a mocking environment as it serves the following purposes : * The front end can

Read More »

Handling Authentication with Supabase, Analog and tRPC

In this video, I cover how to handle authentication with Supabase, Analog and tRPC. It’s based on my Angular Global Summit talk about the SPARTAN stack you can find on my blog as well. Code snippets Create the auth client Do you want to become an Angular architect? Check out Angular Architect Accelerator.

Read More »