SSO mit Quarkus, Angular und Keycloak

19.01.2022Stefan Welsch
Cloud Security Keycloak Quarkus Angular Single Sign On

Heute zeige ich Ihnen wie Sie Keycloak als OIDC-Client für Quarkus mit einem Angular Frontend nutzen können. Es ist ein sehr praktisches Thema, welches ich in meinen letzten Projekten schon öfter angetroffen habe. Ob man nun Quarkus oder Spring Boot nutzt, spielt hierbei keine wesentliche Rolle. Lediglich die Konfigurationen werden in beiden Setups etwas unterschiedlich sein.

Ein paar Worte zu Keycloak

Keycloak ist ein in Java geschriebenes Open-Source Software-Projekt, welches nach einem Jahr Entwicklungszeit im September 2014 sein erstes Release erlebte. Es ist daher kein besonders neues Projekt. Mit Keycloak lässt sich SSO mit Identity- und Access Management für moderne Applikationen realisieren. Es implementiert dabei das OpenId-Connect-Protokoll. Bis März 2018 wurde das Projekt von der WildFly-Community getrieben. Seither nutzt es RedHat als Treiber für seine RH-SSO Produkte. Keycloak bietet unter anderem die folgenden Features:

  • Benutzer Registrierung
  • Social Logins
  • SSO aller Applikationen die beim gleichen Realm registriert sind
  • 2-Faktor-Authentifizierung
  • LDAP-Integration über eine Federation

Die Funktionsweise von Keycloak ist im Prinzip recht einfach. Nach der erfolgreichen Authentifizierung eines Benutzers stellt Keycloak ein Identity Token in Form eines JSON Web Token für diesen Benutzer aus. Gleichzeitig wird auch ein Access Token ausgestellt, welcher die Anwendung dazu befähigt, im Namen des Benutzers Dienste aufzurufen. Das Identity Token bildet den Unterschied von OIDC zum OAuth2.0 Protokoll da hier vor allem Informationen über die Identität des Benutzers gespeichert werden. Bei OAuth2.0 wurden nur die Rechte eines Benutzers in Form eines Access Tokens berücksichtigt.

Alle Clients, die an einen Realm angeschlossen sind, stehen nach der einmaligen Anmeldung eines Benutzers für diesen ohne weitere Anmeldung zur Verfügung.

Setup

Quarkus

Wir erstellen uns eine Quarkus-Applikation (Version 2.5.1) mit den Extensions OpenID Connect- und Keycloak-Authorization. Den Rest lasse ich hier default.

In unserer Quarkus Applikation selbst erstellen wir uns einen Ordner /src/main/ui in welchem später unsere Angular Applikation platziert wird. Unsere Ordnerstruktur sieht also folgendermassen aus.

Angular

