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:
Lorenz Hilpert
2025-10-01 14:52:10 +02:00
parent 47a051c214
commit 827828aee2
6 changed files with 260 additions and 226 deletions

View File

@@ -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();

View File

@@ -1,42 +1,50 @@
import { inject, Injectable, Injector } from '@angular/core';
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 { IsaLogProvider } from '../providers';
import { LogLevel } from '@core/logger';
import { injectOnline$ } from '../services/network-status.service';
@Injectable()
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,
) {}
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)),
);
}
handleError(error: HttpErrorResponse): Observable<any> {
if (error.status === 401) {
const strategy = this.injector.get(LoginStrategy);
return from(strategy.login('Sie sind nicht mehr angemeldet')).pipe(mergeMap(() => NEVER));
}
if (!error.url.endsWith('/isa/logging')) {
this._isaLogProvider.log(LogLevel.ERROR, 'Http Error', error);
}
return throwError(error);
}
}
import { inject, Injectable, Injector } from '@angular/core';
import {
HttpInterceptor,
HttpEvent,
HttpHandler,
HttpRequest,
HttpErrorResponse,
} from '@angular/common/http';
import { from, NEVER, Observable, throwError } from 'rxjs';
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';
@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
readonly offline$ = injectOnline$().pipe(filter((online) => !online));
readonly injector = inject(Injector);
constructor(private _isaLogProvider: IsaLogProvider) {}
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),
),
);
}
handleError(error: HttpErrorResponse): Observable<any> {
if (error.status === 401) {
const strategy = this.injector.get(LoginStrategy);
return from(strategy.login('Sie sind nicht mehr angemeldet')).pipe(
mergeMap(() => NEVER),
);
}
if (!error.url.endsWith('/isa/logging')) {
this._isaLogProvider.log(LogLevel.ERROR, 'Http Error', error);
}
return throwError(error);
}
}

View File

