πŸ“’
openid-connect-workshop
  • Introduction
  • Introduction
    • Requirements and Setup
    • Sample Application Architecture
  • Intro Labs
    • Authorization Grant Flows in Action
    • Authorization Code Grant Demo
    • GitHub Client
  • Hands-On Labs
    • Resource Server
    • Client (Authorization Code Flow)
    • Client (Client Credentials Flow)
    • Testing JWT Auth&Authz
    • JWT Testing Server
    • SPA Client (Authz Code with PKCE)
  • Bonus Labs​
    • Multi-Tenant Resource Server
    • Micronaut
    • Quarkus
    • Keycloak Testcontainers
Powered by GitBook
On this page
  • Lab Contents
  • Learning Targets
  • Folder Contents
  • Start the Lab
  • Explore and run the initial application
  • Step 1: Install the angular-oauth2-oidc library
  • Step 2: Configure the library
  • Step 3: Implementing the authentication triggering method
  • Step 4: Add a few additional features
  • Run the completed application

Was this helpful?

  1. Hands-On Labs

SPA Client (Authz Code with PKCE)

PreviousJWT Testing ServerNextMulti-Tenant Resource Server

Last updated 4 years ago

Was this helpful?

In this third lab we want to build again an OAuth2/OIDC client for the resource server we have built in .

In contrast to and this time the client will be using the and the corresponding .

Lab Contents

Learning Targets

In this sixth workshop lab you will be learning how to build an OAuth 2.0/OIDC compliant frontend using Angular, that works together with the .

In contrast to this time we will see how to build a client with a browser environment without having a secure back-channel. We will use the most modern way to make this possible by facilitating the grant with .

The latest IETF working group drafts of and clearly deprecated the in favor of the + grant.

After you have completed this lab you will have learned:

  • that you can also use OAuth2 and OpenID Connect in a browser environment that does not provide any secure back-channel

Folder Contents

Inside the folder lab 6 you can find 2 applications:

  • library-client-spa-initial: This is the client application we will use as starting point for this lab

  • library-client-spa-complete: This client application is the completed OAuth 2.0/OIDC client for this lab

Start the Lab

Explore and run the initial application

Start by downloading the necessary dependencies. Therefore, change to the lab6/library-client-spa-initial folder using a terminal execute npm install via command line.

Then navigate your IDE of choice (suggesting VS Code or IntelliJ) to the lab6/library-client-spa-initial project and at first explore this project a bit. Then start the application by running ng serve on your commandline.

You will notice that the application starts up but in your browsers console you'll notice some failing HTTP requests when accessing the application. (should be 401 errors) This is because there's no authentication to your IAM solution (Keycloak).

Now stop the client application again (Ctrl-C). You can leave the resource server running as we will need this after we have finished this client.

Step 1: Install the angular-oauth2-oidc library

In this step you're supposed to install the library, nothing else.

npm i angular-oauth2-oidc --save

Next step is to import the library in your app.module.ts in the imports array.

    OAuthModule.forRoot({
      resourceServer: {
        allowedUrls: ['http://localhost:9091/'],
        sendAccessToken: true
      }
    })

Step 2: Configure the library

The library we just installed gives us the ability to use the recommended authorization code grant with PKCE.

This allows us to use this improved authorization code grant without having a secure back-channel. As our application is executing in the user's browser, we are in an unsafe environment, meaning there's no secure channel the user (and by this means also an attacker) can eavesdrop.

Let's get started by creating a new service to encapsulate the authentication (and handle a few implementation quirks): ng g s services/auth --skip-tests

Now open the created file services/auth.service.ts and initialize the configuration object:

AuthConfig = {

    // Url of the Identity Provider
    issuer: 'http://localhost:8080/auth/realms/workshop',

    // URL of the SPA to redirect the user to after login
    redirectUri: window.location.origin + '/index.html',

    // The SPA's id. The SPA is registered with this id at the auth-server
    clientId: 'spa',

    responseType: 'code',
    disableAtHashCheck: true,
    scope: 'openid profile offline_access',
    useSilentRefresh: false,
    showDebugInformation: true,
}