Als Nächstes erstellen wir uns die Angular Applikation (Version 12). Ich bin hier den Getting Started Guide (https://angular.io/start) komplett durchgegangen, habe mir anschliessend die Applikation heruntergeladen und in den Ordner /src/main/ui entpackt. Unser Projekt sollte jetzt folgendermassen aussehen.

Damit wir CORS Fehler vermeiden müssen wir in der Angular Applikation einen Proxy verwenden, um auf das Quarkus Backend zuzugreifen. Hierzu legen wir uns eine Datei proxy.conf.json mit dem folgenden Inhalt an.

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

Es werden nun also alle Aufrufe an /api an unser Quarkus Backend gesendet.

In der package.json Datei müssen wir dazu noch das start Script leicht abändern, damit diese Proxy Konfiguration auch genutzt wird.

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

Keycloak

Als Nächstes wollen wir uns Keycloak aufsetzen. Keycloak ist ein Open-Source Identity und Access Management Tool, welches sehr vielseitig ist. So bietet es OpenId Connect, OAuth2 und SAML2 als Protokolle an und bietet weiterhin eine User Federation an, mit welcher man Benutzer aus einem firmeneigenem LDAP importieren kann. Im Laufe des TechUp werden wir noch auf ein paar Features von Keycloak eingehen, aber erstmal sorgen wir dafür, dass wir uns lokal Keycloak zur Verfügung stellen.

Dazu erstellen wir uns ein docker-compose.yaml File mit dem folgenden Inhalt

 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

Wir haben hier 2 Services definiert. Einmal den Keycloak Service selbst und als zweites eine Postgres Datenbank um unsere Konfigurationen zu speichern.

Nachdem wir docker compose up ausgeführt haben, können wir uns mit Benutzername admin und Passwort admin an der Adminkonsole unter http://localhost:8180 von Keycloak anmelden.

Das Image jboss/keycloak hat auf meinem neuen MacBook Pro mit M1 Prozessor nicht funktioniert. Es gibt hier zwei Möglichkeiten:

  • Image wizzn/keycloak nutzen

  • Klonen des offiziellen Repositories git@github.com:keycloak/keycloak-containers.git und das Image lokal selber bauen 😉 (so habe ich es gemacht)

Wenn Keycloak gestartet ist und wir uns in der Adminkonsole angemeldet haben, sollten wir beim Zugriff nun die folgende Seite sehen.

Wir haben nun alle Applikationen aufgesetzt und können diese jeweils separat starten. An dieser Stelle wollen wir unser Setup verifizieren.

Wir öffnen ein neues Terminal und wechseln in den Root Folder der Quarkus Applikation. Hier geben wir mvn quarkus:dev ein. Der Server sollte starten und wir können über http://localhost:8080 darauf zugreifen und folgenden Screen sehen.

Nun gehen wir in das Angular Verzeichnis und geben dort npm install && npm run start ein. Die Angular Applikation sollte starten und ein Browserfenster sollte sich automatisch öffnen mit dem folgenden Inhalt.

Setup done! 🌠

Konfiguration

Keycloak

Realm

Als Nächstes kümmern wir uns um die Konfiguration. Wir starten hier mit Keycloak. Als Erstes erstellen wir uns einen neuen Realm. Per Default gibt es nur den Master Realm. Realms werden in einer hierarchischen Struktur angelegt. Der Master Realm bildet dabei die Wurzel. So dürfen beispielsweise Admin Accounts, welche im Master Realm angelegt werden, auf alle anderen Realms zugreifen. Es ist also sinnvoll für Benutzer und Applikationen in einer Organisation einen eigenen Realm anzulegen.

Um einen neuen Realm anzulegen, klicken wir auf den Pfeil neben dem Master Realm und dort im Popup auf “Add Realm”. Anschliessen vergeben wir einen sinnvollen Namen und klicken auf “Create”

Anschliessen sollte unser neuer Realm automatisch geöffnet werden.

Clients

Frontend

Nun legen wir uns die erforderlichen Clients an. Clients können die Authentifizierung eines Benutzers anfordern. Dabei kommen Clients generell in 2 Formen vor. Der erste Client-Typ ist eine Anwendung, die am Single-Sign-On teilnehmen möchte. Diese Clients wollen nur, dass Keycloak ihnen die entsprechende Sicherheit bietet. Der andere Client-Typ ist einer, der ein Zugriffstoken anfordert, damit er andere Dienste im Namen des authentifizierten Benutzers aufrufen kann.

Wir werden uns nun einen OpenID Connect Client (OIDC Client) erstellen. Dazu gehen wir auf Clients und klicken anschliessend auf den “Create” Button.

Als Client ID geben wir den Namen der Applikation ein. Da wir eine Produktliste haben, nenne ich den Client in unserem Fall productlist-frontend. Warum ich den frontend Suffix nehme sehen wir später noch genauer. Als Root URL geben wir hier die URL unserer Angular Applikation ein, also per Default http://localhost:4200.

Wir sollten nun einen neuen Client mit den folgenden Konfigurationen sehen.

Geben wir unserer Applikation noch einen Namen. Dies ist der Anzeigename für den Client, wenn er auf einer Keycloak-UI angezeigt wird.

Wichtig ist hier noch der “Access Type”. Hiermit bestimmen wir die Art des OIDC Clients. Es gibt dabei 3 verschiedene Arten:

Vertraulich (confidential) Der vertrauliche Zugriffstyp ist für serverseitige Applikationen, die einen Client-Secret benötigen, wenn sie einen Zugriffscode in ein Zugriffstoken umwandeln. Dieser Typ sollte für serverseitige Anwendungen verwendet werden.

Öffentlich (public) Der öffentliche Zugriffstyp ist für clientseitige Applikationen, die eine Browseranmeldung durchführen müssen. Bei einer clientseitigen Anwendung gibt es keine Möglichkeit, einen Secret sicher aufzubewahren. Stattdessen ist es sehr wichtig, den Zugriff einzuschränken, indem die richtigen Umleitungs-URIs für den Client konfiguriert werden.

Nur Inhaber (bearer-only)
Der Nur-Inhaber-Zugriffstyp bedeutet, dass die Anwendung nur Bearer-Token-Anforderungen zulässt. Wenn dies aktiviert ist, kann diese Anwendung nicht an Browser-Logins teilnehmen.

An unserem Frontend soll sich jeder anmelden können. Wir wählen hier deshalb den “Access Type” public. Hier sehen wir nun auch, warum das Suffix Frontend an dieser Stelle Sinn macht.

Wir müssen im Frontend Client erstmal nichts weiter einstellen.

Backend

Kümmern wir uns als Nächstes um das Backend. Wir gehen wieder auf die Clients Übersichtsseite und klicken auf “Create”.

Als Client ID wählen wir nun productlist-backend und als Root URL diesmal die URL von unserem Quarkus-Server, also http://localhost:8080.

Anders als bei unserem Frontend Client ändern wir hier den Access Type auf confidential. Wie wir sehen können ändern sich auf die Konfigurationsmöglichkeiten, nachdem wir den Typ geändert haben.

TODO:

Hier müssen wir nun ein paar notwendige Änderungen vornehmen, damit unsere Authentifizierung später funktioniert.

Benutzer und Rollen

Als Letztes müssen wir uns noch Benutzer und Rollen anlegen, mit denen wir später auf die Applikation zugreifen dürfen.

Wir legen uns 2 Rollen und 3 Benutzer an. Dabei bilden wir die folgende Auflistung (User/Roles) ab:

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

Zum Anlegen der Rollen klicken wir auf “Roles” → Add Role. Nachdem wir die Rolle angelegt haben sehen wir die folgende Seite. Wir machen dies für beide Rollen.

Anschliessend legen wir uns über Users → Add User die neuen Benutzer an.

Den Benutzern müssen wir nun noch die Rolle zuweisen und ein Passwort vergeben. Dazu gehen wir erstmal in den Tab “Role Mappings”, selektieren die entsprechenden Rollen und klicken auf “Add selected”. Danach gehen wir in den Tab “Credentials” und vergeben hier ein Passwort für den Benutzer. Ich habe hier einfach den Benutzernamen als Passwort verwendet. Das Flag “Temporary” kann man entfernen. Durch die Aktivierung dieses Flags, kann man erzwingen, dass der Benutzer nach der ersten Anmeldung sein Passwort ändern muss.

Wenn wir alle Benutzer angelegt haben sollten wir unter dem Menüpunkt “Users” die folgenden Einträge sehen (“View all users” klicken, falls die Tabelle leer bleibt).

Wir sind mit der Konfiguration von Keycloak jetzt fertig und kümmern uns nun um unser Frontend und Backend.

Angular

Wollen wir uns als Erstes die Angular Konfigurationen anschauen? Als Erstes müssen wir uns dort die Keycloak Libraries installieren. Wir wechseln also in den Ordner /src/main/ui und geben den folgenden Befehl ein.

1
npm install keycloak-angular keycloak-js

Nach der Installation müssen wir das Keycloak Modul noch importieren und konfigurieren. Dazu ergänzen wir in der app.module.ts die folgenden Angaben.

 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 { }

Bei den initOptions geben wir noch die Option onLoad: 'login-required' an. Damit wird sichergestellt, dass die Seite nur authentifiziert aufgerufen werden kann. Wenn wir die Seite nun aufrufen, sollten wir nicht mehr die Applikation, sondern das Login Fenster von Keycloak sehen.

Wir können uns hier nun mit den entsprechenden Anmeldedaten anmelden und sollten anschliessend wieder unsere Applikation sehen. Aber was genau ist nun passiert? Schauen wir uns den Request, den wir nach der Anmeldung machen, in der Browser Konsole mal etwas genauer an.

Wir sehen, dass der erste Aufruf immer gegen Keycloak geht. Dort werden unsere Cookies überprüft und ersetzt und es erfolgt anschliessend ein Redirect zu unserer Applikation. Die ganze Authentifizierung erfolgt mittels des “Authorization Code Flow”. Dieser sorgt dafür, dass wir einen JWT Token erhalten, mit dem wir uns bei weiteren Requests authentifizieren können. Wollen wir uns mal anschauen, was genau in unserem JWT Token steht. Dazu müssen wir erst unsere Applikation etwas modifizieren, damit wir einen Aufruf gegen die API von Keycloak machen können.

Wir ergänzen die app.component.ts folgendermassen. Im OnInit machen wir also einfach einen Aufruf gegen Keycloak, um uns das Benutzerprofil zu laden.

 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();
    }
  }
}

