Keycloak + Protractor E2E Test

Keycloak ist eine charmante Authentifizierungs-Lösung die sich dank keycloak-angular relativ Problemlos in der eigenen Anwendung verankern lässt. Im Arbeitsalltag stolpert man dann aber in Kombination mit E2E-Tests über das ein oder andere Problem…

Protractor ist cool, kümmert es sich doch normalerweise um all die asyncronen HTTP-Calls und Timer die in unserer Anwendung ablaufen. So wird der Test den wir deklarieren erst dann durchgeführt wenn z.B. die HTTP Abfrage + Rendering auch vollständig durchlaufen ist. Wenn wir Keycloak einsetzen scheitert dieses Vorgehen leider mit einem Timeout:

Failed: Angular could not be found on the page http://localhost:4200/.

If this is not an Angular application, you may need to turn off waiting for Angular.

Recht hat er ja auch irgendwie. Unsere Anwendung wird den User in aller Regel auf die Login-Seite von Keycloak weiter leiten. Was wir nun finden werden sind Ratschläge die Syncronisation zu deaktivieren:

  browser.waitForAngularEnabled(false);

Soweit so gut, nach erfolgreichem Login einfach wieder aktivieren und… nein doch nicht.

Keycloak blockiert bei diesem Vorgehen jegliche weiteren Tests, die somit auf einen Timeout laufen.

Nun könnten wir natürlich die Synchronisation wie oben zu sehen einfach deaktiviert lassen. Das wird uns allerdings nur wenig Glücklich machen, müssen wir doch so an sehr sehr vielen Stellen unnötige Prüfungen und „sleeps“ verwenden, da wir nun selber auf die Abarbeitung der Asyncronen Calls reagieren und/oder warten müssen. Im entsprechenden BugTicket finden wir aber die Idee Keycloak einfach außerhalb von Angular zu initialisieren:

https://github.com/mauriciovigolo/keycloak-angular/issues/73#issuecomment-393967726

Um das bei Angular-Start durch zu führen + in unserem Beispiel sogar noch die Konfiguration für Keycloak von externer Quelle zu ziehen könnte das so aussehen:


const keycloakService = new KeycloakService();

@NgModule({
  ...
  providers: [
    {
      provide: KeycloakService,
      useValue: keycloakService,
    },
    {
      provide: APP_INITIALIZER,
      useFactory: onAppInit,
      deps: [KeycloakConfigControllerService],
      multi: true,
    },
  ]
})
export class AppModule implements DoBootstrap {
  constructor(public ngZone: NgZone) {}

  ngDoBootstrap(appRef: ApplicationRef) {
    this.initWithKeycloak().then(() => appRef.bootstrap(AppComponent));
  }

  initWithKeycloak() {
    return new Promise((resolve) => {
      this.ngZone.runOutsideAngular(() => {
        keycloakService
          .init({
          // normal init here
          })
          .then(() => {
            resolve();
          })
      });
    });
  }
}

export function onAppInit(kcConfig: KeycloakConfigControllerService) {
  return () =>
    new Promise<any>((resolve) => {
      kcConfig.getKeycloakConfig().subscribe((r) => {
        Object.assign(config, r);
        resolve();
      });
    });
}

Spring Boot Swagger Keycloak

Spring Boot + Swagger sind ein eingespieltes Team, um die eigenen Rest-APIs als OpenApi Beschreibung zur Verfügung zu stellen. Dafür bedarf es kaum Aufwand, reicht es doch die entsprechenden Abhängigkeiten per Maven zu definieren (für OpenApi 3.0 : org.springdoc, springdoc-openapi-ui).

Klar. In aller Regel wollen wir zusätzliche Information für unsere Schnittstellen bereitstellen und spätestens, wenn unsere Rest-Aufrufe eine Authentifizierung benötigen bedarf es zusätzlicher Konfiguration um z.B. die swagger-ui nutzen zu können. Setzen wir dabei auf eine OAuth2 Variante, z.B. mit Keycloak könnte die einfachste der Konfigurationen so aussehen:

@Configuration
public class OpenApiConfig {

        @Value("${app.version}")
        private String appVersion;

        @Value("${app.name}")
        private String appName;

        @Value("${app.security.auth-server-url}")
        private String authServerUrl;

        @Value("${app.security.realm}")
        private String realm;

        @Value("${app.security.client-ui}")
        private String client;

        @Bean
        public OpenAPI customOpenAPI() {
                OAuthFlows flows = new OAuthFlows();
                OAuthFlow flow = new OAuthFlow();

                flow.setAuthorizationUrl(authServerUrl + "/realms/" + realm + "/protocol/openid-connect/auth");

                Scopes scopes = new Scopes();
                flow.setScopes(scopes);
                flows = flows.implicit(flow);

                return new OpenAPI()
                                .components(new Components().addSecuritySchemes("keycloak",
                                                new SecurityScheme().type(SecurityScheme.Type.OAUTH2).flows(flows)))
                                .info(new Info().title(appName)
                                                .version(appVersion))
                                .addSecurityItem(new SecurityRequirement().addList("keycloak",
                                                Arrays.asList("read", "write")));
        }

}

Angular-CLI Proxy + JEE

In vielen kleinen bis mittleren Projekten wird unsere spätere Angular-Anwendung zusammen mit dem JEE Backend auf ein und demselben Server laufen. Das vereinfacht nicht nur den Betrieb sondern wir müssen uns auch über CORS-Richtlinien keine Gedanken machen.

