SSO with Quarkus, Angular and Keycloak

19.01.2022Stefan Welsch
Cloud Security Keycloak Quarkus Angular Single Sign On

Today I will show you how to use Keycloak as an OIDC client for Quarkus with an Angular frontend. It is a very practical topic, which I have encountered several times in my recent projects. It doesn’t matter whether you use Quarkus or Spring Boot. Only the configurations will be slightly different in both setups.

A few words about Keycloak

Keycloak is an open source software project written in Java, which experienced its first release in September 2014 after a year of development. It is therefore not a particularly new project. With Keycloak SSO can be implemented with identity and access management for modern applications. It implements the OpenId Connect protocol. Until March 2018, the project was driven by the WildFly community. Since then, it has been using RedHat as a driver for its RH-SSO products. Keycloak offers the following features, among others:

  • User registration
  • social logins
  • SSO of all applications registered to the same realm
  • 2-factor authentication
  • LDAP integration via a federation

In principle, the way Keycloak works is quite simple. After successfully authenticating a user, Keycloak issues an Identity Token in the form of a JSON Web Token for that user. At the same time, an access token is issued, which enables the application to call services on behalf of the user. The identity token is the difference between OIDC and the OAuth2.0 protocol, since information about the user’s identity is primarily stored here. With OAuth2.0, only the rights of a user were taken into account in the form of an access token.

All clients that are connected to a realm are available to a user after they have logged on once without having to log on again.

Setup

Quarkus

We create a Quarkus application (Version 2.5.1) with the extensions OpenID Connect and Keycloak Authorization. I leave the rest here as default.

In our Quarkus application itself we create a folder /src/main/ui in which our Angular application will later be placed. So our folder structure looks like this.

Angular