Schauen wir uns anschliessend wieder die Browserkonsole an, so sehen wir, dass es einen neuen Request gegen die Account-Schnittstelle vom Keycloak gibt.

Dieser Request wird mit einem Authentication Header gemacht in dem der JWT Token steht. Wenn wir uns diesen JWT Token einmal kopieren und auf http://jwt.io eingeben, dann können wir alle Informationen sehen, die darin gespeichert werden.

Wir können sehen, dass hier neben Benutzername auch die Rollen mit enthalten sind.

Quarkus

Kümmern wir uns nun um den Backend-Teil. In der GreetingResource gibt es aktuell eine Methode hello. Diese darf von jedem aufgerufen werden. Erstellen wir uns ein paar zusätzliche Methoden, welche nur von authentifizierten Benutzern aufgerufen werden dürfen und zusätzlich noch eine bestimmte Rolle brauchen.

 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";
    }
}

Nun müssen wir in unserer Quarkus Applikation auch noch die OIDC Konfigurationen vornehmen. In der application.properties hinterlegen wir nun die folgenden Konfigurationen.

 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

Als client-id geben wir den Backend-Client (productlist-backend) an, welchen wir weiter oben erstellt haben. Fehlt eigentlich nur noch der Secret. Diesen erhalten wir, wenn wir zurück in die Adminkonsole vom Keycloak gehen und dort in unserem Backend-Client in den Tab “Credentials” gehen. Hier sehen wir einen Secret, welchen wir kopieren und als Secret in den application.properties einfügen.

