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