WEB-487: Add OAuth 2.1 PKCE flow (#2864)
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
/** Angular Imports */
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
||||
|
||||
/** rxjs Imports */
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
/** 3rd party Imports */
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
/** Custom Services */
|
||||
import { AlertService } from '../alert/alert.service';
|
||||
|
||||
@@ -18,7 +19,7 @@ import { environment } from '../../../environments/environment';
|
||||
/** Custom Models */
|
||||
import { LoginContext } from './login-context.model';
|
||||
import { Credentials } from './credentials.model';
|
||||
import { OAuth2Token } from './o-auth2-token.model';
|
||||
import { getOAuthConfig, getActiveAuthMode, AuthMode } from './oauth.config';
|
||||
|
||||
/**
|
||||
* Authentication workflow.
|
||||
@@ -28,17 +29,24 @@ export class AuthenticationService {
|
||||
private http = inject(HttpClient);
|
||||
private alertService = inject(AlertService);
|
||||
private authenticationInterceptor = inject(AuthenticationInterceptor);
|
||||
private oauthService = inject(OAuthService);
|
||||
|
||||
/**
|
||||
* Updates the password for the specified user.
|
||||
* @param {string} userId Target user identifier.
|
||||
* @param {*} passwordObj Payload containing the new password fields.
|
||||
* @returns Updated user response observable.
|
||||
*/
|
||||
changePassword(userId: string, passwordObj: any) {
|
||||
return this.http.put(`/users/${userId}`, passwordObj);
|
||||
}
|
||||
// User logged in boolean
|
||||
private userLoggedIn: boolean;
|
||||
|
||||
private userLoggedIn$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
/** Observable that emits authentication state changes. */
|
||||
public readonly isAuthenticated$ = this.userLoggedIn$.asObservable();
|
||||
|
||||
/** Denotes whether the user credentials should persist through sessions. */
|
||||
private rememberMe: boolean;
|
||||
private rememberMe = false;
|
||||
/**
|
||||
* Denotes the type of storage:
|
||||
*
|
||||
@@ -46,17 +54,15 @@ export class AuthenticationService {
|
||||
*
|
||||
* Local Storage: User credentials should persist through sessions.
|
||||
*/
|
||||
private storage: any;
|
||||
/** User credentials. */
|
||||
|
||||
private storage: Storage = sessionStorage;
|
||||
private credentials: Credentials;
|
||||
private dialogShown = false;
|
||||
private authMode: AuthMode = AuthMode.Basic;
|
||||
|
||||
/** Key to store credentials in storage. */
|
||||
private credentialsStorageKey = 'mifosXCredentials';
|
||||
/** Key to store oauth token details in storage. */
|
||||
private oAuthTokenDetailsStorageKey = 'mifosXOAuthTokenDetails';
|
||||
private readonly credentialsStorageKey = 'mifosXCredentials';
|
||||
/** Key to store two factor authentication token in storage. */
|
||||
private twoFactorAuthenticationTokenStorageKey = 'mifosXTwoFactorAuthenticationToken';
|
||||
private readonly twoFactorAuthenticationTokenStorageKey = 'mifosXTwoFactorAuthenticationToken';
|
||||
|
||||
/**
|
||||
* Initializes the type of storage and authorization headers depending on whether
|
||||
@@ -64,135 +70,173 @@ export class AuthenticationService {
|
||||
* @param {HttpClient} http Http Client to send requests.
|
||||
* @param {AlertService} alertService Alert Service.
|
||||
* @param {AuthenticationInterceptor} authenticationInterceptor Authentication Interceptor.
|
||||
* @param {OAuthService} oauthService OAuth Service.
|
||||
*/
|
||||
constructor() {
|
||||
const authenticationInterceptor = this.authenticationInterceptor;
|
||||
this.authMode = getActiveAuthMode();
|
||||
|
||||
this.userLoggedIn = false;
|
||||
this.rememberMe = false;
|
||||
this.storage = sessionStorage;
|
||||
const savedCredentials = JSON.parse(
|
||||
sessionStorage.getItem(this.credentialsStorageKey) || localStorage.getItem(this.credentialsStorageKey)
|
||||
);
|
||||
if (savedCredentials) {
|
||||
if (savedCredentials.rememberMe) {
|
||||
this.rememberMe = true;
|
||||
this.storage = localStorage;
|
||||
if (this.authMode !== AuthMode.Basic) {
|
||||
this.initializeOAuthService();
|
||||
}
|
||||
|
||||
this.restoreSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OAuth service with runtime settings and hooks up token listeners.
|
||||
*/
|
||||
private initializeOAuthService(): void {
|
||||
this.oauthService.configure(getOAuthConfig());
|
||||
const oauthStorage = environment.enableRememberMe ? localStorage : sessionStorage;
|
||||
this.oauthService.setStorage(oauthStorage);
|
||||
this.oauthService.setupAutomaticSilentRefresh();
|
||||
|
||||
this.oauthService.events.subscribe((event) => {
|
||||
if (event.type === 'token_received' || event.type === 'token_refreshed') {
|
||||
this.updateCredentialsToken();
|
||||
}
|
||||
const twoFactorAccessToken = JSON.parse(this.storage.getItem(this.twoFactorAuthenticationTokenStorageKey));
|
||||
if (environment.oauth.enabled) {
|
||||
this.refreshOAuthAccessToken();
|
||||
} else {
|
||||
authenticationInterceptor.setAuthorizationToken(savedCredentials.base64EncodedAuthenticationKey);
|
||||
});
|
||||
|
||||
this.cleanupLegacyStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores persisted credentials/tokens from storage and rehydrates the session state.
|
||||
*/
|
||||
private restoreSession(): void {
|
||||
const savedCredentials = this.getSavedCredentials();
|
||||
if (!savedCredentials) return;
|
||||
|
||||
if (savedCredentials.rememberMe) {
|
||||
this.rememberMe = true;
|
||||
this.storage = localStorage;
|
||||
this.oauthService.setStorage(this.storage);
|
||||
}
|
||||
|
||||
if (this.authMode !== AuthMode.Basic) {
|
||||
// OAuth2/OIDC: Use angular-oauth2-oidc library for token management
|
||||
if (this.oauthService.hasValidAccessToken()) {
|
||||
this.authenticationInterceptor.setAuthorizationToken(this.oauthService.getAccessToken());
|
||||
this.userLoggedIn$.next(true);
|
||||
} else if (this.oauthService.getRefreshToken()) {
|
||||
this.oauthService
|
||||
.refreshToken()
|
||||
.then(() => this.userLoggedIn$.next(true))
|
||||
.catch(() => this.logout().subscribe());
|
||||
}
|
||||
if (twoFactorAccessToken) {
|
||||
authenticationInterceptor.setTwoFactorAccessToken(twoFactorAccessToken.token);
|
||||
} else {
|
||||
// Basic Auth
|
||||
this.authenticationInterceptor.setAuthorizationToken(savedCredentials.base64EncodedAuthenticationKey);
|
||||
|
||||
const twoFactorToken = JSON.parse(this.storage.getItem(this.twoFactorAuthenticationTokenStorageKey));
|
||||
if (twoFactorToken) {
|
||||
this.authenticationInterceptor.setTwoFactorAccessToken(twoFactorToken.token);
|
||||
}
|
||||
// Emit the correct initial state
|
||||
this.userLoggedIn = true;
|
||||
|
||||
this.userLoggedIn$.next(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the latest OAuth access token in both the interceptor and stored credentials.
|
||||
*/
|
||||
private updateCredentialsToken(): void {
|
||||
const accessToken = this.oauthService.getAccessToken();
|
||||
if (!accessToken) return;
|
||||
|
||||
this.authenticationInterceptor.setAuthorizationToken(accessToken);
|
||||
|
||||
const credentials = this.getCredentials();
|
||||
if (credentials) {
|
||||
credentials.accessToken = accessToken;
|
||||
this.storage.setItem(this.credentialsStorageKey, JSON.stringify(credentials));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the cached credentials from session or local storage, if present.
|
||||
* @returns {Credentials | null} Stored credentials or null when absent.
|
||||
*/
|
||||
private getSavedCredentials(): Credentials | null {
|
||||
const stored =
|
||||
sessionStorage.getItem(this.credentialsStorageKey) || localStorage.getItem(this.credentialsStorageKey);
|
||||
return stored ? JSON.parse(stored) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates the user.
|
||||
* @param {LoginContext} loginContext Login parameters.
|
||||
* @returns {Observable<boolean>} True if authentication is successful.
|
||||
*/
|
||||
login(loginContext: LoginContext) {
|
||||
login(loginContext?: LoginContext): Observable<boolean> {
|
||||
this.alertService.alert({ type: 'Authentication Start', message: 'Please wait...' });
|
||||
// Only allow Remember Me if enabled in config
|
||||
const rememberAllowed = environment.enableRememberMe === true;
|
||||
this.rememberMe = rememberAllowed ? loginContext.remember : false;
|
||||
|
||||
if (this.authMode !== AuthMode.Basic) {
|
||||
// OAuth2/OIDC: Redirect to authorization server with PKCE
|
||||
this.oauthService.initCodeFlow();
|
||||
return of(true);
|
||||
}
|
||||
|
||||
if (!loginContext) {
|
||||
throw new Error('loginContext is required when using Basic authentication');
|
||||
}
|
||||
|
||||
this.rememberMe = environment.enableRememberMe ? (loginContext?.remember ?? false) : false;
|
||||
this.storage = this.rememberMe ? localStorage : sessionStorage;
|
||||
|
||||
if (environment.oauth.enabled) {
|
||||
let httpParams = new HttpParams();
|
||||
httpParams = httpParams.set('username', loginContext.username);
|
||||
httpParams = httpParams.set('password', loginContext.password);
|
||||
httpParams = httpParams.set('client_id', `${environment.oauth.appId}`);
|
||||
httpParams = httpParams.set('grant_type', 'password');
|
||||
httpParams = httpParams.set('remember_me', this.rememberMe ? 'true' : 'false');
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.set('Content-Type', 'application/x-www-form-urlencoded');
|
||||
return this.http.post(`${environment.oauth.serverUrl}/token`, httpParams.toString(), { headers: headers }).pipe(
|
||||
map((tokenResponse: OAuth2Token) => {
|
||||
this.getUserDetails(tokenResponse);
|
||||
return of(true);
|
||||
// Basic Auth: Direct authentication with Fineract
|
||||
return this.http
|
||||
.post('/authentication', {
|
||||
username: loginContext.username,
|
||||
password: loginContext.password,
|
||||
remember: this.rememberMe
|
||||
})
|
||||
.pipe(
|
||||
map((credentials: Credentials) => {
|
||||
this.onLoginSuccess(credentials);
|
||||
return true;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return this.http
|
||||
.post('/authentication', {
|
||||
username: loginContext.username,
|
||||
password: loginContext.password,
|
||||
remember: this.rememberMe
|
||||
})
|
||||
.pipe(
|
||||
map((credentials: Credentials) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches user details from the server.
|
||||
* @returns {Promise<void>} Promise that resolves when user details are fetched.
|
||||
*/
|
||||
private async getUserDetails(): Promise<void> {
|
||||
const accessToken = this.oauthService.getAccessToken();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.authMode === AuthMode.OIDC) {
|
||||
const url = `${environment.OIDC.oidcApiUrl}authentication/userdetails`;
|
||||
this.http.post<{ object: Credentials }>(url, { token: accessToken }).subscribe({
|
||||
next: (response) => {
|
||||
const credentials = response.object;
|
||||
credentials.accessToken = accessToken;
|
||||
this.onLoginSuccess(credentials);
|
||||
return of(true);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the user details after oauth2 authentication.
|
||||
*
|
||||
* Sets the oauth2 token refresh time.
|
||||
* @param {OAuth2Token} tokenResponse OAuth2 Token details.
|
||||
*/
|
||||
private getUserDetails(tokenResponse: OAuth2Token) {
|
||||
this.refreshTokenOnExpiry(tokenResponse.expires_in);
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.set('Authorization', 'bearer ' + tokenResponse.access_token);
|
||||
this.http
|
||||
.get(`${environment.serverUrl}/userdetails`, { headers: headers })
|
||||
.subscribe((credentials: Credentials) => {
|
||||
this.onLoginSuccess(credentials);
|
||||
if (!credentials.shouldRenewPassword) {
|
||||
this.storage.setItem(this.oAuthTokenDetailsStorageKey, JSON.stringify(tokenResponse));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the oauth2 token to refresh on expiry.
|
||||
* @param {number} expiresInTime OAuth2 token expiry time in seconds.
|
||||
*/
|
||||
private refreshTokenOnExpiry(expiresInTime: number) {
|
||||
setTimeout(() => this.refreshOAuthAccessToken(), expiresInTime * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the oauth2 authorization token.
|
||||
*/
|
||||
private refreshOAuthAccessToken() {
|
||||
var oAuthRefreshToken = JSON.parse(this.storage.getItem(this.oAuthTokenDetailsStorageKey));
|
||||
if (oAuthRefreshToken == null) {
|
||||
return;
|
||||
}
|
||||
oAuthRefreshToken = JSON.parse(this.storage.getItem(this.oAuthTokenDetailsStorageKey)).refresh_token;
|
||||
this.authenticationInterceptor.removeAuthorization();
|
||||
const credentials = JSON.parse(this.storage.getItem(this.credentialsStorageKey));
|
||||
let httpParams = new HttpParams();
|
||||
httpParams = httpParams.set('username', credentials.username);
|
||||
httpParams = httpParams.set('client_id', `${environment.oauth.appId}`);
|
||||
httpParams = httpParams.set('refresh_token', oAuthRefreshToken);
|
||||
httpParams = httpParams.set('grant_type', 'refresh_token');
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.set('Content-Type', 'application/x-www-form-urlencoded');
|
||||
return this.http
|
||||
.post(`${environment.oauth.serverUrl}/token`, httpParams.toString(), { headers: headers })
|
||||
.subscribe((tokenResponse: OAuth2Token) => {
|
||||
this.storage.setItem(this.oAuthTokenDetailsStorageKey, JSON.stringify(tokenResponse));
|
||||
this.authenticationInterceptor.setAuthorizationToken(tokenResponse.access_token);
|
||||
this.refreshTokenOnExpiry(tokenResponse.expires_in);
|
||||
const credentials = JSON.parse(this.storage.getItem(this.credentialsStorageKey));
|
||||
credentials.accessToken = tokenResponse.access_token;
|
||||
this.storage.setItem(this.credentialsStorageKey, JSON.stringify(credentials));
|
||||
});
|
||||
resolve();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to fetch user details:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
} else if (this.authMode === AuthMode.OAuth2) {
|
||||
const headers = new HttpHeaders().set('Authorization', `Bearer ${accessToken}`);
|
||||
const url = `${environment.oauth.serverUrl}/userdetails`;
|
||||
this.http.get<Credentials>(url, { headers }).subscribe({
|
||||
next: (credentials) => {
|
||||
credentials.accessToken = accessToken;
|
||||
this.onLoginSuccess(credentials);
|
||||
resolve();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to fetch user details:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,13 +249,12 @@ export class AuthenticationService {
|
||||
* Sends an alert on successful login.
|
||||
* @param {Credentials} credentials Authenticated user credentials.
|
||||
*/
|
||||
private onLoginSuccess(credentials: Credentials) {
|
||||
this.userLoggedIn = true;
|
||||
private onLoginSuccess(credentials: Credentials): void {
|
||||
this.userLoggedIn$.next(true); // ✅ notify observers
|
||||
// Ensure the rememberMe value is preserved in credentials
|
||||
credentials.rememberMe = this.rememberMe;
|
||||
|
||||
if (environment.oauth.enabled) {
|
||||
if (this.authMode !== AuthMode.Basic) {
|
||||
this.authenticationInterceptor.setAuthorizationToken(credentials.accessToken);
|
||||
} else {
|
||||
this.authenticationInterceptor.setAuthorizationToken(credentials.base64EncodedAuthenticationKey);
|
||||
@@ -241,21 +284,31 @@ export class AuthenticationService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout ongoing Oauth2 session.
|
||||
* Handles the OAuth callback.
|
||||
* @returns {Promise<boolean>} True if the OAuth callback was successful.
|
||||
*/
|
||||
private logoutAuthSession() {
|
||||
const oAuthRefreshToken = JSON.parse(this.storage.getItem(this.oAuthTokenDetailsStorageKey)).refresh_token;
|
||||
const credentials = JSON.parse(this.storage.getItem(this.credentialsStorageKey));
|
||||
this.authenticationInterceptor.removeAuthorizationTenant();
|
||||
let httpParams = new HttpParams();
|
||||
httpParams = httpParams.set('username', credentials.username);
|
||||
httpParams = httpParams.set('client_id', `${environment.oauth.appId}`);
|
||||
httpParams = httpParams.set('refresh_token', oAuthRefreshToken);
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.set('Content-Type', 'application/x-www-form-urlencoded');
|
||||
return this.http
|
||||
.post(`${environment.oauth.serverUrl}/logout`, httpParams.toString(), { headers: headers })
|
||||
.subscribe();
|
||||
async handleOAuthCallback(): Promise<boolean> {
|
||||
try {
|
||||
// index.html preserves the OAuth callback query string in sessionStorage before redirecting to /#/callback, since Angular routing consumes query params before the OAuth library can process them.
|
||||
let queryString = sessionStorage.getItem('oauth_callback_query');
|
||||
|
||||
if (queryString) {
|
||||
sessionStorage.removeItem('oauth_callback_query');
|
||||
await this.oauthService.tryLoginCodeFlow({ customHashFragment: queryString });
|
||||
} else {
|
||||
await this.oauthService.tryLoginCodeFlow();
|
||||
}
|
||||
|
||||
if (this.oauthService.hasValidAccessToken()) {
|
||||
await this.getUserDetails();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('OAuth callback failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,15 +321,31 @@ export class AuthenticationService {
|
||||
this.http.post('/twofactor/invalidate', { token: twoFactorToken.token }).subscribe();
|
||||
this.authenticationInterceptor.removeTwoFactorAuthorization();
|
||||
}
|
||||
const oAuthRefreshToken = JSON.parse(this.storage.getItem(this.oAuthTokenDetailsStorageKey));
|
||||
if (oAuthRefreshToken) {
|
||||
this.logoutAuthSession();
|
||||
}
|
||||
|
||||
// Clear any pending OAuth callback data
|
||||
sessionStorage.removeItem('oauth_callback_query');
|
||||
|
||||
this.authenticationInterceptor.removeAuthorization();
|
||||
this.setCredentials();
|
||||
this.resetDialog();
|
||||
this.userLoggedIn = false;
|
||||
this.userLoggedIn$.next(false); // ✅ notify observers
|
||||
this.userLoggedIn$.next(false);
|
||||
|
||||
if (this.authMode === AuthMode.OIDC) {
|
||||
// OIDC: Use library to handle logout (redirects to OIDC provider)
|
||||
this.oauthService.logOut();
|
||||
} else if (this.authMode === AuthMode.OAuth2) {
|
||||
// OAuth2 (Fineract): Clear library tokens and server session
|
||||
this.oauthService.logOut(true); // true = don't redirect
|
||||
// Call Fineract logout endpoint in a popup to clear server session (includes JSESSIONID cookie)
|
||||
// Then close the popup and navigate to login page
|
||||
const logoutWindow = window.open(environment.oauth.logoutUrl, '_blank', 'width=100,height=100');
|
||||
setTimeout(() => {
|
||||
if (logoutWindow) {
|
||||
logoutWindow.close();
|
||||
}
|
||||
window.location.href = `${window.location.origin}/#/login`;
|
||||
}, 500);
|
||||
}
|
||||
return of(true);
|
||||
}
|
||||
|
||||
@@ -297,11 +366,10 @@ export class AuthenticationService {
|
||||
* @returns {boolean} True if the user is authenticated.
|
||||
*/
|
||||
isAuthenticated(): boolean {
|
||||
return !!(
|
||||
JSON.parse(
|
||||
sessionStorage.getItem(this.credentialsStorageKey) || localStorage.getItem(this.credentialsStorageKey)
|
||||
) && this.twoFactorAccessTokenIsValid()
|
||||
);
|
||||
if (this.authMode !== AuthMode.Basic) {
|
||||
return this.oauthService.hasValidAccessToken();
|
||||
}
|
||||
return !!(this.getSavedCredentials() && this.twoFactorAccessTokenIsValid());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,29 +388,40 @@ export class AuthenticationService {
|
||||
*
|
||||
* @param {Credentials} credentials Authenticated user credentials.
|
||||
*/
|
||||
private setCredentials(credentials?: Credentials) {
|
||||
private setCredentials(credentials?: Credentials): void {
|
||||
if (credentials) {
|
||||
credentials.rememberMe = this.rememberMe;
|
||||
// Make sure we're using the correct storage based on rememberMe value
|
||||
this.storage = credentials.rememberMe ? localStorage : sessionStorage;
|
||||
this.oauthService.setStorage(this.storage);
|
||||
this.storage.setItem(this.credentialsStorageKey, JSON.stringify(credentials));
|
||||
} else {
|
||||
// Clear credentials from both storage types to ensure complete logout
|
||||
localStorage.removeItem(this.credentialsStorageKey);
|
||||
sessionStorage.removeItem(this.credentialsStorageKey);
|
||||
localStorage.removeItem(this.oAuthTokenDetailsStorageKey);
|
||||
sessionStorage.removeItem(this.oAuthTokenDetailsStorageKey);
|
||||
localStorage.removeItem(this.twoFactorAuthenticationTokenStorageKey);
|
||||
sessionStorage.removeItem(this.twoFactorAuthenticationTokenStorageKey);
|
||||
[
|
||||
localStorage,
|
||||
sessionStorage
|
||||
].forEach((store) => {
|
||||
store.removeItem(this.credentialsStorageKey);
|
||||
store.removeItem(this.twoFactorAuthenticationTokenStorageKey);
|
||||
});
|
||||
this.cleanupLegacyStorage();
|
||||
}
|
||||
}
|
||||
|
||||
public saveZitadelCredentials(credentials: Credentials): void {
|
||||
this.setCredentials(credentials);
|
||||
}
|
||||
|
||||
public saveZitadeloAuthTokenDetailsStorageKey(tokenResponse: OAuth2Token): void {
|
||||
this.storage.setItem(this.oAuthTokenDetailsStorageKey, JSON.stringify(tokenResponse));
|
||||
private cleanupLegacyStorage(): void {
|
||||
const legacyKeys = [
|
||||
'mifosXZitadelTokenDetails',
|
||||
'mifosXOAuthTokenDetails',
|
||||
'token_start_time',
|
||||
'refresh_expires_in',
|
||||
'mifosXZitadel',
|
||||
'auth_code'
|
||||
// Note: Do NOT remove 'PKCE_verifier' here - it's needed by angular-oauth2-oidc for the callback
|
||||
];
|
||||
legacyKeys.forEach((key) => {
|
||||
localStorage.removeItem(key);
|
||||
sessionStorage.removeItem(key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,7 +480,7 @@ export class AuthenticationService {
|
||||
* Sends an alert on successful login.
|
||||
* @param {any} response Two factor authentication token details.
|
||||
*/
|
||||
private onOTPValidateSuccess(response: any) {
|
||||
private onOTPValidateSuccess(response: any): void {
|
||||
this.authenticationInterceptor.setTwoFactorAccessToken(response.token);
|
||||
if (this.credentials.shouldRenewPassword) {
|
||||
this.alertService.alert({
|
||||
@@ -443,6 +522,6 @@ export class AuthenticationService {
|
||||
* Get user logged in
|
||||
*/
|
||||
getUserLoggedIn(): boolean {
|
||||
return this.userLoggedIn;
|
||||
return this.userLoggedIn$.value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* OAuth2 token model.
|
||||
* OAuth2/OIDC token response model.
|
||||
*/
|
||||
export interface OAuth2Token {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
refresh_token: string;
|
||||
refresh_token?: string;
|
||||
expires_in: number;
|
||||
scope: string;
|
||||
scope?: string;
|
||||
// ID token (only present in OIDC responses)
|
||||
id_token?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { AuthConfig } from 'angular-oauth2-oidc';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
export enum AuthMode {
|
||||
OAuth2 = 'oauth2',
|
||||
OIDC = 'oidc',
|
||||
Basic = 'basic'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which authentication mode should be enabled based on runtime configuration.
|
||||
* @returns {AuthMode} The active authentication mode.
|
||||
*/
|
||||
export function getActiveAuthMode(): AuthMode {
|
||||
if (environment.OIDC.oidcServerEnabled) {
|
||||
return AuthMode.OIDC;
|
||||
} else if (environment.oauth.enabled) {
|
||||
return AuthMode.OAuth2;
|
||||
}
|
||||
return AuthMode.Basic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the Angular OAuth service configuration for the active authentication mode.
|
||||
* @returns {AuthConfig} OAuth/OIDC configuration consumed by angular-oauth2-oidc.
|
||||
*/
|
||||
export function getOAuthConfig(): AuthConfig {
|
||||
const authMode = getActiveAuthMode();
|
||||
|
||||
switch (authMode) {
|
||||
case AuthMode.OIDC:
|
||||
return getOIDCConfig();
|
||||
case AuthMode.OAuth2:
|
||||
return getOAuth2Config();
|
||||
default:
|
||||
return { clientId: '' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the configuration required for OIDC-compliant providers.
|
||||
* @returns {AuthConfig} OIDC configuration block.
|
||||
*/
|
||||
function getOIDCConfig(): AuthConfig {
|
||||
const frontUrl = environment.OIDC.oidcFrontUrl || window.location.origin;
|
||||
|
||||
return {
|
||||
issuer: environment.OIDC.oidcBaseUrl,
|
||||
clientId: environment.OIDC.oidcClientId,
|
||||
redirectUri: `${frontUrl}/callback`,
|
||||
postLogoutRedirectUri: `${frontUrl}/#/login`,
|
||||
responseType: 'code',
|
||||
scope: 'openid profile email offline_access',
|
||||
oidc: true,
|
||||
useSilentRefresh: false,
|
||||
requireHttps: environment.production,
|
||||
showDebugInformation: !environment.production,
|
||||
sessionChecksEnabled: false,
|
||||
clearHashAfterLogin: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the configuration required for classic OAuth2 providers (e.g., Fineract).
|
||||
* @returns {AuthConfig} OAuth2 configuration block.
|
||||
*/
|
||||
function getOAuth2Config(): AuthConfig {
|
||||
const frontendUrl = window.location.origin;
|
||||
|
||||
return {
|
||||
issuer: environment.oauth.serverUrl,
|
||||
loginUrl: environment.oauth.authorizeUrl,
|
||||
tokenEndpoint: environment.oauth.tokenUrl,
|
||||
redirectUri: environment.oauth.redirectUri,
|
||||
postLogoutRedirectUri: `${frontendUrl}/#/login`,
|
||||
clientId: environment.oauth.appId,
|
||||
responseType: 'code',
|
||||
scope: environment.oauth.scope,
|
||||
useSilentRefresh: false,
|
||||
oidc: false,
|
||||
// Skip issuer validation for OAuth2 (non-OIDC) flows
|
||||
skipIssuerCheck: true,
|
||||
// Disable strict discovery validation for custom OAuth2 endpoints
|
||||
strictDiscoveryDocumentValidation: false,
|
||||
requireHttps: environment.production,
|
||||
showDebugInformation: !environment.production,
|
||||
sessionChecksEnabled: false,
|
||||
clearHashAfterLogin: false
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user