diff --git a/package-lock.json b/package-lock.json index ebde9e34f..3f2bcff3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@swimlane/ngx-graph": "^11.0.0", "@tailwindcss/forms": "^0.5.4", "@tinymce/tinymce-angular": "^9.1.1", + "angular-oauth2-oidc": "^20.0.0", "chart.js": "^4.5.1", "d3": "^7.9.0", "exceljs": "^4.4.0", @@ -9897,6 +9898,19 @@ "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": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", diff --git a/package.json b/package.json index 8b65122d4..51ea6fa75 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@swimlane/ngx-graph": "^11.0.0", "@tailwindcss/forms": "^0.5.4", "@tinymce/tinymce-angular": "^9.1.1", + "angular-oauth2-oidc": "^20.0.0", "chart.js": "^4.5.1", "d3": "^7.9.0", "exceljs": "^4.4.0", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1f0e7868e..939867af8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -58,6 +58,7 @@ import { TokenInterceptor as ZitadelTokenInterceptor } from './zitadel/token.int import { AuthService } from './zitadel/auth.service'; import { environment } from '../environments/environment'; import { CallbackComponent } from './zitadel/callback/callback.component'; +import { OAuthModule } from 'angular-oauth2-oidc'; export class CustomMissingTranslationHandler implements MissingTranslationHandler { handle(params: MissingTranslationHandlerParams): string { @@ -120,7 +121,8 @@ export function HttpLoaderFactory(http: HttpClient) { ConfigurationWizardModule, AppRoutingModule, NotFoundComponent, - CallbackComponent + CallbackComponent, + OAuthModule.forRoot() ], providers: [ DatePipe, diff --git a/src/app/core/authentication/authentication.service.ts b/src/app/core/authentication/authentication.service.ts index ae3d2e557..c85dc82e3 100644 --- a/src/app/core/authentication/authentication.service.ts +++ b/src/app/core/authentication/authentication.service.ts @@ -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 = new BehaviorSubject(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} True if authentication is successful. */ - login(loginContext: LoginContext) { + login(loginContext?: LoginContext): Observable { 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} Promise that resolves when user details are fetched. + */ + private async getUserDetails(): Promise { + 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(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} 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 { + 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; } } diff --git a/src/app/core/authentication/o-auth2-token.model.ts b/src/app/core/authentication/o-auth2-token.model.ts index f41198f14..e8566ded8 100644 --- a/src/app/core/authentication/o-auth2-token.model.ts +++ b/src/app/core/authentication/o-auth2-token.model.ts @@ -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; } diff --git a/src/app/core/authentication/oauth.config.ts b/src/app/core/authentication/oauth.config.ts new file mode 100644 index 000000000..877429b42 --- /dev/null +++ b/src/app/core/authentication/oauth.config.ts @@ -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 + }; +} diff --git a/src/app/core/http/api-prefix.interceptor.ts b/src/app/core/http/api-prefix.interceptor.ts index ab2734683..fb13efd41 100644 --- a/src/app/core/http/api-prefix.interceptor.ts +++ b/src/app/core/http/api-prefix.interceptor.ts @@ -31,6 +31,7 @@ export class ApiPrefixInterceptor implements HttpInterceptor { /** * 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:')) { request = request.clone({ url: baseUrl + request.url }); diff --git a/src/app/core/shell/sidenav/sidenav.component.ts b/src/app/core/shell/sidenav/sidenav.component.ts index 6660c6455..20ce1fe32 100644 --- a/src/app/core/shell/sidenav/sidenav.component.ts +++ b/src/app/core/shell/sidenav/sidenav.component.ts @@ -24,9 +24,7 @@ import { MatIcon } from '@angular/material/icon'; import { MatLine } from '@angular/material/grid-list'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; -/** Environment Configuration and Zitadel*/ -import { environment } from '../../../../environments/environment'; -import { AuthService } from 'app/zitadel/auth.service'; +import { catchError, finalize, of, take } from 'rxjs'; /** * Sidenav component. @@ -56,7 +54,6 @@ export class SidenavComponent implements OnInit, AfterViewInit { private settingsService = inject(SettingsService); private configurationWizardService = inject(ConfigurationWizardService); private popoverService = inject(PopoverService); - private authService = inject(AuthService); /** True if sidenav is in collapsed state. */ @Input() sidenavCollapsed: boolean; @@ -103,13 +100,17 @@ export class SidenavComponent implements OnInit, AfterViewInit { /** * Logs out the authenticated user and redirects to login page. + * Uses unified AuthenticationService which handles both OAuth2 and OIDC logout. */ logout() { - if (!environment.OIDC.oidcServerEnabled) { - this.authenticationService.logout().subscribe(() => this.router.navigate(['/login'], { replaceUrl: true })); - } else { - this.authService.logout(); - } + this.authenticationService + .logout() + .pipe( + take(1), + catchError(() => of(void 0)), + finalize(() => this.router.navigate(['/login'], { replaceUrl: true })) + ) + .subscribe(); } /** diff --git a/src/app/core/shell/toolbar/toolbar.component.ts b/src/app/core/shell/toolbar/toolbar.component.ts index b599651a7..8d981e4fa 100644 --- a/src/app/core/shell/toolbar/toolbar.component.ts +++ b/src/app/core/shell/toolbar/toolbar.component.ts @@ -19,8 +19,8 @@ import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { Router, RouterLink } from '@angular/router'; /** rxjs Imports */ -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; +import { catchError, finalize, map, take } from 'rxjs/operators'; /** Custom Services */ 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 { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; -/** Environment Configuration and Zitadel*/ import { environment } from '../../../../environments/environment'; -import { AuthService } from 'app/zitadel/auth.service'; /** * Toolbar component. @@ -77,7 +75,6 @@ export class ToolbarComponent implements OnInit, AfterViewInit, AfterContentChec private configurationWizardService = inject(ConfigurationWizardService); private dialog = inject(MatDialog); private changeDetector = inject(ChangeDetectorRef); - private authService = inject(AuthService); /* Reference of institution */ @ViewChild('institution') institution: ElementRef; @@ -134,13 +131,17 @@ export class ToolbarComponent implements OnInit, AfterViewInit, AfterContentChec /** * Logs out the authenticated user and redirects to login page. + * Uses unified AuthenticationService which handles both OAuth2 and OIDC logout. */ logout() { - if (!environment.OIDC.oidcServerEnabled) { - this.authenticationService.logout().subscribe(() => this.router.navigate(['/login'], { replaceUrl: true })); - } else { - this.authService.logout(); - } + this.authenticationService + .logout() + .pipe( + take(1), + catchError(() => of(void 0)), + finalize(() => this.router.navigate(['/login'], { replaceUrl: true })) + ) + .subscribe(); } /** diff --git a/src/app/login/login-form/login-form.component.html b/src/app/login/login-form/login-form.component.html index 79fcb59aa..8cb47499d 100644 --- a/src/app/login/login-form/login-form.component.html +++ b/src/app/login/login-form/login-form.component.html @@ -1,4 +1,5 @@ -@if (!oidcServerEnabled) { + +@if (!oauthEnabled) {
} -@if (oidcServerEnabled) { -
- @if (!isLoggedIn) { -
- -
- } - @if (isLoggedIn) { -
-

Welcome! You are logged in.

- - -
{{ protectedData | json }}
-
- } + +@if (oauthEnabled) { +
+
} diff --git a/src/app/login/login-form/login-form.component.ts b/src/app/login/login-form/login-form.component.ts index 633e14fe3..a747b9b97 100644 --- a/src/app/login/login-form/login-form.component.ts +++ b/src/app/login/login-form/login-form.component.ts @@ -1,22 +1,18 @@ /** Angular Imports */ import { Component, OnInit, inject } from '@angular/core'; -import { FormGroup, FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; /** rxjs Imports */ import { finalize } from 'rxjs/operators'; /** Custom Services */ 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 { MatIconButton, MatButton } from '@angular/material/button'; -import { MatCheckbox } from '@angular/material/checkbox'; import { MatProgressBar } from '@angular/material/progress-bar'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; -/** Custom Service Zitadel */ -import { AuthService } from '../../zitadel/auth.service'; import { environment } from '../../../environments/environment'; /** @@ -30,7 +26,6 @@ import { environment } from '../../../environments/environment'; ...STANDALONE_SHARED_IMPORTS, MatPrefix, FaIconComponent, - MatIconButton, MatProgressBar, MatProgressSpinner ] @@ -38,7 +33,6 @@ import { environment } from '../../../environments/environment'; export class LoginFormComponent implements OnInit { private formBuilder = inject(FormBuilder); private authenticationService = inject(AuthenticationService); - private authService = inject(AuthService); /** Login form group. */ loginForm: FormGroup; @@ -46,7 +40,8 @@ export class LoginFormComponent implements OnInit { passwordInputType: string = 'password'; /** True if loading. */ 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 */ 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. */ - - loginOIDC() { - this.authService.login(); - } - - getUsers() { - this.authService.getUsers(); - } - - logout() { - this.authService.logout(); + loginOAuth() { + this.loading = true; + this.authenticationService + .login() + .pipe( + finalize(() => { + this.loading = false; + }) + ) + .subscribe({ + error: () => { + // Error handling is managed by the authentication service + } + }); } /** diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index 4cfd7b293..855d002cc 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -102,6 +102,10 @@ export class LoginComponent implements OnInit, OnDestroy { } 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; } diff --git a/src/app/web-app.component.ts b/src/app/web-app.component.ts index 1362f6994..af3525190 100644 --- a/src/app/web-app.component.ts +++ b/src/app/web-app.component.ts @@ -33,8 +33,6 @@ import { animate, style, transition, trigger } from '@angular/animations'; import { I18nService } from './core/i18n/i18n.service'; import { ThemingService } from './shared/theme-toggle/theming.service'; -import { AuthService } from './zitadel/auth.service'; - /** Initialize Logger */ const log = new Logger('MifosX'); @@ -123,8 +121,7 @@ export class WebAppComponent implements OnInit, OnDestroy { private themingService: ThemingService, private dateUtils: Dates, private idle: IdleTimeoutService, - private dialog: MatDialog, - private authService: AuthService + private dialog: MatDialog ) {} @HostBinding('class') public cssClass: string; @@ -141,10 +138,6 @@ export class WebAppComponent implements OnInit, OnDestroy { * 4) Alerts */ - login() { - this.authService.login(); - } - ngOnInit() { this.themingService.theme.subscribe((value: string) => { this.cssClass = value; @@ -252,11 +245,7 @@ export class WebAppComponent implements OnInit, OnDestroy { }); this.dialog.open(SessionTimeoutDialogComponent); setTimeout(() => { - if (!environment.OIDC.oidcServerEnabled) { - this.logout(); - } else { - this.authService.logout(); - } + this.logout(); }, 1000); }); } diff --git a/src/app/zitadel/auth.service.ts b/src/app/zitadel/auth.service.ts index be39beff9..0c961846a 100644 --- a/src/app/zitadel/auth.service.ts +++ b/src/app/zitadel/auth.service.ts @@ -1,190 +1,19 @@ import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http'; 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'; @Injectable({ providedIn: 'root' }) export class AuthService { - private authenticationService = inject(AuthenticationService); private http = inject(HttpClient); - private route = inject(ActivatedRoute); private router = inject(Router); - private alertService = inject(AlertService); - 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 oauthService = inject(OAuthService); 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 { - const rawToken = sessionStorage.getItem('mifosXZitadelTokenDetails'); - - 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 { - 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 { - 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 }); - }); + return this.oauthService.getAccessToken(); } public deletUser(userId: string) { @@ -315,112 +144,11 @@ export class AuthService { }); } - refreshToken(): Promise { - 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)}`; - + async refreshToken(): Promise { try { - const response = await fetch(logoutUrl, { - method: 'GET', - credentials: 'include' - }); - - if (!response.ok) { - console.error('Error en logout:', response.status, response.statusText); - } + await this.oauthService.refreshToken(); } catch (error) { - console.error('Error de red en logout:', 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'); + throw error; } } } diff --git a/src/app/zitadel/callback/callback.component.ts b/src/app/zitadel/callback/callback.component.ts index c0a13e17c..c8a0b04e6 100644 --- a/src/app/zitadel/callback/callback.component.ts +++ b/src/app/zitadel/callback/callback.component.ts @@ -1,32 +1,36 @@ import { Component, OnInit, inject } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { AuthService } from '../auth.service'; +import { Router } from '@angular/router'; +import { AuthenticationService } from '../../core/authentication/authentication.service'; +import { AlertService } from '../../core/alert/alert.service'; @Component({ selector: 'mifosx-callback', templateUrl: './callback.component.html' }) export class CallbackComponent implements OnInit { - private route = inject(ActivatedRoute); private router = inject(Router); - private authService = inject(AuthService); + private alertService = inject(AlertService); + private authenticationService = inject(AuthenticationService); async ngOnInit(): Promise { try { - const code = sessionStorage.getItem('auth_code'); - if (code) { - const codeVerifier = sessionStorage.getItem('code_verifier'); - try { - await this.authService.exchangeCodeForTokens(code, codeVerifier); - } finally { - // Clean up sensitive data immediately after use - sessionStorage.removeItem('auth_code'); - sessionStorage.removeItem('code_verifier'); - } + const success = await this.authenticationService.handleOAuthCallback(); + + if (success) { + this.router.navigate(['/home']); + } else { + this.alertService.alert({ + type: 'Authentication Failed', + message: 'Unable to complete authentication. Please try again.' + }); + this.router.navigate(['/login']); } } catch (error) { - console.error('Authentication failed:', error); - // Navigate to login page + console.error('Authentication callback failed:', error); + this.alertService.alert({ + type: 'Authentication Error', + message: 'An error occurred during authentication. Please try again.' + }); this.router.navigate(['/login']); } } diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 5371ca608..b456d270b 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -2,8 +2,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '251204', - 'hash': '925391ff' + 'version': '251211', + 'hash': 'g667887c5f' }, 'allow_switching_backend_instance': true }; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 085e61e04..5927fa0f3 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -26,9 +26,14 @@ export const environment = { apiActuator: loadedEnv.apiActuator || '/fineract-provider', serverUrl: '', oauth: { - enabled: loadedEnv['oauthServerEnabled'] || false, // For connecting to Mifos X using OAuth2 Authentication change the value to true - serverUrl: loadedEnv['oauthServerUrl'] || '', - appId: loadedEnv['oauthAppId'] || '' + enabled: loadedEnv.oauthServerEnabled === true, + serverUrl: loadedEnv.oauthServerUrl || '', + logoutUrl: loadedEnv.oauthServerLogoutUrl || '', + appId: loadedEnv.oauthAppId || '', + authorizeUrl: loadedEnv.oauthAuthorizeUrl || '', + tokenUrl: loadedEnv.oauthTokenUrl || '', + redirectUri: loadedEnv.oauthRedirectUri || '', + scope: loadedEnv.oauthScope || '' }, /** Feature flag for Remember Me functionality */ enableRememberMe: false, @@ -66,7 +71,7 @@ export const environment = { minPasswordLength: loadedEnv['minPasswordLength'] || 12, OIDC: { - oidcServerEnabled: window['env']['oidcServerEnabled'] || false, + oidcServerEnabled: window['env']['oidcServerEnabled'] === true || window['env']['oidcServerEnabled'] === 'true', oidcBaseUrl: window['env']['oidcBaseUrl'] || '', oidcClientId: window['env']['oidcClientId'] || '', oidcApiUrl: window['env']['oidcApiUrl'] || '', diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 9791b34f1..78b8d25d8 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -32,9 +32,14 @@ export const environment = { /** Feature flag for Remember Me functionality */ enableRememberMe: false, 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 || '', - appId: loadedEnv.oauthAppId || '' + logoutUrl: loadedEnv.oauthServerLogoutUrl || '', + appId: loadedEnv.oauthAppId || '', + authorizeUrl: loadedEnv.oauthAuthorizeUrl || '', + tokenUrl: loadedEnv.oauthTokenUrl || '', + redirectUri: loadedEnv.oauthRedirectUri || '', + scope: loadedEnv.oauthScope || '' }, warningDialog: { title: 'Warning', @@ -70,7 +75,7 @@ export const environment = { minPasswordLength: loadedEnv.minPasswordLength || 12, OIDC: { - oidcServerEnabled: window['env']['oidcServerEnabled'] || false, + oidcServerEnabled: window['env']['oidcServerEnabled'] === true || window['env']['oidcServerEnabled'] === 'true', oidcBaseUrl: window['env']['oidcBaseUrl'] || '', oidcClientId: window['env']['oidcClientId'] || '', oidcApiUrl: window['env']['oidcApiUrl'] || '', diff --git a/src/index.html b/src/index.html index 870489224..62e89c33a 100644 --- a/src/index.html +++ b/src/index.html @@ -25,7 +25,7 @@ } const code = getQueryParam('code'); if (code) { - sessionStorage.setItem('auth_code', code); + sessionStorage.setItem('oauth_callback_query', window.location.search); window.location.href = '/#/callback'; }