[ACS-6927] - Fully compliant with OIDC: ADF (#9452)

* [ACS-6927] - Fully compliant with OIDC: ADF

* Fix after CR
This commit is contained in:
dominikiwanekhyland 2024-03-22 16:18:19 +01:00 committed by GitHub
parent aab03cc864
commit deea720dac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 392 additions and 16 deletions

View File

@ -28,6 +28,11 @@ if [ -n "${APP_CONFIG_OAUTH2_CLIENTID}" ]; then
-i "${NGINX_ENVSUBST_OUTPUT_DIR}/app.config.json"
fi
if [ -n "${APP_CONFIG_OAUTH2_CLIENT_SECRET}" ]; then
sed -e "s/\"secret\": \".*\"/\"secret\": \"${APP_CONFIG_OAUTH2_CLIENT_SECRET}\"/g" \
-i "${NGINX_ENVSUBST_OUTPUT_DIR}/app.config.json"
fi
if [ -n "${APP_CONFIG_OAUTH2_IMPLICIT_FLOW}" ]; then
sed -e "s/\"implicitFlow\": [^,]*/\"implicitFlow\": ${APP_CONFIG_OAUTH2_IMPLICIT_FLOW}/g" \
-i "${NGINX_ENVSUBST_OUTPUT_DIR}/app.config.json"
@ -38,6 +43,26 @@ if [ -n "${APP_CONFIG_OAUTH2_CODE_FLOW}" ]; then
-i "${NGINX_ENVSUBST_OUTPUT_DIR}/app.config.json"
fi
if [ -n "${APP_CONFIG_OAUTH2_LOGOUT_URL}" ]; then
sed -e "s/\"logoutUrl\": [^,]*/\"logoutUrl\": ${APP_CONFIG_OAUTH2_LOGOUT_URL}/g" \
-i "${NGINX_ENVSUBST_OUTPUT_DIR}/app.config.json"
fi
if [ -n "${APP_CONFIG_OAUTH2_LOGOUT_PARAMETERS}" ]; then
sed -e "s/\"logoutParameters\": [^,]*/\"logoutParameters\": ${APP_CONFIG_OAUTH2_LOGOUT_PARAMETERS}/g" \
-i "${NGINX_ENVSUBST_OUTPUT_DIR}/app.config.json"
fi
if [ -n "${APP_CONFIG_OAUTH2_AUDIENCE}" ]; then
sed -e "s/\"audience\": [^,]*/\"audience\": ${APP_CONFIG_OAUTH2_AUDIENCE}/g" \
-i "${NGINX_ENVSUBST_OUTPUT_DIR}/app.config.json"
fi
if [ -n "${APP_CONFIG_OAUTH2_SCOPE}" ]; then
sed -e "s/\"scope\": [^,]*/\"scope\": ${APP_CONFIG_OAUTH2_SCOPE}/g" \
-i "${NGINX_ENVSUBST_OUTPUT_DIR}/app.config.json"
fi
if [ -n "${APP_CONFIG_OAUTH2_SILENT_LOGIN}" ]; then
sed -e "s/\"silentLogin\": [^,]*/\"silentLogin\": ${APP_CONFIG_OAUTH2_SILENT_LOGIN}/g" \
-i "${NGINX_ENVSUBST_OUTPUT_DIR}/app.config.json"

View File

@ -13,8 +13,13 @@ docker run --rm -it \
--env APP_CONFIG_IDENTITY_HOST=$APP_CONFIG_IDENTITY_HOST \
--env APP_CONFIG_OAUTH2_HOST=$APP_CONFIG_OAUTH2_HOST \
--env APP_CONFIG_OAUTH2_CLIENTID=$APP_CONFIG_OAUTH2_CLIENTID \
--env APP_CONFIG_OAUTH2_CLIENT_SECRET=$APP_CONFIG_OAUTH2_SECRET \
--env APP_CONFIG_OAUTH2_IMPLICIT_FLOW=$APP_CONFIG_OAUTH2_IMPLICIT_FLOW \
--env APP_CONFIG_OAUTH2_IMPLICIT_FLOW=$APP_CONFIG_OAUTH2_CODE_FLOW \
--env APP_CONFIG_OAUTH2_CODE_FLOW=$APP_CONFIG_OAUTH2_CODE_FLOW \
--env APP_CONFIG_OAUTH2_LOGOUT_URL=$APP_CONFIG_OAUTH2_LOGOUT_URL \
--env APP_CONFIG_OAUTH2_LOGOUT_PARAMETERS=$APP_CONFIG_OAUTH2_LOGOUT_PARAMETERS \
--env APP_CONFIG_OAUTH2_AUDIENCE=$APP_CONFIG_OAUTH2_AUDIENCE \
--env APP_CONFIG_OAUTH2_SCOPE=$APP_CONFIG_OAUTH2_SCOPE \
--env APP_CONFIG_OAUTH2_SILENT_LOGIN=$APP_CONFIG_OAUTH2_SILENT_LOGIN \
--env APP_CONFIG_OAUTH2_REDIRECT_SILENT_IFRAME_URI=$APP_CONFIG_OAUTH2_REDIRECT_SILENT_IFRAME_URI \
--env APP_CONFIG_BPM_HOST=$APP_CONFIG_BPM_HOST \

View File

@ -0,0 +1,166 @@
---
Title: Authentication Configuration
---
# Authentication Configuration
This guide outlines the configuration of authentication methods within the application, focusing on OAuth2 and its parameters as defined in app.config.json. It also provides examples for integrating with different identity providers (IdPs) such as Keycloak and Auth0.
# Authentication Types
The authType parameter specifies the authentication method, with BASIC and OAUTH as possible values. The default setting is BASIC.
```json
{
"authType": "OAUTH"
}
```
# OAuth2 Configuration
OAuth2 is a protocol that allows the application to authorize operations without exposing user credentials. The configuration includes several parameters essential for setting up OAuth2 authentication.
## Required Parameters
host: The base URL of the authorization server.
clientId: The ID assigned to the application by the authorization server.
scope: The scope of the access request.
## Optional Parameters
oidc: Defines the use of OpenID Connect during the implicit flow.
issuer: The issuer's URI.
silentLogin: Enables silent authentication.
secret: The application's secret, used for secure authentication.
redirectUri: Where to redirect after a successful login.
postLogoutRedirectUri: Where to redirect after logging out.
refreshTokenTimeout, silentRefreshRedirectUri, silentRefreshTimeout: Control refresh token behavior.
publicUrls: URLs that do not require authentication.
dummyClientSecret: A workaround for auth servers requiring a client secret for the password flow.
skipIssuerCheck: Whether to skip issuer validation in the discovery document.
strictDiscoveryDocumentValidation: Ensures all URLs in the discovery document start with the issuer's URL.
implicitFlow, codeFlow: Configure the flow for authentication.
logoutUrl: The URL for logging out.
logoutParameters: Specifies parameters to be included in the logout request as an array of strings, such as ["client_id", "returnTo", "response_type"]. This allows for dynamic configuration of logout parameters tailored to specific IdP requirements.
audience: Identifies the recipients of the token.
# Examples
## Keycloak Configuration
```json
{
"authType": "OAUTH",
"oauth2": {
"host": "{protocol}//{hostname}{:port}/auth/realms/alfresco",
"clientId": "alfresco",
"scope": "openid profile email",
"implicitFlow": false,
"codeFlow": true,
"silentLogin": true,
"publicUrls": ["**/preview/s/*", "**/settings"],
"redirectSilentIframeUri": "{protocol}//{hostname}{:port}/assets/silent-refresh.html",
"redirectUri": "/",
"redirectUriLogout": "/",
"skipIssuerCheck": true,
"strictDiscoveryDocumentValidation": false
}
}
```
## Auth0 Configuration
```json
{
"authType": "OAUTH",
"oauth2": {
"host": "https://your-idp.auth0.com",
"clientId": "",
"secret": "",
"scope": "openid profile email offline_access",
"implicitFlow": false,
"codeFlow": true,
"silentLogin": true,
"publicUrls": [
"**/preview/s/*",
"**/settings"
],
"redirectSilentIframeUri": "{protocol}//{hostname}{:port}/assets/silent-refresh.html",
"redirectUri": "/",
"redirectUriLogout": "/",
"logoutUrl": "https://your-idp.auth0.com/v2/logout",
"logoutParameters": ["client_id", "returnTo"],
"audience": "http://localhost:3000",
"skipIssuerCheck": true,
"strictDiscoveryDocumentValidation": false
}
}
```
## Cognito Configuration
```json
{
"oauth2": {
"host": "https://cognito-idp.your-idp-url",
"clientId": "",
"secret": "",
"scope": "openid profile email",
"implicitFlow": false,
"codeFlow": true,
"silentLogin": true,
"publicUrls": ["**/preview/s/*", "**/settings"],
"redirectSilentIframeUri": "{protocol}//{hostname}{:port}/assets/silent-refresh.html",
"redirectUri": "http://your-env-name/view/authentication-confirmation/",
"redirectUriLogout": "/",
"logoutParameters": ["client_id", "redirect_uri", "response_type"],
"logoutUrl": "https://your-idp-url/oauth2/logout",
"skipIssuerCheck": true,
"strictDiscoveryDocumentValidation": false
}
}
```
### Handling Redirects with Amazon Cognito
When integrating with Amazon Cognito, special handling is required to ensure that the application can properly process authentication confirmation redirects, particularly when using hash-based routing in Angular applications. Due to Cognito's restrictions on redirect URLs, which do not allow fragments (#), you may encounter issues when the redirect URI points directly to a route within a single-page application (SPA) that relies on hash-based navigation.
To address this, include the following script tag within the <head> section of your index.html file. This script checks the current URL path for a specific pattern (view/authentication-confirmation) and modifies the URL to include a hash (#) if it's missing, ensuring the application correctly handles the redirect after Cognito authentication:
```html
<script>
(function() {
if (window.location.pathname.includes('view/authentication-confirmation') && !window.location.pathname.includes('#')) {
window.location.replace('/#' + window.location.pathname + window.location.search);
}
})();
</script>
```
# Docker Environment Variables
These settings can be customized in a Docker environment using the following environment variables:
APP_CONFIG_OAUTH2_HOST
APP_CONFIG_OAUTH2_CLIENTID
APP_CONFIG_OAUTH2_CLIENT_SECRET
APP_CONFIG_OAUTH2_IMPLICIT_FLOW
APP_CONFIG_OAUTH2_CODE_FLOW
APP_CONFIG_OAUTH2_AUDIENCE
APP_CONFIG_OAUTH2_SCOPE
APP_CONFIG_OAUTH2_LOGOUT_URL
APP_CONFIG_OAUTH2_LOGOUT_PARAMETERS
APP_CONFIG_OAUTH2_SILENT_LOGIN
APP_CONFIG_OAUTH2_REDIRECT_SILENT_IFRAME_URI
APP_CONFIG_OAUTH2_REDIRECT_LOGIN
APP_CONFIG_OAUTH2_REDIRECT_LOGOUT
Adjust the above examples according to your specific environment and authentication provider settings. These configurations ensure that the application can securely authenticate users through OAuth2, aligning with the current best practices in web application security.

View File

@ -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."
}
}
},

View File

@ -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[];

View File

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

View File

@ -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
}
]

View File

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

View File

@ -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'
});
});
});
});

View File

@ -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;
}
}
}