After you've added this configuration to your service class (or as a constant to the file) you can start implementing the authentication. Start by adding instances of OAuthService and Router using dependency injection.

  constructor(
    private OAuthService,
    private Router
  ) {

Now start by adding a few subjects and observables to your class. These are needed to synchronize some function calls (e.g. checking token claims should only be done once authentication is completed) and prevent race conditions.

  private isAuthenticatedSubject$ = new BehaviorSubject<boolean>(false);
  public isAuthenticated$ = this.isAuthenticatedSubject$.asObservable();

  private isDoneLoadingSubject$ = new ReplaySubject<boolean>();
  public isDoneLoading$ = this.isDoneLoadingSubject$.asObservable();

  /**
   * Publishes `true` if and only if (a) all the asynchronous initial
   * login calls have completed or errored, and (b) the user ended up
   * being authenticated.
   *
   * In essence, it combines:
   *
   * - the latest known state of whether the user is authorized
   * - whether the ajax calls for initial log in have all been done
   */
  public Observable<boolean> = combineLatest([
    this.isAuthenticated$,
    this.isDoneLoading$
  ]).pipe(map(values => values.every(b => b)));

Let's get started to set up the library in your constructor()-function:

  constructor(
    private OAuthService,
    private Router
  ) {
    this.oauthService.configure(authConfig);

    this.oauthService.events
      .subscribe(_ => {
        this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken());
      });

    this.oauthService.setupAutomaticSilentRefresh();
  }

As you can see the configuration object we create before is put into the OAuthService. Afterwards the tokenValidationHandler is set to NullValidationHandler. This has one reason: The library currently has a bug which leads to being unable to disable the at_hash check with JwksValidationHandler. Once this has been fixed, you should NOT use NullValidationHandler.

setupAutomaticSilentRefresh() is used to enable background refreshing of the tokens once they exceed a percentage of their maximum lifetime. (by default 75%)

Step 3: Implementing the authentication triggering method

Next step is to implement the method that starts the authentication process. This process is split into multiple parts:

  1. Start the actual login procedure that redirects the user-agent to the authentication server discovered in 1. The library decides whether to use implicit or authorization code grant (with PKCE) by evaluating if responseType: 'code' was set.

  2. After the login has taken place, the library will automatically store the tokens and resolve the promise. After resolving the promise we are publishing that the login is done loading. In addition the query params are cleared by navigating to /.

NOTE: Currently it's not possible to keep the state when using authorization code grant with PKCE. This is a limitation by the library and will soon be fixed.

public runInitialLoginSequence(): Promise<void> {
  return this.oauthService.loadDiscoveryDocument()
          .then(() => this.oauthService.tryLoginCodeFlow())
          .then(() => {
            this.isDoneLoadingSubject$.next(true);
            // remove query params
            this.router.navigate(['']);
          })
          .catch(() => this.isDoneLoadingSubject$.next(true));
}

After you implemented this function, you can use it in your AppComponent component (app/app.component.ts) :

  constructor(private AuthService) {
    this.authService.runInitialLoginSequence();
  }

Step 4: Add a few additional features

As you can now already see, you are directly forced to login, and your token is later on used to query APIs. But there are a few things missing:

  • Your routes are not protected yet. Meaning by clever modification, users can access any part of your UI.

  • The name of the user logged in is not shown.

  • The logout button has no function at all

Let's get these bullet points fixed step-by-step.

Step 4a: Guarding routes

Start by generating guard classes using the Angular CLI ng g g guards/auth and ng g g guards/bookCreate. If you're asked, select canActivate. This will create two classes app/guards/auth.guard.ts and app/guards/book-create.guard.ts. Both should implement the CanActivate-interface. If not, add that manually.

We'll start by modifying the routing first (without having the guards implemented). Open your app/app-routing.module.ts and apply modifications (import necessary classes):

const routes: Routes = [
  {
    path: '',
    canActivate: [AuthGuard],
    children: [
      {path: 'createBook', component: BookCreateComponent, canActivate: [BookCreateGuard]},
      {path: '', component: BookListComponent, canActivate: [AuthGuard]}
    ]
  },
  {
    path: '**',
    component: BookListComponent,
    canActivate: [AuthGuard]
  }
];

Now you shouldn't be able to access any component (except the header) anymore. That's correct, let's get this fixed, starting with the AuthGuard component:

export class AuthGuard implements CanActivate {

  private isAuthenticated: boolean;

  constructor(private authService: AuthService) {
    this.authService.isAuthenticated$.subscribe(i => this.isAuthenticated = i);
  }

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot,
  ): Observable<boolean> {
    return this.authService.isDoneLoading$
      .pipe(filter(isDone => isDone))
      .pipe(tap(_ => this.isAuthenticated || this.authService.login()))
      .pipe(map(_ => this.isAuthenticated));
  }
}

Next we'll implement the BookCreateGuard, which will be less complicated:

export class BookCreateGuard implements CanActivate {

  constructor(
    private authService: AuthService,
    private router: Router
    ) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
    if(!this.authService.hasRole('LIBRARY_CURATOR')) {
      return this.router.navigate(['']);
    }
    return true;
  }
}

