I have an angular app (and a nestjs backend) and I try to handle user login with the help of keycloak. As the buzzword rich question title states I have kind of a "weird" architecture.
custom element
with the help of angular (works)standalone component
(works)keycloak-angular
in main.ts to do the login (works, I get a token, username, roles,...)HttpClient
for the requests of that component (custom element) to its backend (requests do work, but the bearer token is not added)According to the readme of keycloak-angular:
"By default, all HttpClient requests will add the Authorization header"
Also when I add a shouldAddToken
function to the KeycloakOptions
that I set in the keycloakService.init
method, this function is never called.
I thought, maybe the HttpClient in the component is another instance than the one in main.ts but I checked it and it is the same instance. (have set (httpService as any).mytest = 1;
in main.ts and the httpService in the component had that parameter).
Any idea what I could miss?
The relevant code in main.ts
(async () => {
// inspired by https://www.angulararchitects.io/aktuelles/angular-elements-web-components-with-standalone-components/
const app = await createApplication({
providers: [
provideHttpClient(),
KeycloakService,
{
// I guess this should not be necessary, but I tryed anyway
provide: HTTP_INTERCEPTORS,
useClass: KeycloakBearerInterceptor,
multi: true,
deps: [KeycloakService]
},
],
});
try {
await initializeKeycloak(app);
} catch (err: any) {
console.error('Seems like there was a problem with the login', err);
return;
}
// register my root component as CustomElement
customElements.define(
'my-component',
createCustomElement(AppComponent, { injector: app.injector })
);
})();
async function initializeKeycloak(app: ApplicationRef) {
const keycloakService = app.injector.get(KeycloakService);
return keycloakService.init({
// keycloak address and client data coming from environment file
config: environment.keycloak.config,
/* without this getUsername() returns the Error "User not logged in or user profile was not loaded." */
loadUserProfileAtStartUp: true,
initOptions: {
enableLogging: true,
onLoad: 'login-required',
// someone on the web had the same problem (no auth header) and fixed it with this (not sure if this is relevant, it does not help in my case)
silentCheckSsoRedirectUri:
window.location.origin + '/assets/silent-check-sso.html',
},
shouldAddToken: (request) => {
// is never logged
console.log('XXXXXXXXXXX addToken?', request);
return true;
}
}).then(async loggedIn => {
if (loggedIn) {
// returns sane data
console.log("loggedIn: ", {
loggedIn,
username: keycloakService.getUsername(),
roles: keycloakService.getUserRoles().join(', '),
jwt: jwt_decode(await keycloakService.getToken())
});
// returns true
console.log("keycloakService.enableBearerInterceptor", keycloakService.enableBearerInterceptor);
} else {
console.log(`keycloak.init returned ` + loggedIn);
}
return loggedIn;
});
}
Update
Two things I am not sure about and that could cause the Keycloak Interceptor to not add the bearer token:
Maybe they are not added if the domain is different?
Maybe the HttpInterceptor needs to be added for every module that does http request?! Already tried to add the following (without success) to one of the modules that is imported by my AppModule and does requests via the HttpClient.
{
provide: HTTP_INTERCEPTORS,
useClass: KeycloakBearerInterceptor,
multi: true,
...
The HttpClient is the same instance everywhere (as written earlier).
Problem solved.
As I use a standalone component I can't add the HttpClientModule to the imports array of the AppModule. The new way of doing this is the method
provideHttpClient
.
Instead of adding constructs like this to your providers array
{
// I guess this should not be necessary, but I tryed anyway
provide: HTTP_INTERCEPTORS,
useClass: KeycloakBearerInterceptor,
multi: true,
deps: [KeycloakService]
},
you can add the new "functional interceptors" to the provideHttpClient
like so
provideHttpClient(
withInterceptors([ requestInterceptor ])
)
where requestInterceptor is a simple function instead of a service
function requestInterceptor(
req: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> {
...
}
You can also add "Legacy Interceptors" like before BUT you have to tell the new API to search and inject them in your providers via withInterceptorsFromDi()
. And that's what was missing in my code.
Looks like this now
const app = await createApplication({
providers: [
...,
provideHttpClient(
withInterceptors([
// just testing the new way of intercepting
requestInterceptor
]),
withInterceptorsFromDi()
),
KeycloakAngularModule,
KeycloakService,
KeycloakBearerInterceptor,
{
provide: HTTP_INTERCEPTORS,
useClass: KeycloakBearerInterceptor,
multi: true,
deps: [KeycloakService]
},
{
// testing own legacy interceptor
provide: HTTP_INTERCEPTORS,
useClass: LoggingInterceptor,
multi: true
},
],
});
All 3 interceptors work.
This article was my knight in shining armor