mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
bugfix(auth): enhance authentication flow and error handling
- Ensure access token is present during initialization. - Improve error logging for identity claims validation. - Update dependencies for better compatibility.
This commit is contained in:
@@ -117,7 +117,9 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
statusElement.innerHTML = 'Authentifizierung wird durchgeführt...';
|
||||
const strategy = injector.get(LoginStrategy);
|
||||
await strategy.login();
|
||||
return;
|
||||
}
|
||||
|
||||
statusElement.innerHTML = 'Native Container wird initialisiert...';
|
||||
const nativeContainer = injector.get(NativeContainerService);
|
||||
await nativeContainer.init();
|
||||
@@ -141,7 +143,7 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
});
|
||||
|
||||
// Inject tab navigation service to initialize it
|
||||
injector.get(TabNavigationService);
|
||||
injector.get(TabNavigationService).init();
|
||||
} catch (error) {
|
||||
console.error('Error during app initialization', error);
|
||||
laoderElement.remove();
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { inject, Injectable, Injector } from '@angular/core';
|
||||
import { HttpInterceptor, HttpEvent, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
|
||||
import {
|
||||
HttpInterceptor,
|
||||
HttpEvent,
|
||||
HttpHandler,
|
||||
HttpRequest,
|
||||
HttpErrorResponse,
|
||||
} from '@angular/common/http';
|
||||
import { from, NEVER, Observable, throwError } from 'rxjs';
|
||||
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
|
||||
import { catchError, filter, mergeMap, takeUntil, tap } from 'rxjs/operators';
|
||||
import { AuthService, LoginStrategy } from '@core/auth';
|
||||
import { catchError, filter, mergeMap, takeUntil } from 'rxjs/operators';
|
||||
import { LoginStrategy } from '@core/auth';
|
||||
import { IsaLogProvider } from '../providers';
|
||||
import { LogLevel } from '@core/logger';
|
||||
import { injectOnline$ } from '../services/network-status.service';
|
||||
@@ -13,16 +18,17 @@ export class HttpErrorInterceptor implements HttpInterceptor {
|
||||
readonly offline$ = injectOnline$().pipe(filter((online) => !online));
|
||||
readonly injector = inject(Injector);
|
||||
|
||||
constructor(
|
||||
private _modal: UiModalService,
|
||||
private _auth: AuthService,
|
||||
private _isaLogProvider: IsaLogProvider,
|
||||
) {}
|
||||
constructor(private _isaLogProvider: IsaLogProvider) {}
|
||||
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
intercept(
|
||||
req: HttpRequest<any>,
|
||||
next: HttpHandler,
|
||||
): Observable<HttpEvent<any>> {
|
||||
return next.handle(req).pipe(
|
||||
takeUntil(this.offline$),
|
||||
catchError((error: HttpErrorResponse, caught: any) => this.handleError(error)),
|
||||
catchError((error: HttpErrorResponse, caught: any) =>
|
||||
this.handleError(error),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +36,9 @@ export class HttpErrorInterceptor implements HttpInterceptor {
|
||||
if (error.status === 401) {
|
||||
const strategy = this.injector.get(LoginStrategy);
|
||||
|
||||
return from(strategy.login('Sie sind nicht mehr angemeldet')).pipe(mergeMap(() => NEVER));
|
||||
return from(strategy.login('Sie sind nicht mehr angemeldet')).pipe(
|
||||
mergeMap(() => NEVER),
|
||||
);
|
||||
}
|
||||
|
||||
if (!error.url.endsWith('/isa/logging')) {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { coerceArray } from "@angular/cdk/coercion";
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { Config } from "@core/config";
|
||||
import { isNullOrUndefined } from "@utils/common";
|
||||
import { AuthConfig, OAuthService } from "angular-oauth2-oidc";
|
||||
import { JwksValidationHandler } from "angular-oauth2-oidc-jwks";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { coerceArray } from '@angular/cdk/coercion';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Config } from '@core/config';
|
||||
import { isNullOrUndefined } from '@utils/common';
|
||||
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
|
||||
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Storage key for the URL to redirect to after login
|
||||
*/
|
||||
const REDIRECT_URL_KEY = "auth_redirect_url";
|
||||
const REDIRECT_URL_KEY = 'auth_redirect_url';
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthService {
|
||||
private readonly _initialized = new BehaviorSubject<boolean>(false);
|
||||
@@ -26,9 +26,9 @@ export class AuthService {
|
||||
private readonly _oAuthService: OAuthService,
|
||||
) {
|
||||
this._oAuthService.events?.subscribe((event) => {
|
||||
if (event.type === "token_received") {
|
||||
if (event.type === 'token_received') {
|
||||
console.log(
|
||||
"SSO Token Expiration:",
|
||||
'SSO Token Expiration:',
|
||||
new Date(this._oAuthService.getAccessTokenExpiration()),
|
||||
);
|
||||
|
||||
@@ -45,15 +45,15 @@ export class AuthService {
|
||||
|
||||
async init() {
|
||||
if (this._initialized.getValue()) {
|
||||
throw new Error("AuthService is already initialized");
|
||||
throw new Error('AuthService is already initialized');
|
||||
}
|
||||
|
||||
this._authConfig = this._config.get("@core/auth");
|
||||
this._authConfig = this._config.get('@core/auth');
|
||||
|
||||
this._authConfig.redirectUri = window.location.origin;
|
||||
|
||||
this._authConfig.silentRefreshRedirectUri =
|
||||
window.location.origin + "/silent-refresh.html";
|
||||
window.location.origin + '/silent-refresh.html';
|
||||
this._authConfig.useSilentRefresh = true;
|
||||
|
||||
this._oAuthService.configure(this._authConfig);
|
||||
@@ -63,6 +63,10 @@ export class AuthService {
|
||||
|
||||
await this._oAuthService.loadDiscoveryDocumentAndTryLogin();
|
||||
|
||||
if (!this._oAuthService.getAccessToken()) {
|
||||
throw new Error('No access token. User is not authenticated.');
|
||||
}
|
||||
|
||||
this._initialized.next(true);
|
||||
}
|
||||
|
||||
@@ -72,7 +76,7 @@ export class AuthService {
|
||||
|
||||
isIdTokenValid() {
|
||||
console.log(
|
||||
"ID Token Expiration:",
|
||||
'ID Token Expiration:',
|
||||
new Date(this._oAuthService.getIdTokenExpiration()),
|
||||
);
|
||||
return this._oAuthService.hasValidIdToken();
|
||||
@@ -80,7 +84,7 @@ export class AuthService {
|
||||
|
||||
isAccessTokenValid() {
|
||||
console.log(
|
||||
"ACCESS Token Expiration:",
|
||||
'ACCESS Token Expiration:',
|
||||
new Date(this._oAuthService.getAccessTokenExpiration()),
|
||||
);
|
||||
return this._oAuthService.hasValidAccessToken();
|
||||
@@ -107,8 +111,8 @@ export class AuthService {
|
||||
if (isNullOrUndefined(token)) {
|
||||
return null;
|
||||
}
|
||||
const base64Url = token.split(".")[1];
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
|
||||
const encoded = window.atob(base64);
|
||||
return JSON.parse(encoded);
|
||||
@@ -148,7 +152,7 @@ export class AuthService {
|
||||
hasRole(role: string | string[]) {
|
||||
const roles = coerceArray(role);
|
||||
|
||||
const userRoles = this.getClaimByKey("role");
|
||||
const userRoles = this.getClaimByKey('role');
|
||||
|
||||
if (isNullOrUndefined(userRoles)) {
|
||||
return false;
|
||||
@@ -160,8 +164,8 @@ export class AuthService {
|
||||
async refresh() {
|
||||
try {
|
||||
if (
|
||||
this._authConfig.responseType.includes("code") &&
|
||||
this._authConfig.scope.includes("offline_access")
|
||||
this._authConfig.responseType.includes('code') &&
|
||||
this._authConfig.scope.includes('offline_access')
|
||||
) {
|
||||
await this._oAuthService.refreshToken();
|
||||
} else {
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
import { inject, InjectionToken, signal, Signal } from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import z from 'zod';
|
||||
|
||||
export const USER_SUB = new InjectionToken<Signal<string>>(
|
||||
'core.storage.user-sub',
|
||||
{
|
||||
factory: () => {
|
||||
const auth = inject(OAuthService, { optional: true });
|
||||
return signal(auth?.getIdentityClaims()?.['sub'] ?? 'anonymous');
|
||||
const _logger = logger(() => ({
|
||||
context: 'USER_SUB',
|
||||
}));
|
||||
const auth = inject(OAuthService);
|
||||
|
||||
const claims = auth.getIdentityClaims();
|
||||
|
||||
if (!claims || typeof claims !== 'object' || !('sub' in claims)) {
|
||||
const err = new Error(
|
||||
'No valid identity claims found. User is anonymous.',
|
||||
);
|
||||
_logger.error(err.message);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const validation = z.string().safeParse(claims['sub']);
|
||||
|
||||
if (!validation.success) {
|
||||
const err = new Error('Invalid "sub" claim in identity claims.');
|
||||
_logger.error(err.message, { claims });
|
||||
throw err;
|
||||
}
|
||||
|
||||
return signal(validation.data);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -30,11 +30,7 @@ export class TabNavigationService {
|
||||
#tabService = inject(TabService);
|
||||
#title = inject(Title);
|
||||
|
||||
constructor() {
|
||||
this.#initializeNavigationSync();
|
||||
}
|
||||
|
||||
#initializeNavigationSync() {
|
||||
init() {
|
||||
this.#router.events
|
||||
.pipe(filter((event) => event instanceof NavigationEnd))
|
||||
.subscribe((event: NavigationEnd) => {
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
"@ngrx/signals": "^20.0.0",
|
||||
"@ngrx/store": "^20.0.0",
|
||||
"@ngrx/store-devtools": "^20.0.0",
|
||||
"angular-oauth2-oidc": "20.0.0",
|
||||
"angular-oauth2-oidc-jwks": "20.0.0",
|
||||
"angular-oauth2-oidc": "^20.0.2",
|
||||
"angular-oauth2-oidc-jwks": "^20.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
|
||||
Reference in New Issue
Block a user