diff --git a/UI/Web/src/app/_interceptors/jwt.interceptor.ts b/UI/Web/src/app/_interceptors/jwt.interceptor.ts index 4cc9cf5f1..74f5243d3 100644 --- a/UI/Web/src/app/_interceptors/jwt.interceptor.ts +++ b/UI/Web/src/app/_interceptors/jwt.interceptor.ts @@ -3,22 +3,20 @@ import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/c import {Observable, switchMap} from 'rxjs'; import { AccountService } from '../_services/account.service'; import { take } from 'rxjs/operators'; -import { OidcService } from '../_services/oidc.service'; @Injectable() export class JwtInterceptor implements HttpInterceptor { - constructor(private accountService: AccountService, private oidcService: OidcService) { } + constructor(private accountService: AccountService) {} intercept(request: HttpRequest, next: HttpHandler): Observable> { return this.accountService.currentUser$.pipe( take(1), switchMap(user => { if (user) { - const token = this.oidcService.hasValidToken() ? this.oidcService.token : user.token; request = request.clone({ setHeaders: { - Authorization: `Bearer ${token}` + Authorization: `Bearer ${user.oidcToken ?? user.token}` } }); } diff --git a/UI/Web/src/app/_models/user.ts b/UI/Web/src/app/_models/user.ts index 29bef2fbc..5a8cb70b0 100644 --- a/UI/Web/src/app/_models/user.ts +++ b/UI/Web/src/app/_models/user.ts @@ -4,6 +4,9 @@ import {Preferences} from './preferences/preferences'; // This interface is only used for login and storing/retrieving JWT from local storage export interface User { username: string; + // This is set by the oidc service, will always take precedence over the Kavita generated token + // When set, the refresh logic for the Kavita token will not run + oidcToken: string; token: string; refreshToken: string; roles: string[]; diff --git a/UI/Web/src/app/_services/account.service.ts b/UI/Web/src/app/_services/account.service.ts index f903f8ad0..52a618fc5 100644 --- a/UI/Web/src/app/_services/account.service.ts +++ b/UI/Web/src/app/_services/account.service.ts @@ -17,7 +17,6 @@ import {takeUntilDestroyed, toSignal} from "@angular/core/rxjs-interop"; import {Action} from "./action-factory.service"; import {LicenseService} from "./license.service"; import {LocalizationService} from "./localization.service"; -import {OidcService} from "./oidc.service"; export enum Role { Admin = 'Admin', @@ -47,7 +46,6 @@ export const allRoles = [ export class AccountService { private readonly destroyRef = inject(DestroyRef); - private readonly oidcService = inject(OidcService); private readonly licenseService = inject(LicenseService); private readonly localizationService = inject(LocalizationService); @@ -94,6 +92,10 @@ export class AccountService { }); } + oidcEnabled() { + return this.httpClient.get(this.baseUrl + "oidc/enabled"); + } + canInvokeAction(user: User, action: Action) { const isAdmin = this.hasAdminRole(user); const canDownload = this.hasDownloadRole(user); @@ -217,6 +219,7 @@ export class AccountService { tap((response: User) => { const user = response; if (user) { + user.oidcToken = token; this.setCurrentUser(user); } }), @@ -260,7 +263,7 @@ export class AccountService { this.licenseService.hasValidLicense().subscribe(); } // oidc handles refreshing itself - if (!this.oidcService.hasValidToken()) { + if (!this.currentUser.oidcToken) { this.startRefreshTokenTimer(); } } diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index daeae4c99..ab03fbb0a 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -11,7 +11,6 @@ import {DashboardUpdateEvent} from "../_models/events/dashboard-update-event"; import {SideNavUpdateEvent} from "../_models/events/sidenav-update-event"; import {SiteThemeUpdatedEvent} from "../_models/events/site-theme-updated-event"; import {ExternalMatchRateLimitErrorEvent} from "../_models/events/external-match-rate-limit-error-event"; -import {OidcService} from "./oidc.service"; export enum EVENTS { UpdateAvailable = 'UpdateAvailable', @@ -147,7 +146,7 @@ export class MessageHubService { */ public onlineUsers$ = this.onlineUsersSource.asObservable(); - constructor(private oidcService: OidcService) {} + constructor() {} /** * Tests that an event is of the type passed @@ -166,7 +165,7 @@ export class MessageHubService { createHubConnection(user: User) { this.hubConnection = new HubConnectionBuilder() .withUrl(this.hubUrl + 'messages', { - accessTokenFactory: () => this.oidcService.hasValidToken() ? this.oidcService.token : user.token + accessTokenFactory: () => user.oidcToken ?? user.token }) .withAutomaticReconnect() //.withStatefulReconnect() // Requires signalr@8.0 diff --git a/UI/Web/src/app/_services/oidc.service.ts b/UI/Web/src/app/_services/oidc.service.ts index 04dfe3b4b..4710e5cbe 100644 --- a/UI/Web/src/app/_services/oidc.service.ts +++ b/UI/Web/src/app/_services/oidc.service.ts @@ -18,6 +18,7 @@ export class OidcService { private readonly oauth2 = inject(OAuthService); private readonly httpClient = inject(HttpClient); + private readonly accountService = inject(AccountService); private readonly destroyRef = inject(DestroyRef); private readonly toastR = inject(ToastrService); @@ -55,6 +56,17 @@ export class OidcService { }); } + this.oauth2.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => { + if (event.type !== "token_refreshed" && event.type != 'token_received') return; + + this.accountService.currentUser$.pipe(take(1)).subscribe(user => { + if (!user) return; // Don't update tokens when we're not logged in. But what's going on? + + // TODO: Do we need to refresh the SignalR connection here? + user.oidcToken = this.token; + }); + }); + this.config().subscribe(oidcSetting => { if (!oidcSetting.authority) { this._loaded.set(true); @@ -109,8 +121,4 @@ export class OidcService { return this.oauth2.getAccessToken(); } - hasValidToken() { - return this.oauth2.hasValidAccessToken(); - } - }