@@ -1,174 +1,178 @@
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";
@Injectable({
providedIn: "root",
})
export class AuthService {
private readonly _initialized = new BehaviorSubject<boolean>(false);
get initialized$() {
return this._initialized.asObservable();
}
private _authConfig: AuthConfig;
constructor(
private _config: Config,
private readonly _oAuthService: OAuthService,
) {
this._oAuthService.events?.subscribe((event) => {
if (event.type === "token_received") {
console.log(
"SSO Token Expiration:",
new Date(this._oAuthService.getAccessTokenExpiration()),
);
// Handle redirect after successful authentication
setTimeout(() => {
const redirectUrl = this._getAndClearRedirectUrl();
if (redirectUrl) {
window.location.href = redirectUrl;
}
}, 100);
}
});
}
async init() {
if (this._initialized.getValue()) {
throw new Error("AuthService is already initialized");
}
this._authConfig = this._config.get("@core/auth");
this._authConfig.redirectUri = window.location.origin;
this._authConfig.silentRefreshRedirectUri =
window.location.origin + "/silent-refresh.html";
this._authConfig.useSilentRefresh = true;
this._oAuthService.configure(this._authConfig);
this._oAuthService.tokenValidationHandler = new JwksValidationHandler();
this._oAuthService.setupAutomaticSilentRefresh();
await this._oAuthService.loadDiscoveryDocumentAndTryLogin();
this._initialized.next(true);
}
isAuthenticated() {
return this.isIdTokenValid();
}
isIdTokenValid() {
console.log(
"ID Token Expiration:",
new Date(this._oAuthService.getIdTokenExpiration()),
);
return this._oAuthService.hasValidIdToken();
}
isAccessTokenValid() {
console.log(
"ACCESS Token Expiration:",
new Date(this._oAuthService.getAccessTokenExpiration()),
);
return this._oAuthService.hasValidAccessToken();
}
getToken() {
return this._oAuthService.getAccessToken();
}
getClaims() {
const token = this._oAuthService.getAccessToken();
return this.parseJwt(token);
}
getClaimByKey(key: string) {
const claims = this.getClaims();
if (isNullOrUndefined(claims)) {
return null;
}
return claims[key];
}
parseJwt(token: string) {
if (isNullOrUndefined(token)) {
return null;
}
const base64Url = token.split(".")[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const encoded = window.atob(base64);
return JSON.parse(encoded);
}
/**
* Saves the URL to redirect to after successful login
*/
_saveRedirectUrl(): void {
localStorage.setItem(REDIRECT_URL_KEY, window.location.href);
}
/**
* Gets and clears the saved redirect URL
*/
_getAndClearRedirectUrl(): string | null {
const url = localStorage.getItem(REDIRECT_URL_KEY);
localStorage.removeItem(REDIRECT_URL_KEY);
return url;
}
login() {
this._saveRedirectUrl();
this._oAuthService.initLoginFlow();
}
setKeyCardToken(token: string) {
this._oAuthService.customQueryParams = {
temp_token: token,
};
}
async logout() {
await this._oAuthService.revokeTokenAndLogout();
}
hasRole(role: string | string[]) {
const roles = coerceArray(role);
const userRoles = this.getClaimByKey("role");
if (isNullOrUndefined(userRoles)) {
return false;
}
return roles.every((r) => userRoles.includes(r));
}
async refresh() {
try {
if (
this._authConfig.responseType.includes("code") &&
this._authConfig.scope.includes("offline_access")
) {
await this._oAuthService.refreshToken();
} else {
await this._oAuthService.silentRefresh();
}
} catch (error) {
console.error(error);
}
}
}
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';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private readonly _initialized = new BehaviorSubject<boolean>(false);
get initialized$() {
return this._initialized.asObservable();
}
private _authConfig: AuthConfig;
constructor(
private _config: Config,
private readonly _oAuthService: OAuthService,
) {
this._oAuthService.events?.subscribe((event) => {
if (event.type === 'token_received') {
console.log(
'SSO Token Expiration:',
new Date(this._oAuthService.getAccessTokenExpiration()),
);
// Handle redirect after successful authentication
setTimeout(() => {
const redirectUrl = this._getAndClearRedirectUrl();
if (redirectUrl) {
window.location.href = redirectUrl;
}
}, 100);
}
});
}
async init() {
if (this._initialized.getValue()) {
throw new Error('AuthService is already initialized');
}
this._authConfig = this._config.get('@core/auth');
this._authConfig.redirectUri = window.location.origin;
this._authConfig.silentRefreshRedirectUri =
window.location.origin + '/silent-refresh.html';
this._authConfig.useSilentRefresh = true;
this._oAuthService.configure(this._authConfig);
this._oAuthService.tokenValidationHandler = new JwksValidationHandler();
this._oAuthService.setupAutomaticSilentRefresh();
await this._oAuthService.loadDiscoveryDocumentAndTryLogin();
if (!this._oAuthService.getAccessToken()) {
throw new Error('No access token. User is not authenticated.');
}
this._initialized.next(true);
}
isAuthenticated() {
return this.isIdTokenValid();
}
isIdTokenValid() {
console.log(
'ID Token Expiration:',
new Date(this._oAuthService.getIdTokenExpiration()),
);
return this._oAuthService.hasValidIdToken();
}
isAccessTokenValid() {
console.log(
'ACCESS Token Expiration:',
new Date(this._oAuthService.getAccessTokenExpiration()),
);
return this._oAuthService.hasValidAccessToken();
}
getToken() {
return this._oAuthService.getAccessToken();
}
getClaims() {
const token = this._oAuthService.getAccessToken();
return this.parseJwt(token);
}
getClaimByKey(key: string) {
const claims = this.getClaims();
if (isNullOrUndefined(claims)) {
return null;
}
return claims[key];
}
parseJwt(token: string) {
if (isNullOrUndefined(token)) {
return null;
}
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const encoded = window.atob(base64);
return JSON.parse(encoded);
}
/**
* Saves the URL to redirect to after successful login
*/
_saveRedirectUrl(): void {
localStorage.setItem(REDIRECT_URL_KEY, window.location.href);
}
/**
* Gets and clears the saved redirect URL
*/
_getAndClearRedirectUrl(): string | null {
const url = localStorage.getItem(REDIRECT_URL_KEY);
localStorage.removeItem(REDIRECT_URL_KEY);
return url;
}
login() {
this._saveRedirectUrl();
this._oAuthService.initLoginFlow();
}
setKeyCardToken(token: string) {
this._oAuthService.customQueryParams = {
temp_token: token,
};
}
async logout() {
await this._oAuthService.revokeTokenAndLogout();
}
hasRole(role: string | string[]) {
const roles = coerceArray(role);
const userRoles = this.getClaimByKey('role');
if (isNullOrUndefined(userRoles)) {
return false;
}
return roles.every((r) => userRoles.includes(r));
}
async refresh() {
try {
if (
this._authConfig.responseType.includes('code') &&
this._authConfig.scope.includes('offline_access')
) {
await this._oAuthService.refreshToken();
} else {
await this._oAuthService.silentRefresh();
}
} catch (error) {
console.error(error);
}
}
}