Next we create the Angular application (Version 12), I’m here using the Getting Started Guide from (https://angular.io/start), then downloaded the application and unzipping in into the /src/main/ui folder. Our project should now look like this.

In order to avoid CORS errors, we have to use a proxy in the Angular application to access the Quarkus backend. For this we create a file proxy.conf.json with the following content.

1
2
3
4
5
6
{
  "/api": {
    "target": "http://localhost:8080",
    "secure": false
  }
}

So now all calls to /api are sent to our Quarkus backend.

In the package.json file we also have to change the start script slightly so that this proxy configuration is also used.

1
"start": "ng serve" --> "start": "ng serve --open --proxy-config proxy.conf.json"

Keycloak

Next, let’s set up Keycloak. Keycloak is an open source identity and access management tool that is very versatile. It offers OpenId Connect, OAuth2 and SAML2 as protocols and also offers a user federation with which users can be imported from a company’s own LDAP. In the course of the TechUp we will go into a few more features of Keycloak, but first we will ensure that we make Keycloak available to us locally.

To do this, we create a docker-compose.yaml file with the following content

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
version: "2"

services:

  keycloak:
    image: jboss/keycloak
    environment:
      KEYCLOAK_USER: admin
      KEYCLOAK_PASSWORD: admin
      DB_VENDOR: postgres
      DB_ADDR: keycloak-db
      DB_DATABASE: keycloak
      DB_USER: keycloak
      DB_PASSWORD: keycloak
    depends_on:
      - keycloak-db
    ports:
      - 8180:8080
      - 8543:8443

  keycloak-db:
    image: postgres:alpine
    environment:
      POSTGRES_PASSWORD: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_DB: keycloak
    volumes:
      - ./postgres_data:/var/lib/postgresql/data

We have defined 2 services here. First the Keycloak service itself and second a Postgres database to store our configurations.

After running docker compose up, we can log in to Keycloak’s admin console at http://localhost:8180 with username admin and password admin.

The jboss/keycloak image didn’t work on my new MacBook Pro with M1 processor. There are two options here:

  • use image wizzn/keycloak
  • clone the official repository git@github.com:keycloak/keycloak-containers.git and build the image locally yourself (that’s how I did it).

Once Keycloak has started and we’ve logged into the admin console, we should now see the following page when accessing it.

We have now set up all applications and can start them separately. At this point we want to verify our setup.

We open a new terminal and switch to the root folder of the Quarkus application. Here we enter mvn quarkus:dev. The server should start and we can access it via http://localhost:8080 and see the following screen.

Now we go to the Angular directory and enter npm install && npm run start there. The Angular application should start and a browser window should open automatically with the following content.

Setup done!

(blue star)

Configuration

Keycloak

Realm

Next we take care of the configuration. We start here with Keycloak. First we create a new realm. By default, there is only the master realm. Realms are created in a hierarchical structure. The master realm forms the root. For example, admin accounts that are created in the master realm can access all other realms. So it makes sense to create a separate realm for users and applications in an organization.

To create a new realm, we click on the arrow next to the master realm and there in the popup on “Add Realm”. Then we assign a meaningful name and click on “Create”

Our new realm should then open automatically.

Customers

Front end

Now let’s create the required clients. Clients can request authentication of a user. Clients generally come in 2 forms. The first client type is an application that wants to participate in single sign-on. These clients just want Keycloak to provide them with the appropriate security. The other type of client is one that requests an access token so that it can invoke other services on behalf of the authenticated user.

We will now create an OpenID Connect Client (OIDC Client). To do this, we go to Clients and then click on the “Create” button.

We enter the name of the application as Client ID. Since we have a product list, I’ll call the client productlist-frontend in our case. We will see later why I use the frontend suffix. We enter the URL of our Angular application as the root URL, i.e. http://localhost:4200 by default.

We should now see a new client with the following configurations.

Let’s give our application a name. This is the friendly name for the client when viewed on a Keycloak UI.

The “Access Type” is also important here. With this we determine the type of OIDC client. There are 3 different types:

Confidential The confidential access type is for server-side applications that require a client secret when converting an access code into an access token. This type should be used for server-side applications.

Public The public access type is for client-side applications that need to perform a browser login. With a client-side application, there is no way to keep a secret safe. Instead, it’s very important to restrict access by configuring the correct redirect URIs for the client.

Bearer only The bearer-only access type means that the application only allows bearer token requests. If enabled, this application cannot participate in browser logins.

Everyone should be able to log in to our frontend. We therefore choose the “Access Type” public. Here we now also see why the suffix frontend makes sense at this point.

We don’t have to set anything else in the frontend client.

Backend

Next, let’s take care of the backend. We go back to the Clients overview page and click on “Create”.

We now select productlist-backend as Client ID and this time the URL of our Quarkus server as root URL, i.e. http://localhost:8080.

Unlike our frontend client, we change the access type to confidential. As we can see, the configuration options change after we change the type.

TO DO:

Here we now have to make a few necessary changes so that our authentication works later.

Users and Roles

Finally, we have to create users and roles that we can later use to access the application.

We create 2 roles and 3 users. We create the following table:

  • John - productlist-share, productlist-notify
  • Cindy - productlist-share
  • Chris - productlist-notify

To create the roles, we click on “Roles” → Add Role. After we have created the role, we see the following page. We do this for both roles.

Then we create the new users via Users → Add User.

We now have to assign the role to the users and assign a password. To do this, we first go to the “Role Mappings” tab, select the relevant roles and click on “Add selected”. Then we go to the “Credentials” tab and assign a password for the user here. I just used the username as the password here. The “Temporary” flag can be removed. By activating this flag, you can force the user to change their password after the first login.

When we have created all users, we should see the following entries under the “Users” menu item: (click “View all users”, if the table stays empty).

We are now finished configuring Keycloak and now take care of our frontend and backend.

Angular

Let’s look at the Angular configurations first? First we have to install the Keycloak libraries there. So we go to the /src/main/ui folder and enter the following command.

1
npm install keycloak-angular keycloak-js

After the installation we still have to import and configure the Keycloak module. To do this, we add the following information to the app.module.ts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { TopBarComponent } from './top-bar/top-bar.component';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductAlertsComponent } from './product-alerts/product-alerts.component';
import { KeycloakAngularModule, KeycloakService } from "keycloak-angular";

function initializeKeycloak(keycloak: KeycloakService) {
  return () =>
      keycloak.init({
        config: {
          url: 'http://localhost:8180/auth',
          realm: 'b-nova',
          clientId: 'productlist-frontend',
        },
        initOptions: {
            onLoad: 'login-required'
        },
        bearerExcludedUrls: ['/assets']
      });
}

@NgModule({
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    RouterModule.forRoot([
      { path: '', component: ProductListComponent },
    ]),
    KeycloakAngularModule
  ],
  declarations: [
    AppComponent,
    TopBarComponent,
    ProductListComponent,
    ProductAlertsComponent
  ],
  bootstrap: [
    AppComponent
  ],
  providers: [
      {
          provide: APP_INITIALIZER,
          useFactory: initializeKeycloak,
          multi: true,
          deps: [KeycloakService],
      },
  ]
})
export class AppModule { }

In the initOptions we also specify the option onLoad: 'login-required'. This ensures that the page can only be accessed with authentication. If we now call up the page, we should no longer see the application, but the Keycloak login window.

We can now log in here with the corresponding login data and should then see our application again. But what exactly happened now? Let’s take a closer look at the request we make after logging-in in the browser console.

We see that the first call always goes against Keycloak. Our cookies are checked and replaced there and then a redirect to our application takes place. All authentication is done using the Authorization Code Flow. This ensures that we receive a JWT token with which we can authenticate ourselves for further requests. Let’s take a look at what exactly is in our JWT token. To do this, we first need to modify our application a bit so that we can make a call to the Keycloak API.

