Only use OIDC token when it's valid

Need to extend the wait logic to the JWT interceptor as well
This commit is contained in:
Amelia 2025-07-03 15:58:47 +02:00
parent f868f5df91
commit 63a5750f28
No known key found for this signature in database
GPG key ID: D6D0ECE365407EAA
5 changed files with 14 additions and 25 deletions

View file

@ -3,20 +3,22 @@ import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/c
import {Observable, switchMap} from 'rxjs'; import {Observable, switchMap} from 'rxjs';
import { AccountService } from '../_services/account.service'; import { AccountService } from '../_services/account.service';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { OidcService } from '../_services/oidc.service';
@Injectable() @Injectable()
export class JwtInterceptor implements HttpInterceptor { export class JwtInterceptor implements HttpInterceptor {
constructor(private accountService: AccountService) {} constructor(private accountService: AccountService, private oidcService: OidcService) { }
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return this.accountService.currentUser$.pipe( return this.accountService.currentUser$.pipe(
take(1), take(1),
switchMap(user => { switchMap(user => {
if (user) { if (user) {
const token = this.oidcService.hasValidToken() ? this.oidcService.token : user.token;
request = request.clone({ request = request.clone({
setHeaders: { setHeaders: {
Authorization: `Bearer ${user.oidcToken ?? user.token}` Authorization: `Bearer ${token}`
} }
}); });
} }

View file

@ -4,9 +4,6 @@ import {Preferences} from './preferences/preferences';
// This interface is only used for login and storing/retrieving JWT from local storage // This interface is only used for login and storing/retrieving JWT from local storage
export interface User { export interface User {
username: string; 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; token: string;
refreshToken: string; refreshToken: string;
roles: string[]; roles: string[];

View file

@ -17,6 +17,7 @@ import {takeUntilDestroyed, toSignal} from "@angular/core/rxjs-interop";
import {Action} from "./action-factory.service"; import {Action} from "./action-factory.service";
import {LicenseService} from "./license.service"; import {LicenseService} from "./license.service";
import {LocalizationService} from "./localization.service"; import {LocalizationService} from "./localization.service";
import {OidcService} from "./oidc.service";
export enum Role { export enum Role {
Admin = 'Admin', Admin = 'Admin',
@ -46,6 +47,7 @@ export const allRoles = [
export class AccountService { export class AccountService {
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly oidcService = inject(OidcService);
private readonly licenseService = inject(LicenseService); private readonly licenseService = inject(LicenseService);
private readonly localizationService = inject(LocalizationService); private readonly localizationService = inject(LocalizationService);
@ -92,10 +94,6 @@ export class AccountService {
}); });
} }
oidcEnabled() {
return this.httpClient.get<boolean>(this.baseUrl + "oidc/enabled");
}
canInvokeAction(user: User, action: Action) { canInvokeAction(user: User, action: Action) {
const isAdmin = this.hasAdminRole(user); const isAdmin = this.hasAdminRole(user);
const canDownload = this.hasDownloadRole(user); const canDownload = this.hasDownloadRole(user);
@ -219,7 +217,6 @@ export class AccountService {
tap((response: User) => { tap((response: User) => {
const user = response; const user = response;
if (user) { if (user) {
user.oidcToken = token;
this.setCurrentUser(user); this.setCurrentUser(user);
} }
}), }),
@ -263,7 +260,7 @@ export class AccountService {
this.licenseService.hasValidLicense().subscribe(); this.licenseService.hasValidLicense().subscribe();
} }
// oidc handles refreshing itself // oidc handles refreshing itself
if (!this.currentUser.oidcToken) { if (!this.oidcService.hasValidToken()) {
this.startRefreshTokenTimer(); this.startRefreshTokenTimer();
} }
} }

View file

@ -11,6 +11,7 @@ import {DashboardUpdateEvent} from "../_models/events/dashboard-update-event";
import {SideNavUpdateEvent} from "../_models/events/sidenav-update-event"; import {SideNavUpdateEvent} from "../_models/events/sidenav-update-event";
import {SiteThemeUpdatedEvent} from "../_models/events/site-theme-updated-event"; import {SiteThemeUpdatedEvent} from "../_models/events/site-theme-updated-event";
import {ExternalMatchRateLimitErrorEvent} from "../_models/events/external-match-rate-limit-error-event"; import {ExternalMatchRateLimitErrorEvent} from "../_models/events/external-match-rate-limit-error-event";
import {OidcService} from "./oidc.service";
export enum EVENTS { export enum EVENTS {
UpdateAvailable = 'UpdateAvailable', UpdateAvailable = 'UpdateAvailable',
@ -146,7 +147,7 @@ export class MessageHubService {
*/ */
public onlineUsers$ = this.onlineUsersSource.asObservable(); public onlineUsers$ = this.onlineUsersSource.asObservable();
constructor() {} constructor(private oidcService: OidcService) {}
/** /**
* Tests that an event is of the type passed * Tests that an event is of the type passed
@ -165,7 +166,7 @@ export class MessageHubService {
createHubConnection(user: User) { createHubConnection(user: User) {
this.hubConnection = new HubConnectionBuilder() this.hubConnection = new HubConnectionBuilder()
.withUrl(this.hubUrl + 'messages', { .withUrl(this.hubUrl + 'messages', {
accessTokenFactory: () => user.oidcToken ?? user.token accessTokenFactory: () => this.oidcService.hasValidToken() ? this.oidcService.token : user.token
}) })
.withAutomaticReconnect() .withAutomaticReconnect()
//.withStatefulReconnect() // Requires signalr@8.0 //.withStatefulReconnect() // Requires signalr@8.0

View file

@ -18,7 +18,6 @@ export class OidcService {
private readonly oauth2 = inject(OAuthService); private readonly oauth2 = inject(OAuthService);
private readonly httpClient = inject(HttpClient); private readonly httpClient = inject(HttpClient);
private readonly accountService = inject(AccountService);
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly toastR = inject(ToastrService); private readonly toastR = inject(ToastrService);
@ -56,17 +55,6 @@ 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 => { this.config().subscribe(oidcSetting => {
if (!oidcSetting.authority) { if (!oidcSetting.authority) {
this._loaded.set(true); this._loaded.set(true);
@ -121,4 +109,8 @@ export class OidcService {
return this.oauth2.getAccessToken(); return this.oauth2.getAccessToken();
} }
hasValidToken() {
return this.oauth2.hasValidAccessToken();
}
} }