mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[ACS-6927] - Fully compliant with OIDC: ADF (#9452)
* [ACS-6927] - Fully compliant with OIDC: ADF * Fix after CR
This commit is contained in:
committed by
GitHub
parent
aab03cc864
commit
deea720dac
@@ -1579,10 +1579,24 @@
|
||||
"description": " Defines whether every url provided by the discovery document has to start with the issuer's url."
|
||||
},
|
||||
"implicitFlow": {
|
||||
"type": ["boolean", "string"]
|
||||
"type": ["boolean", "string"],
|
||||
"description": "Enables the Implicit Flow for authentication, suitable for client-side apps where the client secret cannot be stored securely. It directly returns the access token."
|
||||
},
|
||||
"codeFlow": {
|
||||
"type": ["boolean", "string"]
|
||||
"type": ["boolean", "string"],
|
||||
"description": "Activates the Authorization Code Flow, recommended for most applications, including those that can store the client secret securely. It involves an extra step to exchange the authorization code for an access token, enhancing security."
|
||||
},
|
||||
"logoutUrl": {
|
||||
"type": "string",
|
||||
"description": "Identifies the intended recipients of the token, typically the URI of the targeted API. This ensures the token is used only for accessing the specified resources."
|
||||
},
|
||||
"logoutParameters": {
|
||||
"type": "array",
|
||||
"description": "Defines the parameters for the logout request, enabling customization to meet specific identity provider (IdP) requirements. Typical parameters include 'client_id', 'returnTo', and 'response_type'. This allows for adaptable configuration without code changes, ensuring compatibility across different OAuth2/OIDC flows."
|
||||
},
|
||||
"audience": {
|
||||
"type": "string",
|
||||
"description": "The URL where users are redirected after logging out, for example, https://your-auth.auth0.com/v2/logout. It defines the post-logout navigation flow."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -25,6 +25,9 @@ export interface OauthConfigModel {
|
||||
silentLogin?: boolean;
|
||||
secret?: string;
|
||||
redirectUriLogout?: string;
|
||||
logoutUrl?: string;
|
||||
audience?: string;
|
||||
logoutParameters?: Array<string>;
|
||||
redirectSilentIframeUri?: string;
|
||||
refreshTokenTimeout?: number;
|
||||
publicUrls: string[];
|
||||
|
@@ -65,6 +65,10 @@ export class AuthConfigService {
|
||||
clientId: oauth2.clientId,
|
||||
scope: oauth2.scope,
|
||||
dummyClientSecret: oauth2.secret || '',
|
||||
logoutUrl: oauth2.logoutUrl,
|
||||
customQueryParams: {
|
||||
audience: oauth2.audience
|
||||
},
|
||||
...(oauth2.codeFlow && { responseType: 'code' })
|
||||
});
|
||||
}
|
||||
@@ -76,13 +80,17 @@ export class AuthConfigService {
|
||||
|
||||
const oauth2 = this.appConfigService.oauth2;
|
||||
|
||||
const directUrl = oauth2.redirectUri?.startsWith('http');
|
||||
if (directUrl) {
|
||||
return oauth2.redirectUri;
|
||||
}
|
||||
|
||||
const locationOrigin = oauth2.redirectUri && oauth2.redirectUri !== '/' ? this.getLocationOrigin() + '' + oauth2.redirectUri : this.getLocationOrigin();
|
||||
|
||||
const redirectUri = useHash
|
||||
? `${locationOrigin}/#/${viewUrl}`
|
||||
: `${locationOrigin}/${viewUrl}`;
|
||||
|
||||
|
||||
// handle issue from the OIDC library with hashStrategy and implicitFlow, with would append &state to the url with would lead to error
|
||||
// `cannot match any routes`, and displaying the wildcard ** error page
|
||||
return (oauth2.codeFlow || oauth2.implicitFlow) && useHash ? `${redirectUri}/?` : redirectUri;
|
||||
|
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { AuthConfig, AUTH_CONFIG, OAuthModule, OAuthService, OAuthStorage } from 'angular-oauth2-oidc';
|
||||
import { AUTH_CONFIG, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
|
||||
import { AlfrescoApiNoAuthService } from '../../api-factories/alfresco-api-no-auth.service';
|
||||
import { AlfrescoApiService } from '../../services/alfresco-api.service';
|
||||
import { AuthenticationService } from '../services/authentication.service';
|
||||
@@ -31,14 +31,11 @@ import { AuthenticationConfirmationComponent } from './view/authentication-confi
|
||||
/**
|
||||
* Create a Login Factory function
|
||||
*
|
||||
* @param oAuthService auth service
|
||||
* @param storage storage service
|
||||
* @param config auth configuration
|
||||
* @param redirectService auth redirect service
|
||||
* @returns a factory function
|
||||
*/
|
||||
export function loginFactory(oAuthService: OAuthService, storage: OAuthStorage, config: AuthConfig) {
|
||||
const service = new RedirectAuthService(oAuthService, storage, config);
|
||||
return () => service.init();
|
||||
export function loginFactory(redirectService: RedirectAuthService): () => Promise<boolean> {
|
||||
return () => redirectService.init();
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
@@ -58,7 +55,7 @@ export function loginFactory(oAuthService: OAuthService, storage: OAuthStorage,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: loginFactory,
|
||||
deps: [OAuthService, OAuthStorage, AUTH_CONFIG],
|
||||
deps: [RedirectAuthService],
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
|
@@ -78,7 +78,7 @@ export class RedirectAuthService extends AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
init() {
|
||||
init(): Promise<boolean> {
|
||||
if (isPromise(this.authConfig)) {
|
||||
return this.authConfig.then((config) => this.configureAuth(config));
|
||||
}
|
||||
@@ -152,7 +152,7 @@ export class RedirectAuthService extends AuthService {
|
||||
return DEFAULT_REDIRECT;
|
||||
}
|
||||
|
||||
private configureAuth(config: AuthConfig) {
|
||||
private configureAuth(config: AuthConfig): Promise<boolean> {
|
||||
this.oauthService.configure(config);
|
||||
this.oauthService.tokenValidationHandler = new JwksValidationHandler();
|
||||
|
||||
|
@@ -0,0 +1,129 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { OidcAuthenticationService } from './oidc-authentication.service';
|
||||
import { OAuthService, OAuthStorage } from 'angular-oauth2-oidc';
|
||||
import { AppConfigService, AuthService } from '@alfresco/adf-core';
|
||||
import { AUTH_MODULE_CONFIG } from '../oidc/auth-config';
|
||||
|
||||
interface MockAppConfigOAuth2 {
|
||||
oauth2: {
|
||||
logoutParameters: Array<string>;
|
||||
};
|
||||
}
|
||||
|
||||
class MockAppConfigService {
|
||||
config: MockAppConfigOAuth2 = {
|
||||
oauth2: {
|
||||
logoutParameters: ['client_id', 'returnTo', 'response_type']
|
||||
}
|
||||
};
|
||||
|
||||
setConfig(newConfig: { logoutParameters: Array<string> }) {
|
||||
this.config.oauth2 = newConfig;
|
||||
}
|
||||
|
||||
get(key: string, defaultValue?: { logoutParameters: Array<string> }) {
|
||||
if (key === 'oauth2') {
|
||||
return this.config.oauth2;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MockOAuthService {
|
||||
clientId = 'testClientId';
|
||||
redirectUri = 'testRedirectUri';
|
||||
logOut = jasmine.createSpy();
|
||||
}
|
||||
|
||||
describe('OidcAuthenticationService', () => {
|
||||
let service: OidcAuthenticationService;
|
||||
let oauthService: OAuthService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
OidcAuthenticationService,
|
||||
{ provide: AppConfigService, useClass: MockAppConfigService },
|
||||
{ provide: OAuthService, useClass: MockOAuthService },
|
||||
{ provide: OAuthStorage, useValue: {} },
|
||||
{ provide: AUTH_MODULE_CONFIG, useValue: {} },
|
||||
{ provide: AuthService, useValue: {} }
|
||||
]
|
||||
});
|
||||
service = TestBed.inject(OidcAuthenticationService);
|
||||
oauthService = TestBed.inject(OAuthService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
let mockAppConfigService: MockAppConfigService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockAppConfigService = TestBed.inject(AppConfigService) as any;
|
||||
});
|
||||
|
||||
it('should handle logout with default parameters', () => {
|
||||
service.logout();
|
||||
expect(oauthService.logOut).toHaveBeenCalledWith({
|
||||
client_id: 'testClientId',
|
||||
returnTo: 'testRedirectUri',
|
||||
response_type: 'code'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle logout with additional parameter redirect_uri', () => {
|
||||
mockAppConfigService.setConfig({
|
||||
logoutParameters: ['client_id', 'returnTo', 'redirect_uri', 'response_type']
|
||||
});
|
||||
|
||||
service.logout();
|
||||
|
||||
expect(oauthService.logOut).toHaveBeenCalledWith({
|
||||
client_id: 'testClientId',
|
||||
returnTo: 'testRedirectUri',
|
||||
redirect_uri: 'testRedirectUri',
|
||||
response_type: 'code'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle logout with an empty configuration object', () => {
|
||||
mockAppConfigService.setConfig({ logoutParameters: [] });
|
||||
|
||||
service.logout();
|
||||
|
||||
expect(oauthService.logOut).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it('should ignore undefined parameters', () => {
|
||||
mockAppConfigService.setConfig({
|
||||
logoutParameters: ['client_id', 'unknown_param']
|
||||
});
|
||||
service.logout();
|
||||
|
||||
expect(oauthService.logOut).toHaveBeenCalledWith({
|
||||
client_id: 'testClientId'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -17,7 +17,7 @@
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { OAuthService, OAuthStorage } from 'angular-oauth2-oidc';
|
||||
import { EMPTY, Observable, defer } from 'rxjs';
|
||||
import { Observable, defer, EMPTY } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service';
|
||||
import { OauthConfigModel } from '../models/oauth-config.model';
|
||||
@@ -151,7 +151,8 @@ export class OidcAuthenticationService extends BaseAuthenticationService {
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.oauthService.logOut();
|
||||
const logoutOptions = this.getLogoutOptions();
|
||||
this.oauthService.logOut(logoutOptions);
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
@@ -191,4 +192,32 @@ export class OidcAuthenticationService extends BaseAuthenticationService {
|
||||
return header.set('Authorization', 'bearer ' + token);
|
||||
}
|
||||
|
||||
private getLogoutOptions(): object {
|
||||
const oauth2Config = this.appConfig.get<OauthConfigModel>(AppConfigValues.OAUTHCONFIG, null);
|
||||
const logoutParamsList = oauth2Config?.logoutParameters || [];
|
||||
|
||||
return logoutParamsList.reduce((options, param) => {
|
||||
const value = this.getLogoutParamValue(param);
|
||||
if (value !== undefined) {
|
||||
options[param] = value;
|
||||
}
|
||||
return options;
|
||||
}, {});
|
||||
}
|
||||
|
||||
private getLogoutParamValue(param: string): string | undefined {
|
||||
switch (param) {
|
||||
case 'client_id':
|
||||
return this.oauthService.clientId;
|
||||
case 'returnTo':
|
||||
return this.oauthService.redirectUri;
|
||||
case 'redirect_uri':
|
||||
return this.oauthService.redirectUri;
|
||||
case 'response_type':
|
||||
return 'code';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user