You'll see, that hasRole is missing in your AuthService. You can try to implement it on your own by fiddling with the id token, or you simply take my set of convenience methods and add them to your app/services/auth.service.ts:

  public hasRole(role: string) {
    let claims: any = this.oauthService.getIdentityClaims();
    if (claims && claims.groups) {
      let roles: string[] = claims.groups;
      roles = roles.map(role => role.toUpperCase());
      return roles.includes(role.toLocaleUpperCase());
    }
    return false;
  }

  public getFullname() {
    let claims: any = this.oauthService.getIdentityClaims();
    if (claims && claims.name) {
      return claims.name;
    }
    return undefined;
  }

  public login() { this.oauthService.initCodeFlow(); }
  public logout() { this.oauthService.logOut(); }
  public refresh() { this.oauthService.silentRefresh(); }
  public hasValidToken() { return this.oauthService.hasValidAccessToken(); }

Your routes should now be protected, but you still see the Create Book button, even if you're not authorized to do so. Let's fix this quickly. Go to app/header/header.component.ts and inject the AuthService:

constructor(private AuthService) { }

Now go to the template app/header/header.component.html and add a ngIf to the jumbotron at the bottom:

 <div class="jumbotron" *ngIf="hasRole('LIBRARY_CURATOR') || hasRole('LIBRARY_ADMIN'">
     <a class="btn btn-primary" href="#" role="button" [routerLink]="['/createBook']" routerLinkActive="router-link-active">Create Book</a>
 </div>

If you're not an admin or a curator, you shouldn't see the whole jumbotron anymore by now.

Step 4b Show the user's name

As you already opened the file that needs to be modified (app/header/header.component.ts) if you followed the guide step-by-step, you can quickly add this feature. Simply fill the fullname attribute on init:

  ngOnInit() {
    this.authService.isDoneLoading$.subscribe(_ => {
      this.fullname = this.authService.getFullname();
    });
  }

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

  hasRole(role: string): boolean {
    return this.authService.hasRole(role);
  }

As you can see we wait until the authService has finished processing, so we can safely access the attribute.

Step 4c Enable logout

As in Step 4b, you'll need to modify the HeaderComponent. Try to implement the logout()-function yourself. πŸ˜‰

Run the completed application

Now it is time to see the completed application running in action.

Then start the application by running ng serve on your commandline.

You will notice that the application starts up, and your browser will automatically redirect to our IAM solution (Keycloak) to authenticate the user.

This concludes our lab on securing a SPA with OAuth 2.0 and OpenID Connect.

how to configure an Angular application to use OAuth2.0/OIDC with authorization code + grant

Now, let's start with Lab 6. Here we will implement the required additions to get a Single-Page-Application (SPA) that calls the resource server we have implemented in . This time we will use the grant with the addition of .

We will use as identity provider. Please again make sure you have set up keycloak as described in the section.

First start the resource server application of Lab 1. If you could not complete the previous Lab yourself then use and start the completed reference application in project .

To see the application action open your browser on (as shown in the terminal after issuing ng serve).

This will install the latest version of 's OIDC certified OAuth 2.0 / OpenID Connect library for the Angular framework. This library already supports the authorization code + PKCE flow. It can also either refresh access tokens by using silent refresh hidden frame workaround or use the real refresh tokens.

disableAtHashCheck is currently necessary as Keycloak does not include an at_hash claim in its id tokens. According to the this claim is optional. Keycloak will add support for this in version 11.0.0.

Kudos to , who published an example on this library which takes care of multiple race condition problems. Major parts of this service are taken directly from his example.

Get the OIDC discovery document as specified in

First make sure you still have Keycloak running, and you have started the resource server application of Lab 1 ().

To see the application action open your browser on (as shown in the terminal after issuing ng serve).

By using the grant + instead of the we reduced risks for token leakage pretty much. The only drawback we are still facing is the fact that the library stores the tokens in browser local storage which is not a safe place in case of a cross site scripting vulnerability.

Please consult the latest draft for more details on this.

PKCE
lab 1
authorization code
PKCE
Keycloak
Setup
lab1/library-server-complete
http://localhost:4200
Manfred Steyer
OIDC Core 1.0 3.1.3.6
Jeroen Heijmans
OpenID Connect Discovery 1.0 incorporating errata set 1
lab1/library-server-complete
http://localhost:4200
authorization code
PKCE
implicit grant
OAuth 2.0 for Browser-Based Apps
lab 1
Lab 2
Lab 3
Angular
OAuth 2.0/OIDC library
resource server of Lab 1
Lab 2
authorization code
PKCE
OAuth 2.0 for Browser-Based Apps
OAuth 2.0 Security Best Current Practice
implicit grant
authorization code
PKCE
Learning Targets
Folder Contents
Hands-On: Implement the OAuth 2.0/OIDC batch client
Explore the initial client application
Step 1: Configure as OAuth2/OIDC client w/ client credentials
Step 2: Configure web client to send bearer access token
Step 3: Run and debug the web client authorities