Nun können wir unseren Quarkus starten und sollten entsprechende Meldungen erhalten, dass wir mit dem OIDC Client verbunden sind.

In unserer Angular Applikation müssen wir uns noch entsprechende Funktionen erstellen um die API im Backend aufrufen zu können. Wir passen also die product-list.component.ts wie folgt an.

 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));
  }
}

Das HttpClientModule muss natürlich auch in der app.module.ts bei den Imports eingefügt werden.

In der product-list.component.html fügen wir noch die entsprechenden Buttons für hello und secure ein.

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>

Nun wollen wir in unserer Angular Applikation mal auf die verschiedenen Buttons klicken und schauen was passiert. Melden wir uns als Erstes mit dem Benutzer cindy an. Wie wir weiter oben sehen, darf Cindy nur share, aber nicht notify.

Wie wir sehen, hat das schon mal funktioniert. Schauen wir uns nun Chris an. Er darf alles ausser share.

Sieht auch gut aus. Als Letztes noch John. Bei ihm sollte jeder Aufruf erfolgreich sein.

Geschafft. Alle Benutzer haben Zugriff wie wir es uns wünschen.

Fazit

Wir haben heute gesehen, wie man mit Quarkus, Angular und Keycloak sehr schnell und sehr einfach eine vollständige Autorisierung mittels OIDC realisieren kann. Natürlich kann man hier noch viel mehr konfigurieren, sodass man grösstmögliche Flexibilität hat. Keycloak erfüllt, obwohl es schon ein Opa in der IT ist, immer noch die Anforderungen an eine moderne Cloud-Umgebung.

Stefan Welsch

Stefan Welsch – Manitu, 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.