WEB-487: Add OAuth 2.1 PKCE flow (#2864)
This commit is contained in:
Generated
+14
@@ -31,6 +31,7 @@
|
|||||||
"@swimlane/ngx-graph": "^11.0.0",
|
"@swimlane/ngx-graph": "^11.0.0",
|
||||||
"@tailwindcss/forms": "^0.5.4",
|
"@tailwindcss/forms": "^0.5.4",
|
||||||
"@tinymce/tinymce-angular": "^9.1.1",
|
"@tinymce/tinymce-angular": "^9.1.1",
|
||||||
|
"angular-oauth2-oidc": "^20.0.0",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
@@ -9897,6 +9898,19 @@
|
|||||||
"node": ">= 14.0.0"
|
"node": ">= 14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/angular-oauth2-oidc": {
|
||||||
|
"version": "20.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/angular-oauth2-oidc/-/angular-oauth2-oidc-20.0.2.tgz",
|
||||||
|
"integrity": "sha512-bMSXEQIuvgq8yqnsIatZggAvCJvY+pm7G8MK0tWCHR93UpFuNN+L5B6pY9CzRg8Ys+VVhkLIBx4zEHbJnv9icg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.5.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": ">=20.0.0",
|
||||||
|
"@angular/core": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-colors": {
|
"node_modules/ansi-colors": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
"@swimlane/ngx-graph": "^11.0.0",
|
"@swimlane/ngx-graph": "^11.0.0",
|
||||||
"@tailwindcss/forms": "^0.5.4",
|
"@tailwindcss/forms": "^0.5.4",
|
||||||
"@tinymce/tinymce-angular": "^9.1.1",
|
"@tinymce/tinymce-angular": "^9.1.1",
|
||||||
|
"angular-oauth2-oidc": "^20.0.0",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import { TokenInterceptor as ZitadelTokenInterceptor } from './zitadel/token.int
|
|||||||
import { AuthService } from './zitadel/auth.service';
|
import { AuthService } from './zitadel/auth.service';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { CallbackComponent } from './zitadel/callback/callback.component';
|
import { CallbackComponent } from './zitadel/callback/callback.component';
|
||||||
|
import { OAuthModule } from 'angular-oauth2-oidc';
|
||||||
|
|
||||||
export class CustomMissingTranslationHandler implements MissingTranslationHandler {
|
export class CustomMissingTranslationHandler implements MissingTranslationHandler {
|
||||||
handle(params: MissingTranslationHandlerParams): string {
|
handle(params: MissingTranslationHandlerParams): string {
|
||||||
@@ -120,7 +121,8 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||||||
ConfigurationWizardModule,
|
ConfigurationWizardModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
NotFoundComponent,
|
NotFoundComponent,
|
||||||
CallbackComponent
|
CallbackComponent,
|
||||||
|
OAuthModule.forRoot()
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
DatePipe,
|
DatePipe,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
/** Angular Imports */
|
/** Angular Imports */
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
||||||
|
|
||||||
/** rxjs Imports */
|
/** rxjs Imports */
|
||||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
/** 3rd party Imports */
|
||||||
|
import { OAuthService } from 'angular-oauth2-oidc';
|
||||||
/** Custom Services */
|
/** Custom Services */
|
||||||
import { AlertService } from '../alert/alert.service';
|
import { AlertService } from '../alert/alert.service';
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ import { environment } from '../../../environments/environment';
|
|||||||
/** Custom Models */
|
/** Custom Models */
|
||||||
import { LoginContext } from './login-context.model';
|
import { LoginContext } from './login-context.model';
|
||||||
import { Credentials } from './credentials.model';
|
import { Credentials } from './credentials.model';
|
||||||
import { OAuth2Token } from './o-auth2-token.model';
|
import { getOAuthConfig, getActiveAuthMode, AuthMode } from './oauth.config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authentication workflow.
|
* Authentication workflow.
|
||||||
@@ -28,17 +29,24 @@ export class AuthenticationService {
|
|||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
private alertService = inject(AlertService);
|
private alertService = inject(AlertService);
|
||||||
private authenticationInterceptor = inject(AuthenticationInterceptor);
|
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) {
|
changePassword(userId: string, passwordObj: any) {
|
||||||
return this.http.put(`/users/${userId}`, passwordObj);
|
return this.http.put(`/users/${userId}`, passwordObj);
|
||||||
}
|
}
|
||||||
// User logged in boolean
|
|
||||||
private userLoggedIn: boolean;
|
|
||||||
private userLoggedIn$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
private userLoggedIn$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
/** Observable that emits authentication state changes. */
|
||||||
public readonly isAuthenticated$ = this.userLoggedIn$.asObservable();
|
public readonly isAuthenticated$ = this.userLoggedIn$.asObservable();
|
||||||
|
|
||||||
/** Denotes whether the user credentials should persist through sessions. */
|
/** Denotes whether the user credentials should persist through sessions. */
|
||||||
private rememberMe: boolean;
|
private rememberMe = false;
|
||||||
/**
|
/**
|
||||||
* Denotes the type of storage:
|
* Denotes the type of storage:
|
||||||
*
|
*
|
||||||
@@ -46,17 +54,15 @@ export class AuthenticationService {
|
|||||||
*
|
*
|
||||||
* Local Storage: User credentials should persist through sessions.
|
* Local Storage: User credentials should persist through sessions.
|
||||||
*/
|
*/
|
||||||
private storage: any;
|
private storage: Storage = sessionStorage;
|
||||||
/** User credentials. */
|
|
||||||
|
|
||||||
private credentials: Credentials;
|
private credentials: Credentials;
|
||||||
private dialogShown = false;
|
private dialogShown = false;
|
||||||
|
private authMode: AuthMode = AuthMode.Basic;
|
||||||
|
|
||||||
/** Key to store credentials in storage. */
|
/** Key to store credentials in storage. */
|
||||||
private credentialsStorageKey = 'mifosXCredentials';
|
private readonly credentialsStorageKey = 'mifosXCredentials';
|
||||||
/** Key to store oauth token details in storage. */
|
|
||||||
private oAuthTokenDetailsStorageKey = 'mifosXOAuthTokenDetails';
|
|
||||||
/** Key to store two factor authentication token in storage. */
|
/** 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
|
* 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 {HttpClient} http Http Client to send requests.
|
||||||
* @param {AlertService} alertService Alert Service.
|
* @param {AlertService} alertService Alert Service.
|
||||||
* @param {AuthenticationInterceptor} authenticationInterceptor Authentication Interceptor.
|
* @param {AuthenticationInterceptor} authenticationInterceptor Authentication Interceptor.
|
||||||
|
* @param {OAuthService} oauthService OAuth Service.
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
const authenticationInterceptor = this.authenticationInterceptor;
|
this.authMode = getActiveAuthMode();
|
||||||
|
|
||||||
this.userLoggedIn = false;
|
if (this.authMode !== AuthMode.Basic) {
|
||||||
this.rememberMe = false;
|
this.initializeOAuthService();
|
||||||
this.storage = sessionStorage;
|
}
|
||||||
const savedCredentials = JSON.parse(
|
|
||||||
sessionStorage.getItem(this.credentialsStorageKey) || localStorage.getItem(this.credentialsStorageKey)
|
this.restoreSession();
|
||||||
);
|
}
|
||||||
if (savedCredentials) {
|
|
||||||
if (savedCredentials.rememberMe) {
|
/**
|
||||||
this.rememberMe = true;
|
* Configures the OAuth service with runtime settings and hooks up token listeners.
|
||||||
this.storage = localStorage;
|
*/
|
||||||
|
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();
|
this.cleanupLegacyStorage();
|
||||||
} else {
|
}
|
||||||
authenticationInterceptor.setAuthorizationToken(savedCredentials.base64EncodedAuthenticationKey);
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
} else {
|
||||||
authenticationInterceptor.setTwoFactorAccessToken(twoFactorAccessToken.token);
|
// 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);
|
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.
|
* Authenticates the user.
|
||||||
* @param {LoginContext} loginContext Login parameters.
|
* @param {LoginContext} loginContext Login parameters.
|
||||||
* @returns {Observable<boolean>} True if authentication is successful.
|
* @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...' });
|
this.alertService.alert({ type: 'Authentication Start', message: 'Please wait...' });
|
||||||
// Only allow Remember Me if enabled in config
|
|
||||||
const rememberAllowed = environment.enableRememberMe === true;
|
if (this.authMode !== AuthMode.Basic) {
|
||||||
this.rememberMe = rememberAllowed ? loginContext.remember : false;
|
// 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;
|
this.storage = this.rememberMe ? localStorage : sessionStorage;
|
||||||
|
|
||||||
if (environment.oauth.enabled) {
|
// Basic Auth: Direct authentication with Fineract
|
||||||
let httpParams = new HttpParams();
|
return this.http
|
||||||
httpParams = httpParams.set('username', loginContext.username);
|
.post('/authentication', {
|
||||||
httpParams = httpParams.set('password', loginContext.password);
|
username: loginContext.username,
|
||||||
httpParams = httpParams.set('client_id', `${environment.oauth.appId}`);
|
password: loginContext.password,
|
||||||
httpParams = httpParams.set('grant_type', 'password');
|
remember: this.rememberMe
|
||||||
httpParams = httpParams.set('remember_me', this.rememberMe ? 'true' : 'false');
|
})
|
||||||
let headers = new HttpHeaders();
|
.pipe(
|
||||||
headers = headers.set('Content-Type', 'application/x-www-form-urlencoded');
|
map((credentials: Credentials) => {
|
||||||
return this.http.post(`${environment.oauth.serverUrl}/token`, httpParams.toString(), { headers: headers }).pipe(
|
this.onLoginSuccess(credentials);
|
||||||
map((tokenResponse: OAuth2Token) => {
|
return true;
|
||||||
this.getUserDetails(tokenResponse);
|
|
||||||
return of(true);
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
return this.http
|
|
||||||
.post('/authentication', {
|
/**
|
||||||
username: loginContext.username,
|
* Fetches user details from the server.
|
||||||
password: loginContext.password,
|
* @returns {Promise<void>} Promise that resolves when user details are fetched.
|
||||||
remember: this.rememberMe
|
*/
|
||||||
})
|
private async getUserDetails(): Promise<void> {
|
||||||
.pipe(
|
const accessToken = this.oauthService.getAccessToken();
|
||||||
map((credentials: Credentials) => {
|
|
||||||
|
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);
|
this.onLoginSuccess(credentials);
|
||||||
return of(true);
|
resolve();
|
||||||
})
|
},
|
||||||
);
|
error: (error) => {
|
||||||
}
|
console.error('Failed to fetch user details:', error);
|
||||||
}
|
reject(error);
|
||||||
|
}
|
||||||
/**
|
});
|
||||||
* Retrieves the user details after oauth2 authentication.
|
} else if (this.authMode === AuthMode.OAuth2) {
|
||||||
*
|
const headers = new HttpHeaders().set('Authorization', `Bearer ${accessToken}`);
|
||||||
* Sets the oauth2 token refresh time.
|
const url = `${environment.oauth.serverUrl}/userdetails`;
|
||||||
* @param {OAuth2Token} tokenResponse OAuth2 Token details.
|
this.http.get<Credentials>(url, { headers }).subscribe({
|
||||||
*/
|
next: (credentials) => {
|
||||||
private getUserDetails(tokenResponse: OAuth2Token) {
|
credentials.accessToken = accessToken;
|
||||||
this.refreshTokenOnExpiry(tokenResponse.expires_in);
|
this.onLoginSuccess(credentials);
|
||||||
let headers = new HttpHeaders();
|
resolve();
|
||||||
headers = headers.set('Authorization', 'bearer ' + tokenResponse.access_token);
|
},
|
||||||
this.http
|
error: (error) => {
|
||||||
.get(`${environment.serverUrl}/userdetails`, { headers: headers })
|
console.error('Failed to fetch user details:', error);
|
||||||
.subscribe((credentials: Credentials) => {
|
reject(error);
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -205,13 +249,12 @@ export class AuthenticationService {
|
|||||||
* Sends an alert on successful login.
|
* Sends an alert on successful login.
|
||||||
* @param {Credentials} credentials Authenticated user credentials.
|
* @param {Credentials} credentials Authenticated user credentials.
|
||||||
*/
|
*/
|
||||||
private onLoginSuccess(credentials: Credentials) {
|
private onLoginSuccess(credentials: Credentials): void {
|
||||||
this.userLoggedIn = true;
|
|
||||||
this.userLoggedIn$.next(true); // ✅ notify observers
|
this.userLoggedIn$.next(true); // ✅ notify observers
|
||||||
// Ensure the rememberMe value is preserved in credentials
|
// Ensure the rememberMe value is preserved in credentials
|
||||||
credentials.rememberMe = this.rememberMe;
|
credentials.rememberMe = this.rememberMe;
|
||||||
|
|
||||||
if (environment.oauth.enabled) {
|
if (this.authMode !== AuthMode.Basic) {
|
||||||
this.authenticationInterceptor.setAuthorizationToken(credentials.accessToken);
|
this.authenticationInterceptor.setAuthorizationToken(credentials.accessToken);
|
||||||
} else {
|
} else {
|
||||||
this.authenticationInterceptor.setAuthorizationToken(credentials.base64EncodedAuthenticationKey);
|
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() {
|
async handleOAuthCallback(): Promise<boolean> {
|
||||||
const oAuthRefreshToken = JSON.parse(this.storage.getItem(this.oAuthTokenDetailsStorageKey)).refresh_token;
|
try {
|
||||||
const credentials = JSON.parse(this.storage.getItem(this.credentialsStorageKey));
|
// 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.
|
||||||
this.authenticationInterceptor.removeAuthorizationTenant();
|
let queryString = sessionStorage.getItem('oauth_callback_query');
|
||||||
let httpParams = new HttpParams();
|
|
||||||
httpParams = httpParams.set('username', credentials.username);
|
if (queryString) {
|
||||||
httpParams = httpParams.set('client_id', `${environment.oauth.appId}`);
|
sessionStorage.removeItem('oauth_callback_query');
|
||||||
httpParams = httpParams.set('refresh_token', oAuthRefreshToken);
|
await this.oauthService.tryLoginCodeFlow({ customHashFragment: queryString });
|
||||||
let headers = new HttpHeaders();
|
} else {
|
||||||
headers = headers.set('Content-Type', 'application/x-www-form-urlencoded');
|
await this.oauthService.tryLoginCodeFlow();
|
||||||
return this.http
|
}
|
||||||
.post(`${environment.oauth.serverUrl}/logout`, httpParams.toString(), { headers: headers })
|
|
||||||
.subscribe();
|
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.http.post('/twofactor/invalidate', { token: twoFactorToken.token }).subscribe();
|
||||||
this.authenticationInterceptor.removeTwoFactorAuthorization();
|
this.authenticationInterceptor.removeTwoFactorAuthorization();
|
||||||
}
|
}
|
||||||
const oAuthRefreshToken = JSON.parse(this.storage.getItem(this.oAuthTokenDetailsStorageKey));
|
|
||||||
if (oAuthRefreshToken) {
|
// Clear any pending OAuth callback data
|
||||||
this.logoutAuthSession();
|
sessionStorage.removeItem('oauth_callback_query');
|
||||||
}
|
|
||||||
this.authenticationInterceptor.removeAuthorization();
|
this.authenticationInterceptor.removeAuthorization();
|
||||||
this.setCredentials();
|
this.setCredentials();
|
||||||
this.resetDialog();
|
this.resetDialog();
|
||||||
this.userLoggedIn = false;
|
this.userLoggedIn$.next(false);
|
||||||
this.userLoggedIn$.next(false); // ✅ notify observers
|
|
||||||
|
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);
|
return of(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,11 +366,10 @@ export class AuthenticationService {
|
|||||||
* @returns {boolean} True if the user is authenticated.
|
* @returns {boolean} True if the user is authenticated.
|
||||||
*/
|
*/
|
||||||
isAuthenticated(): boolean {
|
isAuthenticated(): boolean {
|
||||||
return !!(
|
if (this.authMode !== AuthMode.Basic) {
|
||||||
JSON.parse(
|
return this.oauthService.hasValidAccessToken();
|
||||||
sessionStorage.getItem(this.credentialsStorageKey) || localStorage.getItem(this.credentialsStorageKey)
|
}
|
||||||
) && this.twoFactorAccessTokenIsValid()
|
return !!(this.getSavedCredentials() && this.twoFactorAccessTokenIsValid());
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -320,29 +388,40 @@ export class AuthenticationService {
|
|||||||
*
|
*
|
||||||
* @param {Credentials} credentials Authenticated user credentials.
|
* @param {Credentials} credentials Authenticated user credentials.
|
||||||
*/
|
*/
|
||||||
private setCredentials(credentials?: Credentials) {
|
private setCredentials(credentials?: Credentials): void {
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
credentials.rememberMe = this.rememberMe;
|
credentials.rememberMe = this.rememberMe;
|
||||||
// Make sure we're using the correct storage based on rememberMe value
|
// Make sure we're using the correct storage based on rememberMe value
|
||||||
this.storage = credentials.rememberMe ? localStorage : sessionStorage;
|
this.storage = credentials.rememberMe ? localStorage : sessionStorage;
|
||||||
|
this.oauthService.setStorage(this.storage);
|
||||||
this.storage.setItem(this.credentialsStorageKey, JSON.stringify(credentials));
|
this.storage.setItem(this.credentialsStorageKey, JSON.stringify(credentials));
|
||||||
} else {
|
} else {
|
||||||
// Clear credentials from both storage types to ensure complete logout
|
// Clear credentials from both storage types to ensure complete logout
|
||||||
localStorage.removeItem(this.credentialsStorageKey);
|
[
|
||||||
sessionStorage.removeItem(this.credentialsStorageKey);
|
localStorage,
|
||||||
localStorage.removeItem(this.oAuthTokenDetailsStorageKey);
|
sessionStorage
|
||||||
sessionStorage.removeItem(this.oAuthTokenDetailsStorageKey);
|
].forEach((store) => {
|
||||||
localStorage.removeItem(this.twoFactorAuthenticationTokenStorageKey);
|
store.removeItem(this.credentialsStorageKey);
|
||||||
sessionStorage.removeItem(this.twoFactorAuthenticationTokenStorageKey);
|
store.removeItem(this.twoFactorAuthenticationTokenStorageKey);
|
||||||
|
});
|
||||||
|
this.cleanupLegacyStorage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public saveZitadelCredentials(credentials: Credentials): void {
|
private cleanupLegacyStorage(): void {
|
||||||
this.setCredentials(credentials);
|
const legacyKeys = [
|
||||||
}
|
'mifosXZitadelTokenDetails',
|
||||||
|
'mifosXOAuthTokenDetails',
|
||||||
public saveZitadeloAuthTokenDetailsStorageKey(tokenResponse: OAuth2Token): void {
|
'token_start_time',
|
||||||
this.storage.setItem(this.oAuthTokenDetailsStorageKey, JSON.stringify(tokenResponse));
|
'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.
|
* Sends an alert on successful login.
|
||||||
* @param {any} response Two factor authentication token details.
|
* @param {any} response Two factor authentication token details.
|
||||||
*/
|
*/
|
||||||
private onOTPValidateSuccess(response: any) {
|
private onOTPValidateSuccess(response: any): void {
|
||||||
this.authenticationInterceptor.setTwoFactorAccessToken(response.token);
|
this.authenticationInterceptor.setTwoFactorAccessToken(response.token);
|
||||||
if (this.credentials.shouldRenewPassword) {
|
if (this.credentials.shouldRenewPassword) {
|
||||||
this.alertService.alert({
|
this.alertService.alert({
|
||||||
@@ -443,6 +522,6 @@ export class AuthenticationService {
|
|||||||
* Get user logged in
|
* Get user logged in
|
||||||
*/
|
*/
|
||||||
getUserLoggedIn(): boolean {
|
getUserLoggedIn(): boolean {
|
||||||
return this.userLoggedIn;
|
return this.userLoggedIn$.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* OAuth2 token model.
|
* OAuth2/OIDC token response model.
|
||||||
*/
|
*/
|
||||||
export interface OAuth2Token {
|
export interface OAuth2Token {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
token_type: string;
|
token_type: string;
|
||||||
refresh_token: string;
|
refresh_token?: string;
|
||||||
expires_in: number;
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ export class ApiPrefixInterceptor implements HttpInterceptor {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Ignore URLs that are complete for i18n
|
* Ignore URLs that are complete for i18n
|
||||||
|
* **__NOTE__** OAuth2 endpoints are handled by angular-oauth2-oidc library
|
||||||
*/
|
*/
|
||||||
if (!request.url.includes('http:') && !request.url.includes('https:')) {
|
if (!request.url.includes('http:') && !request.url.includes('https:')) {
|
||||||
request = request.clone({ url: baseUrl + request.url });
|
request = request.clone({ url: baseUrl + request.url });
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ import { MatIcon } from '@angular/material/icon';
|
|||||||
import { MatLine } from '@angular/material/grid-list';
|
import { MatLine } from '@angular/material/grid-list';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
/** Environment Configuration and Zitadel*/
|
import { catchError, finalize, of, take } from 'rxjs';
|
||||||
import { environment } from '../../../../environments/environment';
|
|
||||||
import { AuthService } from 'app/zitadel/auth.service';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sidenav component.
|
* Sidenav component.
|
||||||
@@ -56,7 +54,6 @@ export class SidenavComponent implements OnInit, AfterViewInit {
|
|||||||
private settingsService = inject(SettingsService);
|
private settingsService = inject(SettingsService);
|
||||||
private configurationWizardService = inject(ConfigurationWizardService);
|
private configurationWizardService = inject(ConfigurationWizardService);
|
||||||
private popoverService = inject(PopoverService);
|
private popoverService = inject(PopoverService);
|
||||||
private authService = inject(AuthService);
|
|
||||||
|
|
||||||
/** True if sidenav is in collapsed state. */
|
/** True if sidenav is in collapsed state. */
|
||||||
@Input() sidenavCollapsed: boolean;
|
@Input() sidenavCollapsed: boolean;
|
||||||
@@ -103,13 +100,17 @@ export class SidenavComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs out the authenticated user and redirects to login page.
|
* Logs out the authenticated user and redirects to login page.
|
||||||
|
* Uses unified AuthenticationService which handles both OAuth2 and OIDC logout.
|
||||||
*/
|
*/
|
||||||
logout() {
|
logout() {
|
||||||
if (!environment.OIDC.oidcServerEnabled) {
|
this.authenticationService
|
||||||
this.authenticationService.logout().subscribe(() => this.router.navigate(['/login'], { replaceUrl: true }));
|
.logout()
|
||||||
} else {
|
.pipe(
|
||||||
this.authService.logout();
|
take(1),
|
||||||
}
|
catchError(() => of(void 0)),
|
||||||
|
finalize(() => this.router.navigate(['/login'], { replaceUrl: true }))
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
|||||||
import { Router, RouterLink } from '@angular/router';
|
import { Router, RouterLink } from '@angular/router';
|
||||||
|
|
||||||
/** rxjs Imports */
|
/** rxjs Imports */
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { catchError, finalize, map, take } from 'rxjs/operators';
|
||||||
|
|
||||||
/** Custom Services */
|
/** Custom Services */
|
||||||
import { AuthenticationService } from '../../authentication/authentication.service';
|
import { AuthenticationService } from '../../authentication/authentication.service';
|
||||||
@@ -42,9 +42,7 @@ import { NotificationsTrayComponent as NotificationsTrayComponent_1 } from '../.
|
|||||||
import { ThemeToggleComponent } from '../../../shared/theme-toggle/theme-toggle.component';
|
import { ThemeToggleComponent } from '../../../shared/theme-toggle/theme-toggle.component';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
/** Environment Configuration and Zitadel*/
|
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { AuthService } from 'app/zitadel/auth.service';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toolbar component.
|
* Toolbar component.
|
||||||
@@ -77,7 +75,6 @@ export class ToolbarComponent implements OnInit, AfterViewInit, AfterContentChec
|
|||||||
private configurationWizardService = inject(ConfigurationWizardService);
|
private configurationWizardService = inject(ConfigurationWizardService);
|
||||||
private dialog = inject(MatDialog);
|
private dialog = inject(MatDialog);
|
||||||
private changeDetector = inject(ChangeDetectorRef);
|
private changeDetector = inject(ChangeDetectorRef);
|
||||||
private authService = inject(AuthService);
|
|
||||||
|
|
||||||
/* Reference of institution */
|
/* Reference of institution */
|
||||||
@ViewChild('institution') institution: ElementRef<any>;
|
@ViewChild('institution') institution: ElementRef<any>;
|
||||||
@@ -134,13 +131,17 @@ export class ToolbarComponent implements OnInit, AfterViewInit, AfterContentChec
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs out the authenticated user and redirects to login page.
|
* Logs out the authenticated user and redirects to login page.
|
||||||
|
* Uses unified AuthenticationService which handles both OAuth2 and OIDC logout.
|
||||||
*/
|
*/
|
||||||
logout() {
|
logout() {
|
||||||
if (!environment.OIDC.oidcServerEnabled) {
|
this.authenticationService
|
||||||
this.authenticationService.logout().subscribe(() => this.router.navigate(['/login'], { replaceUrl: true }));
|
.logout()
|
||||||
} else {
|
.pipe(
|
||||||
this.authService.logout();
|
take(1),
|
||||||
}
|
catchError(() => of(void 0)),
|
||||||
|
finalize(() => this.router.navigate(['/login'], { replaceUrl: true }))
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@if (!oidcServerEnabled) {
|
<!-- Basic Auth: Username/password form -->
|
||||||
|
@if (!oauthEnabled) {
|
||||||
<div>
|
<div>
|
||||||
<form class="layout-column" [formGroup]="loginForm" (ngSubmit)="login()" id="login-form">
|
<form class="layout-column" [formGroup]="loginForm" (ngSubmit)="login()" id="login-form">
|
||||||
<mat-form-field class="login-input flex-align-center">
|
<mat-form-field class="login-input flex-align-center">
|
||||||
@@ -62,25 +63,20 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (oidcServerEnabled) {
|
<!-- OAuth/OIDC: Simple login button (no credentials needed) -->
|
||||||
<div class="container container-margin">
|
@if (oauthEnabled) {
|
||||||
@if (!isLoggedIn) {
|
<div class="layout-column">
|
||||||
<div class="layout-column align-center-center">
|
<button
|
||||||
<button mat-raised-button color="primary" class="login-button-first flex-align-center" (click)="loginOIDC()">
|
mat-raised-button
|
||||||
{{ 'labels.buttons.Login' | translate }}
|
color="primary"
|
||||||
@if (loading) {
|
class="login-button-first flex-align-center"
|
||||||
<mat-spinner [diameter]="20"></mat-spinner>
|
(click)="loginOAuth()"
|
||||||
}
|
[disabled]="loading"
|
||||||
</button>
|
>
|
||||||
</div>
|
{{ 'labels.buttons.Login' | translate }}
|
||||||
}
|
@if (loading) {
|
||||||
@if (isLoggedIn) {
|
<mat-spinner [diameter]="20"></mat-spinner>
|
||||||
<div>
|
}
|
||||||
<p>Welcome! You are logged in.</p>
|
</button>
|
||||||
<button (click)="logout()">Logout</button>
|
|
||||||
<button (click)="loadProtectedData()">Load Protected Data</button>
|
|
||||||
<pre>{{ protectedData | json }}</pre>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
/** Angular Imports */
|
/** Angular Imports */
|
||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { FormGroup, FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||||
|
|
||||||
/** rxjs Imports */
|
/** rxjs Imports */
|
||||||
import { finalize } from 'rxjs/operators';
|
import { finalize } from 'rxjs/operators';
|
||||||
|
|
||||||
/** Custom Services */
|
/** Custom Services */
|
||||||
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
||||||
import { MatFormField, MatPrefix, MatLabel, MatError, MatSuffix } from '@angular/material/form-field';
|
import { MatPrefix } from '@angular/material/form-field';
|
||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
import { MatIconButton, MatButton } from '@angular/material/button';
|
|
||||||
import { MatCheckbox } from '@angular/material/checkbox';
|
|
||||||
import { MatProgressBar } from '@angular/material/progress-bar';
|
import { MatProgressBar } from '@angular/material/progress-bar';
|
||||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
/** Custom Service Zitadel */
|
|
||||||
import { AuthService } from '../../zitadel/auth.service';
|
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,7 +26,6 @@ import { environment } from '../../../environments/environment';
|
|||||||
...STANDALONE_SHARED_IMPORTS,
|
...STANDALONE_SHARED_IMPORTS,
|
||||||
MatPrefix,
|
MatPrefix,
|
||||||
FaIconComponent,
|
FaIconComponent,
|
||||||
MatIconButton,
|
|
||||||
MatProgressBar,
|
MatProgressBar,
|
||||||
MatProgressSpinner
|
MatProgressSpinner
|
||||||
]
|
]
|
||||||
@@ -38,7 +33,6 @@ import { environment } from '../../../environments/environment';
|
|||||||
export class LoginFormComponent implements OnInit {
|
export class LoginFormComponent implements OnInit {
|
||||||
private formBuilder = inject(FormBuilder);
|
private formBuilder = inject(FormBuilder);
|
||||||
private authenticationService = inject(AuthenticationService);
|
private authenticationService = inject(AuthenticationService);
|
||||||
private authService = inject(AuthService);
|
|
||||||
|
|
||||||
/** Login form group. */
|
/** Login form group. */
|
||||||
loginForm: FormGroup;
|
loginForm: FormGroup;
|
||||||
@@ -46,7 +40,8 @@ export class LoginFormComponent implements OnInit {
|
|||||||
passwordInputType: string = 'password';
|
passwordInputType: string = 'password';
|
||||||
/** True if loading. */
|
/** True if loading. */
|
||||||
loading = false;
|
loading = false;
|
||||||
oidcServerEnabled = environment.OIDC.oidcServerEnabled;
|
/** Whether OAuth (OIDC or OAuth2) is enabled */
|
||||||
|
oauthEnabled = environment.OIDC.oidcServerEnabled || environment.oauth.enabled;
|
||||||
/** Whether remember me functionality is enabled */
|
/** Whether remember me functionality is enabled */
|
||||||
enableRememberMe = environment.enableRememberMe === true;
|
enableRememberMe = environment.enableRememberMe === true;
|
||||||
|
|
||||||
@@ -80,19 +75,23 @@ export class LoginFormComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticates the user if the credentials are valid to Zitadel.
|
* Initiates OAuth/OIDC login flow.
|
||||||
|
* The unified AuthenticationService handles both Fineract OAuth2 and OIDC providers.
|
||||||
*/
|
*/
|
||||||
|
loginOAuth() {
|
||||||
loginOIDC() {
|
this.loading = true;
|
||||||
this.authService.login();
|
this.authenticationService
|
||||||
}
|
.login()
|
||||||
|
.pipe(
|
||||||
getUsers() {
|
finalize(() => {
|
||||||
this.authService.getUsers();
|
this.loading = false;
|
||||||
}
|
})
|
||||||
|
)
|
||||||
logout() {
|
.subscribe({
|
||||||
this.authService.logout();
|
error: () => {
|
||||||
|
// Error handling is managed by the authentication service
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
displayTenantSelector(): boolean {
|
displayTenantSelector(): boolean {
|
||||||
|
// Hide tenant selector when OAuth2 is enabled (tenant is determined by OAuth server)
|
||||||
|
if (environment.oauth.enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return environment.displayTenantSelector === 'false' ? false : true;
|
return environment.displayTenantSelector === 'false' ? false : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ import { animate, style, transition, trigger } from '@angular/animations';
|
|||||||
import { I18nService } from './core/i18n/i18n.service';
|
import { I18nService } from './core/i18n/i18n.service';
|
||||||
import { ThemingService } from './shared/theme-toggle/theming.service';
|
import { ThemingService } from './shared/theme-toggle/theming.service';
|
||||||
|
|
||||||
import { AuthService } from './zitadel/auth.service';
|
|
||||||
|
|
||||||
/** Initialize Logger */
|
/** Initialize Logger */
|
||||||
const log = new Logger('MifosX');
|
const log = new Logger('MifosX');
|
||||||
|
|
||||||
@@ -123,8 +121,7 @@ export class WebAppComponent implements OnInit, OnDestroy {
|
|||||||
private themingService: ThemingService,
|
private themingService: ThemingService,
|
||||||
private dateUtils: Dates,
|
private dateUtils: Dates,
|
||||||
private idle: IdleTimeoutService,
|
private idle: IdleTimeoutService,
|
||||||
private dialog: MatDialog,
|
private dialog: MatDialog
|
||||||
private authService: AuthService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@HostBinding('class') public cssClass: string;
|
@HostBinding('class') public cssClass: string;
|
||||||
@@ -141,10 +138,6 @@ export class WebAppComponent implements OnInit, OnDestroy {
|
|||||||
* 4) Alerts
|
* 4) Alerts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
login() {
|
|
||||||
this.authService.login();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.themingService.theme.subscribe((value: string) => {
|
this.themingService.theme.subscribe((value: string) => {
|
||||||
this.cssClass = value;
|
this.cssClass = value;
|
||||||
@@ -252,11 +245,7 @@ export class WebAppComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
this.dialog.open(SessionTimeoutDialogComponent);
|
this.dialog.open(SessionTimeoutDialogComponent);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!environment.OIDC.oidcServerEnabled) {
|
this.logout();
|
||||||
this.logout();
|
|
||||||
} else {
|
|
||||||
this.authService.logout();
|
|
||||||
}
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,190 +1,19 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { OAuthService } from 'angular-oauth2-oidc';
|
||||||
|
|
||||||
import { AlertService } from '../../app/core/alert/alert.service';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
|
|
||||||
/** Custom Services */
|
|
||||||
import { AuthenticationService } from '../core/authentication/authentication.service';
|
|
||||||
import { Credentials } from '../core/authentication/credentials.model';
|
|
||||||
import { OAuth2Token } from '../core/authentication/o-auth2-token.model';
|
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private authenticationService = inject(AuthenticationService);
|
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
private route = inject(ActivatedRoute);
|
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private alertService = inject(AlertService);
|
private oauthService = inject(OAuthService);
|
||||||
private translateService = inject(TranslateService);
|
|
||||||
|
|
||||||
private baseUrl = environment.OIDC.oidcBaseUrl;
|
|
||||||
private authUrl = `${this.baseUrl}oauth/v2/authorize`;
|
|
||||||
private tokenUrl = `${this.baseUrl}oauth/v2/token`;
|
|
||||||
private clientId = environment.OIDC.oidcClientId;
|
|
||||||
private api = environment.OIDC.oidcApiUrl;
|
private api = environment.OIDC.oidcApiUrl;
|
||||||
private frontUrl = environment.OIDC.oidcFrontUrl;
|
|
||||||
private redirectUri = `${this.frontUrl}#/callback`;
|
|
||||||
private refreshTimeoutId: any = null;
|
|
||||||
|
|
||||||
async login() {
|
|
||||||
const codeVerifier = this.generateRandomString();
|
|
||||||
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
|
|
||||||
sessionStorage.setItem('code_verifier', codeVerifier);
|
|
||||||
const url =
|
|
||||||
`${this.authUrl}` +
|
|
||||||
`?client_id=${encodeURIComponent(this.clientId)}` +
|
|
||||||
`&redirect_uri=${encodeURIComponent(this.redirectUri)}` +
|
|
||||||
`&response_type=code` +
|
|
||||||
`&scope=openid profile email offline_access` +
|
|
||||||
`&code_challenge=${codeChallenge}` +
|
|
||||||
`&code_challenge_method=S256`;
|
|
||||||
window.location.href = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
logout() {
|
|
||||||
const idToken = localStorage.getItem('id_token');
|
|
||||||
const postLogoutRedirectUri = this.frontUrl + '#/login';
|
|
||||||
|
|
||||||
if (this.refreshTimeoutId) {
|
|
||||||
clearTimeout(this.refreshTimeoutId);
|
|
||||||
this.refreshTimeoutId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!idToken) {
|
|
||||||
window.location.href = postLogoutRedirectUri;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionStorage.removeItem('mifosXCredentials');
|
|
||||||
sessionStorage.removeItem('mifosXZitadelTokenDetails');
|
|
||||||
localStorage.removeItem('access_token');
|
|
||||||
localStorage.removeItem('expires_in');
|
|
||||||
localStorage.removeItem('id_token');
|
|
||||||
localStorage.removeItem('refresh_token');
|
|
||||||
localStorage.removeItem('refresh_expires_in');
|
|
||||||
localStorage.removeItem('token_start_time');
|
|
||||||
localStorage.removeItem('code_verifier');
|
|
||||||
localStorage.removeItem('mifosXZitadel');
|
|
||||||
localStorage.removeItem('auth_code');
|
|
||||||
|
|
||||||
const logoutUrl = `${this.baseUrl}/oidc/v1/end_session?id_token_hint=${idToken}&post_logout_redirect_uri=${encodeURIComponent(postLogoutRedirectUri)}`;
|
|
||||||
window.location.href = logoutUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAccessToken(): string | null {
|
getAccessToken(): string | null {
|
||||||
const rawToken = sessionStorage.getItem('mifosXZitadelTokenDetails');
|
return this.oauthService.getAccessToken();
|
||||||
|
|
||||||
if (rawToken) {
|
|
||||||
const parsedToken: OAuth2Token = JSON.parse(rawToken);
|
|
||||||
return parsedToken.access_token;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
generateRandomString(length = 128) {
|
|
||||||
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
|
||||||
let result = '';
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
result += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateCodeChallenge(verifier: string): Promise<string> {
|
|
||||||
const data = new TextEncoder().encode(verifier);
|
|
||||||
const digest = await crypto.subtle.digest('SHA-256', data);
|
|
||||||
const base64 = btoa(String.fromCharCode(...new Uint8Array(digest)))
|
|
||||||
.replace(/\+/g, '-')
|
|
||||||
.replace(/\//g, '_')
|
|
||||||
.replace(/=+$/, '');
|
|
||||||
return base64;
|
|
||||||
}
|
|
||||||
|
|
||||||
async exchangeCodeForTokens(code: string, codeVerifier: string | null): Promise<void> {
|
|
||||||
const payload = {
|
|
||||||
code: code,
|
|
||||||
code_verifier: codeVerifier || ''
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(this.api + 'authentication/token', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Error exchanging code: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokens: {
|
|
||||||
access_token: string;
|
|
||||||
id_token: string;
|
|
||||||
refresh_token: string;
|
|
||||||
expires_in: number;
|
|
||||||
token_type: string;
|
|
||||||
} = await response.json();
|
|
||||||
|
|
||||||
const token: OAuth2Token = {
|
|
||||||
access_token: tokens.access_token,
|
|
||||||
token_type: tokens.token_type,
|
|
||||||
refresh_token: tokens.refresh_token,
|
|
||||||
expires_in: tokens.expires_in,
|
|
||||||
scope: 'Bearer'
|
|
||||||
};
|
|
||||||
|
|
||||||
sessionStorage.setItem('mifosXZitadelTokenDetails', JSON.stringify(token));
|
|
||||||
localStorage.setItem('id_token', tokens.id_token);
|
|
||||||
localStorage.setItem('refresh_token', tokens.refresh_token);
|
|
||||||
localStorage.setItem('mifosXZitadel', 'true');
|
|
||||||
this.scheduleRefresh(tokens.expires_in);
|
|
||||||
await this.userdetails();
|
|
||||||
} catch (error) {
|
|
||||||
window.location.href = '/#/login';
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
userdetails() {
|
|
||||||
const accessToken = this.getAccessToken();
|
|
||||||
if (!accessToken) {
|
|
||||||
console.error('No access token found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(this.api + 'authentication/userdetails', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ token: accessToken })
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (!res.ok) {
|
|
||||||
this.alertService.alert({
|
|
||||||
type: 'User Details',
|
|
||||||
message: this.translateService.instant('errors.Username or password incorrect.')
|
|
||||||
});
|
|
||||||
this.sesionEnd();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
.then((userInfo) => {
|
|
||||||
const user = userInfo.object;
|
|
||||||
const credentials: Credentials = user;
|
|
||||||
this.authenticationService.saveZitadelCredentials(credentials);
|
|
||||||
window.location.href = '/#/home';
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.alertService.alert({ type: 'User Details', message: error });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public deletUser(userId: string) {
|
public deletUser(userId: string) {
|
||||||
@@ -315,112 +144,11 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshToken(): Promise<void> {
|
async refreshToken(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const rt = localStorage.getItem('refresh_token');
|
|
||||||
|
|
||||||
if (!rt) {
|
|
||||||
//this.logout();
|
|
||||||
return reject('Sin refresh_token');
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = new URLSearchParams();
|
|
||||||
payload.set('grant_type', 'refresh_token');
|
|
||||||
payload.set('refresh_token', rt);
|
|
||||||
payload.set('client_id', this.clientId);
|
|
||||||
|
|
||||||
fetch(this.tokenUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
|
||||||
},
|
|
||||||
body: payload.toString()
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (!res.ok) {
|
|
||||||
return res.text().then((text) => {
|
|
||||||
throw new Error(text);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
.then((tokens) => {
|
|
||||||
if (!tokens || !tokens.access_token || !tokens.expires_in) {
|
|
||||||
throw new Error("The server's response does not contain the expected fields.");
|
|
||||||
}
|
|
||||||
localStorage.setItem('access_token', tokens.access_token);
|
|
||||||
localStorage.setItem('id_token', tokens.id_token ?? '');
|
|
||||||
localStorage.setItem('refresh_token', tokens.refresh_token ?? '');
|
|
||||||
localStorage.setItem('expires_in', tokens.expires_in.toString());
|
|
||||||
localStorage.setItem('refresh_expires_in', tokens.refresh_expires_in?.toString() ?? '');
|
|
||||||
localStorage.setItem('token_start_time', Date.now().toString());
|
|
||||||
this.scheduleRefresh(tokens.expires_in);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
//this.logout();
|
|
||||||
}, 300000);
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleRefresh(expiresIn: number) {
|
|
||||||
const refreshInMs = (expiresIn - 3539) * 1000;
|
|
||||||
if (refreshInMs <= 0) {
|
|
||||||
this.refreshToken();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.refreshTimeoutId) {
|
|
||||||
clearTimeout(this.refreshTimeoutId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refreshTimeoutId = setTimeout(() => {
|
|
||||||
this.refreshToken();
|
|
||||||
}, refreshInMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
async sesionEnd() {
|
|
||||||
const idToken = localStorage.getItem('id_token');
|
|
||||||
const postLogoutRedirectUri = this.frontUrl + '#/login';
|
|
||||||
|
|
||||||
if (this.refreshTimeoutId) {
|
|
||||||
clearTimeout(this.refreshTimeoutId);
|
|
||||||
this.refreshTimeoutId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!idToken) {
|
|
||||||
window.location.href = postLogoutRedirectUri;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logoutUrl = `${this.baseUrl}oidc/v1/end_session?id_token_hint=${idToken}&post_logout_redirect_uri=${encodeURIComponent(postLogoutRedirectUri)}`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(logoutUrl, {
|
await this.oauthService.refreshToken();
|
||||||
method: 'GET',
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error('Error en logout:', response.status, response.statusText);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error de red en logout:', error);
|
throw error;
|
||||||
} finally {
|
|
||||||
sessionStorage.removeItem('mifosXCredentials');
|
|
||||||
sessionStorage.removeItem('mifosXZitadelTokenDetails');
|
|
||||||
localStorage.removeItem('access_token');
|
|
||||||
localStorage.removeItem('expires_in');
|
|
||||||
localStorage.removeItem('id_token');
|
|
||||||
localStorage.removeItem('refresh_token');
|
|
||||||
localStorage.removeItem('refresh_expires_in');
|
|
||||||
localStorage.removeItem('token_start_time');
|
|
||||||
localStorage.removeItem('code_verifier');
|
|
||||||
localStorage.removeItem('mifosXZitadel');
|
|
||||||
localStorage.removeItem('auth_code');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,36 @@
|
|||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { AuthService } from '../auth.service';
|
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
||||||
|
import { AlertService } from '../../core/alert/alert.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-callback',
|
selector: 'mifosx-callback',
|
||||||
templateUrl: './callback.component.html'
|
templateUrl: './callback.component.html'
|
||||||
})
|
})
|
||||||
export class CallbackComponent implements OnInit {
|
export class CallbackComponent implements OnInit {
|
||||||
private route = inject(ActivatedRoute);
|
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private authService = inject(AuthService);
|
private alertService = inject(AlertService);
|
||||||
|
private authenticationService = inject(AuthenticationService);
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const code = sessionStorage.getItem('auth_code');
|
const success = await this.authenticationService.handleOAuthCallback();
|
||||||
if (code) {
|
|
||||||
const codeVerifier = sessionStorage.getItem('code_verifier');
|
if (success) {
|
||||||
try {
|
this.router.navigate(['/home']);
|
||||||
await this.authService.exchangeCodeForTokens(code, codeVerifier);
|
} else {
|
||||||
} finally {
|
this.alertService.alert({
|
||||||
// Clean up sensitive data immediately after use
|
type: 'Authentication Failed',
|
||||||
sessionStorage.removeItem('auth_code');
|
message: 'Unable to complete authentication. Please try again.'
|
||||||
sessionStorage.removeItem('code_verifier');
|
});
|
||||||
}
|
this.router.navigate(['/login']);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Authentication failed:', error);
|
console.error('Authentication callback failed:', error);
|
||||||
// Navigate to login page
|
this.alertService.alert({
|
||||||
|
type: 'Authentication Error',
|
||||||
|
message: 'An error occurred during authentication. Please try again.'
|
||||||
|
});
|
||||||
this.router.navigate(['/login']);
|
this.router.navigate(['/login']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export default {
|
export default {
|
||||||
'mifos_x': {
|
'mifos_x': {
|
||||||
'version': '251204',
|
'version': '251211',
|
||||||
'hash': '925391ff'
|
'hash': 'g667887c5f'
|
||||||
},
|
},
|
||||||
'allow_switching_backend_instance': true
|
'allow_switching_backend_instance': true
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,9 +26,14 @@ export const environment = {
|
|||||||
apiActuator: loadedEnv.apiActuator || '/fineract-provider',
|
apiActuator: loadedEnv.apiActuator || '/fineract-provider',
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
oauth: {
|
oauth: {
|
||||||
enabled: loadedEnv['oauthServerEnabled'] || false, // For connecting to Mifos X using OAuth2 Authentication change the value to true
|
enabled: loadedEnv.oauthServerEnabled === true,
|
||||||
serverUrl: loadedEnv['oauthServerUrl'] || '',
|
serverUrl: loadedEnv.oauthServerUrl || '',
|
||||||
appId: loadedEnv['oauthAppId'] || ''
|
logoutUrl: loadedEnv.oauthServerLogoutUrl || '',
|
||||||
|
appId: loadedEnv.oauthAppId || '',
|
||||||
|
authorizeUrl: loadedEnv.oauthAuthorizeUrl || '',
|
||||||
|
tokenUrl: loadedEnv.oauthTokenUrl || '',
|
||||||
|
redirectUri: loadedEnv.oauthRedirectUri || '',
|
||||||
|
scope: loadedEnv.oauthScope || ''
|
||||||
},
|
},
|
||||||
/** Feature flag for Remember Me functionality */
|
/** Feature flag for Remember Me functionality */
|
||||||
enableRememberMe: false,
|
enableRememberMe: false,
|
||||||
@@ -66,7 +71,7 @@ export const environment = {
|
|||||||
minPasswordLength: loadedEnv['minPasswordLength'] || 12,
|
minPasswordLength: loadedEnv['minPasswordLength'] || 12,
|
||||||
|
|
||||||
OIDC: {
|
OIDC: {
|
||||||
oidcServerEnabled: window['env']['oidcServerEnabled'] || false,
|
oidcServerEnabled: window['env']['oidcServerEnabled'] === true || window['env']['oidcServerEnabled'] === 'true',
|
||||||
oidcBaseUrl: window['env']['oidcBaseUrl'] || '',
|
oidcBaseUrl: window['env']['oidcBaseUrl'] || '',
|
||||||
oidcClientId: window['env']['oidcClientId'] || '',
|
oidcClientId: window['env']['oidcClientId'] || '',
|
||||||
oidcApiUrl: window['env']['oidcApiUrl'] || '',
|
oidcApiUrl: window['env']['oidcApiUrl'] || '',
|
||||||
|
|||||||
@@ -32,9 +32,14 @@ export const environment = {
|
|||||||
/** Feature flag for Remember Me functionality */
|
/** Feature flag for Remember Me functionality */
|
||||||
enableRememberMe: false,
|
enableRememberMe: false,
|
||||||
oauth: {
|
oauth: {
|
||||||
enabled: loadedEnv.oauthServerEnabled || false, // For connecting to Mifos X using OAuth2 Authentication change the value to true
|
enabled: loadedEnv.oauthServerEnabled === true,
|
||||||
serverUrl: loadedEnv.oauthServerUrl || '',
|
serverUrl: loadedEnv.oauthServerUrl || '',
|
||||||
appId: loadedEnv.oauthAppId || ''
|
logoutUrl: loadedEnv.oauthServerLogoutUrl || '',
|
||||||
|
appId: loadedEnv.oauthAppId || '',
|
||||||
|
authorizeUrl: loadedEnv.oauthAuthorizeUrl || '',
|
||||||
|
tokenUrl: loadedEnv.oauthTokenUrl || '',
|
||||||
|
redirectUri: loadedEnv.oauthRedirectUri || '',
|
||||||
|
scope: loadedEnv.oauthScope || ''
|
||||||
},
|
},
|
||||||
warningDialog: {
|
warningDialog: {
|
||||||
title: 'Warning',
|
title: 'Warning',
|
||||||
@@ -70,7 +75,7 @@ export const environment = {
|
|||||||
minPasswordLength: loadedEnv.minPasswordLength || 12,
|
minPasswordLength: loadedEnv.minPasswordLength || 12,
|
||||||
|
|
||||||
OIDC: {
|
OIDC: {
|
||||||
oidcServerEnabled: window['env']['oidcServerEnabled'] || false,
|
oidcServerEnabled: window['env']['oidcServerEnabled'] === true || window['env']['oidcServerEnabled'] === 'true',
|
||||||
oidcBaseUrl: window['env']['oidcBaseUrl'] || '',
|
oidcBaseUrl: window['env']['oidcBaseUrl'] || '',
|
||||||
oidcClientId: window['env']['oidcClientId'] || '',
|
oidcClientId: window['env']['oidcClientId'] || '',
|
||||||
oidcApiUrl: window['env']['oidcApiUrl'] || '',
|
oidcApiUrl: window['env']['oidcApiUrl'] || '',
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
const code = getQueryParam('code');
|
const code = getQueryParam('code');
|
||||||
if (code) {
|
if (code) {
|
||||||
sessionStorage.setItem('auth_code', code);
|
sessionStorage.setItem('oauth_callback_query', window.location.search);
|
||||||
window.location.href = '/#/callback';
|
window.location.href = '/#/callback';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user