Doch zumindest während der Entwicklungszeit wollen wir in aller Regel mit unterschiedlichen Servern arbeiten, bietet uns die Angular-CLI mit seinem eigenen kleinen Dev-Server die perfekte Lösung um unsere Anwendung während der Entwicklung zu begutachten. Damit stolpern wir aber über das bereits erwähnte CORS-Thema. Anstatt nun das Test- und oder Entwicklungs-Backend zurecht zu konfigurieren bietet Angular selbst eine einfache und transparente Lösung. Der Angular-Dev-Server lässt sich ganz einfach mittels einer Proxy-Konfiguration ergänzen, sodass alle Anfragen an eine bestimmten URL um geroutet werden. Damit sparen wir uns zum einen unterschiedliche Environment-Konfigurationen (da unsere APIs nun für die Angular Anwendung immer unter z.B. /api/… erreichbar sindund nicht um den Backend-Server ergänzt werden muss (http://localhost:8080/api/..)) und vor allem umgehen wir mit dieser Umleitung die Security-CORS-Regeln die der Browser anwenden würde.

{
  "/api/*": {
    "target": "http://localhost:8080/api",
    "secure": false,
    "logLevel": "debug",
    "changeOrigin": true,
    "pathRewrite": {
      "^/api": ""
    }
  }
}

proxy.conf.json

Diese Konfiguration lässt sich nun ganz einfach per Kommandozeile beim Starten des Dev-Servers ergänzen

"ng serve --proxy-config proxy.conf.json"
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
: Compiled successfully.
[HPM] Rewriting path from "/api/user" to "/user"
[HPM] GET /api/user ~> http://localhost:8080/api

Angular Theming, Library-Komponenten

In folgendem Artikel: Angular Material Theming haben wir uns bereits angesehen wie wir eigene Themes für Angular Material schreiben. Auch haben wir gesehen wie wir die so definierten Farb-Paletten und Typografie-Elemente in unseren eigenen Komponenten einsetzen können. Das war ganz einfach, durch eine kluge Trennung der Style-Definitionen und einen Import des aktuell verwendeten Themes. Doch was wenn unsere Komponenten nicht Teil unseres Projektes sind, sondern in einem separaten Projekt / eigenen Komponente-Bibliothek ausgelagert sind? Hier haben wir ja keinen direkten Zugriff auf das aktuelle Theme, schließlich wird das ja erst durch die konkrete Anwendung festgelegt.

Unsere eigene Komponenten Bibliothek muss also analog zum Vorgehen bei Angular Materials durch entsprechende Mixin-Funktionen mit dem aktuellen Theme versorgt werden. Um das zu tun bedarf es einiger Vorarbeit innerhalb unserer Komponenten.

Zusätzlich zu der üblichen Komponenten-Style-Datei implementieren wir eine theme.scss für unsere Bibliothek. Diese Datei enthält alle Regeln mit Theme-spezifischen Eigenschaften für die in der Bibliothek enthaltenden Komponenten. Um diese später in den konkreten Anwendungen zu definieren benötigen wir entsprechende Mixin-Funktionen:

@import '~@angular/material/theming';

@mixin ng-lib-theme($theme) {
  $primary: map-get($theme, primary);
  $accent: map-get($theme, accent);

  lib-panel div.body {
    border-top-color: mat-color($primary, 400);
    border-bottom-color: mat-color($accent, 400);
  }
}

@mixin ng-lib-typography($config) {
  lib-panel div.body {
    font-size: mat-font-size($config, title);
  }
}

Wie zu sehen definieren wir jeweils eine eigene Mixin-Funktion für das Theme und Typografie die mit einem entsprechenden Parameter versorgt werden (Theme-Objekt bzw. Typografie-Konfig). Aus dem Theme können wir dann z.B. mittels „map-get“ die einzelnen Paletten extrahieren (primary, accent, warning) die dann wiederum dazu genutzt werden können um konkrete Farbwerte zu ermitteln. Diese werden dann wie bereits gesehen in einzelnen CSS-Regeln verwendet um Theme spezifische Ergänzungen zu den Komponente-Styles zu liefern.

Innerhalb er konkreten Anwendung ähnelt die Registrierung dann erschreckend der von Angular Materials:

@include ng-lib-theme($theme02);
@include ng-lib-typography($custom-typography);

Zugegeben mit „ein bisschen“ CSS ist es hier nicht getan. Auch um die Verwendung von SASS als CSS-Präprozessor kommen wir an dieser Stelle nicht mehr herum. Der ist aber ohnehin zu empfehlen, auch wenn man im eigenen Projekt die Möglichkeiten von SASS vielleicht gar nicht ausschöpft oder sogar nur Kernfunktionen wie die Definition von Variablen nutzt. Ob die Anbindung der eigenen Komponenten an das Material Theming, wie zu letzt gesehen, notwenig ist hängt sicher von der Anwendung selbst ab. Insbesondere bei internen Komponenten-Bibliotheken die keinerlei dynamischen Theming-Funktionalitäten benötigen erfüllt eine zentrale Library mit festgelegtem Look&Feel ( in unserem Beispiel wäre das die theme.scss und variables.scss) sicherlich auch seinen Zweck.

GitHub? Klaro.