diff --git a/webapp/src/app/core/security/keycloak.service.ts b/webapp/src/app/core/security/keycloak.service.ts index cfd61d7c..168f3984 100644 --- a/webapp/src/app/core/security/keycloak.service.ts +++ b/webapp/src/app/core/security/keycloak.service.ts @@ -19,7 +19,6 @@ export interface UserProfile { export class KeycloakService { _keycloak: Keycloak | undefined; profile: UserProfile | undefined; - tokenRefreshInterval = 60; // in seconds get keycloak() { if (!this._keycloak) { @@ -42,29 +41,35 @@ export class KeycloakService { if (!authenticated) { return authenticated; } + // Load user profile this.profile = (await this.keycloak.loadUserInfo()) as unknown as UserProfile; this.profile.token = this.keycloak.token || ''; this.profile.roles = this.keycloak.realmAccess?.roles || []; - // Check refresh token expiry - setInterval(() => { - this.updateToken(); - }, this.tokenRefreshInterval * 1000); - return true; } - private async updateToken() { + /** + * Update access token if it is about to expire or has expired + * This is independent from the silent check sso or refresh token validity. + * @returns + */ + async updateToken() { + if (!this.keycloak.isTokenExpired(60)) { + return false; + } try { - // Try to refresh token if it's about to expire - const refreshed = await this.keycloak.updateToken(this.tokenRefreshInterval + 10); + // Try to refresh token + const refreshed = await this.keycloak.updateToken(60); if (refreshed) { this.profile!.token = this.keycloak.token || ''; } + return refreshed; } catch (error) { console.error('Failed to refresh token:', error); // Redirect to login if refresh fails await this.keycloak.login(); + return false; } } diff --git a/webapp/src/app/core/security/security-interceptor.ts b/webapp/src/app/core/security/security-interceptor.ts index 1e0cdad8..166a2a77 100644 --- a/webapp/src/app/core/security/security-interceptor.ts +++ b/webapp/src/app/core/security/security-interceptor.ts @@ -1,19 +1,26 @@ import { HttpInterceptorFn } from '@angular/common/http'; import { inject } from '@angular/core'; import { SecurityStore } from './security-store.service'; +import { from, switchMap } from 'rxjs'; export const securityInterceptor: HttpInterceptorFn = (req, next) => { const keycloakService = inject(SecurityStore); - const bearer = keycloakService.user()?.bearer; + // update access token to prevent 401s + return from(keycloakService.updateToken()).pipe( + switchMap(() => { + // add bearer token to request + const bearer = keycloakService.user()?.bearer; - if (!bearer) { - return next(req); - } + if (!bearer) { + return next(req); + } - return next( - req.clone({ - headers: req.headers.set('Authorization', `Bearer ${bearer}`) + return next( + req.clone({ + headers: req.headers.set('Authorization', `Bearer ${bearer}`) + }) + ); }) ); }; diff --git a/webapp/src/app/core/security/security-store.service.ts b/webapp/src/app/core/security/security-store.service.ts index 28b2003b..20add32c 100644 --- a/webapp/src/app/core/security/security-store.service.ts +++ b/webapp/src/app/core/security/security-store.service.ts @@ -53,4 +53,14 @@ export class SecurityStore { async signOut() { await this.keycloakService.logout(); } + + async updateToken() { + await this.keycloakService.updateToken(); + // update bearer in user with new token + const user = this.user(); + if (user && this.keycloakService.profile) { + user.bearer = this.keycloakService.profile.token; + this.user.set(user); + } + } }