We add the app.component.ts as follows. So in OnInit we simply make a call to Keycloak to load the user profile.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {Component, OnInit} from '@angular/core';
import {KeycloakProfile} from "keycloak-js";
import {KeycloakService} from "keycloak-angular";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  public isLoggedIn = false;
  public userProfile: KeycloakProfile | null = null;

  constructor(private readonly keycloak: KeycloakService) { }

  public async ngOnInit() {
    this.isLoggedIn = await this.keycloak.isLoggedIn();

    if (this.isLoggedIn) {
      this.userProfile = await this.keycloak.loadUserProfile();
    }
  }
}

If we then look at the browser console again, we see that there is a new request against the account interface from Keycloak.

This request is made with an authentication header containing the JWT token. If we copy this JWT token once and enter http://jwt.io, then we can see all the information that is stored in it.

We can see that in addition to the username, the roles are also included here.

Quarkus

Now let’s take care of the backend part. In the GreetingResource there is currently a method hello. This can be accessed by anyone. Let’s create a few additional methods that can only be called by authenticated users and also need a specific role.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.bnova;

import io.quarkus.security.Authenticated;

import javax.annotation.security.RolesAllowed;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/api")
public class GreetingResource {

    @GET
    @Path("/hello")
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello World";
    }

    @GET
    @Path("/secure")
    @Produces(MediaType.TEXT_PLAIN)
    @Authenticated
    public String secured() {
        return "Hello Secured";
    }
    
    @GET
    @Path("/share")
    @Produces(MediaType.TEXT_PLAIN)
    @RolesAllowed("productlist-share")
    public String share() {
        return "Hello productlist share";
    }

    @GET
    @Path("/notify")
    @Produces(MediaType.TEXT_PLAIN)
    @RolesAllowed("productlist-notify")
    public String notif() {
        return "Hello productlist notify";
    }
}

Now we also have to make the OIDC configurations in our Quarkus application. We now store the following configurations in the application.properties.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# OIDC Configuration
quarkus.oidc.auth-server-url=http://localhost:8180/auth/realms/b-nova
quarkus.oidc.client-id=productlist-backend
quarkus.oidc.tenant-enabled=true
quarkus.oidc.webapp-tenant.auth-server-url=http://localhost:8180/auth/realms/b-nova
quarkus.oidc.webapp-tenant.client-id=productlist-backend
quarkus.oidc.webapp-tenant.application-type=web-app
quarkus.oidc.webapp-tenant.roles.source=accesstoken
quarkus.oidc.tls.verification=none
quarkus.oidc.credentials.secret=XXX
quarkus.oidc.token.issuer=http://localhost:8180/auth/realms/b-nova

As client-id we enter the backend client (productlist-backend), which we created above. The only thing missing is the secret. We get this when we go back to the Keycloak admin console and go to the “Credentials” tab in our backend client. Here we see a secret, which we copy and paste as a secret in the application.properties.

Now we can start our Quarkus and should receive a message that we are connected to the OIDC client.

In our Angular application we still have to create the appropriate functions to be able to call the API in the backend. So we adjust the product-list.component.ts as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export class ProductListComponent {
  products = products;

  constructor(private http: HttpClient) {
  }

  hello() {
    this.http.get("/api/hello").subscribe(res => alert(res));
  }

  secure() {
    this.http.get("/api/secured").subscribe(res => alert(res));
  }

  share() {
    this.http.get("/api/share").subscribe(res => alert(res));
  }

  onNotify() {
    this.http.get("/api/notify").subscribe(res => alert(res));
  }
}

Of course, the HttpClientModule must also be inserted in the app.module.ts for the imports.

In the product-list.component.html we add the corresponding buttons for hello and secure.

1
2
3
4
  <p><button (click)="hello()">Hello</button></p>
  <p><button (click)="secure()">Secure</button></p>
  <p><button (click)="share()">Share</button></p>
  <app-product-alerts (notify)="onNotify()" [product]="product"></app-product-alerts>

Now we want to click on the different buttons in our Angular application and see what happens. First, let’s log in with the user cindy. As we saw above, Cindy is only allowed share but not notify.

As we can see, this has worked before. Now let’s look at Chris. He can do anything except share.

Looks good too. Lastly, John. With him, every call should be successful.

Made. All users have access as we wish.

Conclusion

Today we saw how Quarkus, Angular and Keycloak can be used to quickly and easily implement full authorization using OIDC. Of course you can configure a lot more here, so that you have the greatest possible flexibility. Keycloak, despite being a grandfather in IT, still meets the needs of a modern cloud environment.

Stefan Welsch

Stefan Welsch – Pionier, Stuntman, Mentor. Als Gründer von b-nova ist Stefan immer auf der Suche nach neuen und vielversprechenden Entwicklungsfeldern. Er ist durch und durch Pragmatiker und schreibt daher auch am liebsten Beiträge die sich möglichst nahe an 'real-world' Szenarien anlehnen.