[AAE-12511] implement OIDC authentication capabilities in ADF (#7856)

* feat: add custom AlfrescoApiHttpClient [ci:force]

* feat: update configs

* feat: move api to follow second entry point structure

* feat: add auth module [ci:force]

* Fix rebasing issues

* Isolate oidc package as subfolder

* Canary mode

* [AAE-12498] Fix unit test should load external settings: resolve reponse data instead returning default config

* [AAE-12498] Set @nrwl/eslint-plugin-nx@14.5.4 version to fix lint job that failed because of the 14.8.6 version (https://github.com/Alfresco/alfresco-ng2-components/actions/runs/4165060892/jobs/7207651856\#step:5:3379)

* [AAE-12498] Fix stories:build-storybook:ci issues

* [AAE-7991] cherry-pick e935f7b0b1f56d3bb124d566b248420de7bd0359 from repo https://github.com/Alfresco/alfresco-ng2-components/pull/7818: send onLogin to initialize acs version to fix [C362242] on canary configuration

* [AAE-12498] Fix security hotspot: fix unsafe pseudorandom number generator

* test: add missing tests for oidc-auth.guard

* test: fix lint issues

* chore: remove assignment in return

* [AAE-12498] Remove warning comment because we already know we're doing breaking changes

* [AAE-12498] Add auth-config.service unit tests

* [AAE-12498] Remove getUserProfile from auth service

---------

Co-authored-by: Andras Popovics <popovics@ndras.hu>
Co-authored-by: Amedeo Lepore <amedeo.lepore@hyland.com>
This commit is contained in:
Mikołaj Serwicki 2023-03-07 09:53:11 +01:00 committed by GitHub
parent dd91f2eeb6
commit f4a8084f0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 15034 additions and 17744 deletions

View File

@ -205,6 +205,14 @@
} }
] ]
}, },
"canary": {
"fileReplacements": [
{
"replace": "demo-shell/src/environments/environment.ts",
"with": "demo-shell/src/environments/environment.canary.ts"
}
]
},
"e2e": { "e2e": {
"budgets": [ "budgets": [
{ {
@ -242,6 +250,9 @@
"production": { "production": {
"browserTarget": "demoshell:build:production" "browserTarget": "demoshell:build:production"
}, },
"canary": {
"browserTarget": "demoshell:build:canary"
},
"e2e": { "e2e": {
"browserTarget": "demoshell:build:e2e" "browserTarget": "demoshell:build:e2e"
} }

View File

@ -63,9 +63,11 @@
"highlightable", "highlightable",
"hotfix", "hotfix",
"imgpreview", "imgpreview",
"Inplace",
"intitem", "intitem",
"jira", "jira",
"jsons", "jsons",
"jwks",
"keycodes", "keycodes",
"keyvaluepairs", "keyvaluepairs",
"keyvaluepairsitem", "keyvaluepairsitem",
@ -77,8 +79,6 @@
"mincount", "mincount",
"minlength", "minlength",
"minmax", "minmax",
"jsons",
"Inplace",
"MLTEXT", "MLTEXT",
"mousedrag", "mousedrag",
"mouseenter", "mouseenter",
@ -87,6 +87,7 @@
"nginx", "nginx",
"numbervisibilityprocess", "numbervisibilityprocess",
"OAUTHCONFIG", "OAUTHCONFIG",
"oidc",
"pdfjs", "pdfjs",
"penta", "penta",
"printf", "printf",

View File

@ -20,7 +20,7 @@ import { APP_INITIALIZER, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { FlexLayoutModule } from '@angular/flex-layout'; import { FlexLayoutModule } from '@angular/flex-layout';
import { ChartsModule } from 'ng2-charts'; import { ChartsModule } from 'ng2-charts';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { import {
@ -29,7 +29,7 @@ import {
DebugAppConfigService, DebugAppConfigService,
CoreModule, CoreModule,
CoreAutomationService, CoreAutomationService,
AuthBearerInterceptor AuthModule
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { ExtensionsModule } from '@alfresco/adf-extensions'; import { ExtensionsModule } from '@alfresco/adf-extensions';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
@ -140,6 +140,7 @@ registerLocaleData(localeSv);
environment.e2e ? NoopAnimationsModule : BrowserAnimationsModule, environment.e2e ? NoopAnimationsModule : BrowserAnimationsModule,
ReactiveFormsModule, ReactiveFormsModule,
RouterModule.forRoot(appRoutes, { useHash: true, relativeLinkResolution: 'legacy' }), RouterModule.forRoot(appRoutes, { useHash: true, relativeLinkResolution: 'legacy' }),
...(environment.oidc ? [AuthModule.forRoot({ useHash: true })] : []),
FormsModule, FormsModule,
HttpClientModule, HttpClientModule,
MaterialModule, MaterialModule,
@ -211,10 +212,6 @@ registerLocaleData(localeSv);
SearchFilterChipsComponent SearchFilterChipsComponent
], ],
providers: [ providers: [
{
provide: HTTP_INTERCEPTORS, useClass:
AuthBearerInterceptor, multi: true
},
{ provide: AppConfigService, useClass: DebugAppConfigService }, // not use this service in production { provide: AppConfigService, useClass: DebugAppConfigService }, // not use this service in production
{ {
provide: TRANSLATION_PROVIDER, provide: TRANSLATION_PROVIDER,

View File

@ -119,6 +119,12 @@
formControlName="implicitFlow"> formControlName="implicitFlow">
</mat-slide-toggle> </mat-slide-toggle>
<ng-container *ngIf="supportsCodeFlow">
<label for="codeFlow">{{ 'CORE.HOST_SETTINGS.CODE-FLOW'| translate }}</label>
<mat-slide-toggle class="adf-full-width" name="codeFlow" [color]="'primary'"
formControlName="codeFlow">
</mat-slide-toggle>
</ng-container>
<mat-form-field class="adf-full-width"> <mat-form-field class="adf-full-width">
<mat-label>{{ 'APP.HOST_SETTINGS.REDIRECT'| translate }}</mat-label> <mat-label>{{ 'APP.HOST_SETTINGS.REDIRECT'| translate }}</mat-label>

View File

@ -17,7 +17,7 @@
import { Component, EventEmitter, Output, ViewEncapsulation, OnInit, Input } from '@angular/core'; import { Component, EventEmitter, Output, ViewEncapsulation, OnInit, Input } from '@angular/core';
import { Validators, UntypedFormGroup, UntypedFormBuilder, UntypedFormControl } from '@angular/forms'; import { Validators, UntypedFormGroup, UntypedFormBuilder, UntypedFormControl } from '@angular/forms';
import { AppConfigService, AppConfigValues, StorageService, AlfrescoApiService, OauthConfigModel } from '@alfresco/adf-core'; import { AppConfigService, AppConfigValues, StorageService, AlfrescoApiService, OauthConfigModel, AuthenticationService } from '@alfresco/adf-core';
import { ENTER } from '@angular/cdk/keycodes'; import { ENTER } from '@angular/cdk/keycodes';
export const HOST_REGEX = '^(http|https):\/\/.*[^/]$'; export const HOST_REGEX = '^(http|https):\/\/.*[^/]$';
@ -57,11 +57,13 @@ export class HostSettingsComponent implements OnInit {
// eslint-disable-next-line @angular-eslint/no-output-native // eslint-disable-next-line @angular-eslint/no-output-native
success = new EventEmitter<boolean>(); success = new EventEmitter<boolean>();
constructor(private formBuilder: UntypedFormBuilder, constructor(
private storageService: StorageService, private formBuilder: UntypedFormBuilder,
private alfrescoApiService: AlfrescoApiService, private storageService: StorageService,
private appConfig: AppConfigService) { private alfrescoApiService: AlfrescoApiService,
} private appConfig: AppConfigService,
private auth: AuthenticationService
) {}
ngOnInit() { ngOnInit() {
if (this.providers.length === 1) { if (this.providers.length === 1) {
@ -146,6 +148,7 @@ export class HostSettingsComponent implements OnInit {
secret: oauth.secret, secret: oauth.secret,
silentLogin: oauth.silentLogin, silentLogin: oauth.silentLogin,
implicitFlow: oauth.implicitFlow, implicitFlow: oauth.implicitFlow,
codeFlow: oauth.codeFlow,
publicUrls: [oauth.publicUrls] publicUrls: [oauth.publicUrls]
}); });
} }
@ -185,6 +188,7 @@ export class HostSettingsComponent implements OnInit {
this.storageService.setItem(AppConfigValues.AUTHTYPE, values.authType); this.storageService.setItem(AppConfigValues.AUTHTYPE, values.authType);
this.alfrescoApiService.reset(); this.alfrescoApiService.reset();
this.auth.reset();
this.alfrescoApiService.getInstance().invalidateSession(); this.alfrescoApiService.getInstance().invalidateSession();
this.success.emit(true); this.success.emit(true);
} }
@ -228,6 +232,10 @@ export class HostSettingsComponent implements OnInit {
return this.form.get('authType').value === 'OAUTH'; return this.form.get('authType').value === 'OAUTH';
} }
get supportsCodeFlow(): boolean {
return this.auth.supportCodeFlow;
}
get providersControl(): UntypedFormControl { get providersControl(): UntypedFormControl {
return this.form.get('providersControl') as UntypedFormControl; return this.form.get('providersControl') as UntypedFormControl;
} }
@ -264,6 +272,10 @@ export class HostSettingsComponent implements OnInit {
return this.oauthConfig.get('implicitFlow') as UntypedFormControl; return this.oauthConfig.get('implicitFlow') as UntypedFormControl;
} }
get codeFlow(): UntypedFormControl {
return this.oauthConfig.get('codeFlow') as UntypedFormControl;
}
get silentLogin(): UntypedFormControl { get silentLogin(): UntypedFormControl {
return this.oauthConfig.get('silentLogin') as UntypedFormControl; return this.oauthConfig.get('silentLogin') as UntypedFormControl;
} }

View File

@ -15,5 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
export * from './public-api'; export const environment = {
production: true,
e2e: false,
oidc: true
};

View File

@ -17,5 +17,6 @@
export const environment = { export const environment = {
production: false, production: false,
e2e: true e2e: true,
oidc: false
}; };

View File

@ -17,5 +17,6 @@
export const environment = { export const environment = {
production: true, production: true,
e2e: false e2e: false,
oidc: false
}; };

View File

@ -22,5 +22,6 @@
export const environment = { export const environment = {
production: false, production: false,
e2e: false e2e: false,
oidc: false
}; };

3
lib/core/api/README.md Normal file
View File

@ -0,0 +1,3 @@
# @alfresco/adf-core/api
Secondary entry point of `@alfresco/adf-core`. It can be used by importing from `@alfresco/adf-core/api`.

View File

@ -1,6 +1,5 @@
{ {
"$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": { "lib": {
"entryFile": "public-api.ts" "entryFile": "src/index.ts"
} }
} }

View File

@ -15,8 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
export * from './api-client.factory'; export * from './lib/api-client.factory';
export * from './api-clients.service'; export * from './lib/api-clients.service';
export * from './clients'; export * from './lib/clients';
export * from './types'; export * from './lib/types';
export * from './alfresco-api/alfresco-api.http-client'; export * from './lib/alfresco-api/alfresco-api.http-client';

View File

@ -181,6 +181,9 @@ describe('AlfrescoApiHttpClient', () => {
const req = controller.expectOne('http://example.com?autoRename=true&include=allowableOperations'); const req = controller.expectOne('http://example.com?autoRename=true&include=allowableOperations');
expect(req.request.method).toEqual('POST'); expect(req.request.method).toEqual('POST');
// filedata: (binary)
// include: allowableOperations
const body = req.request.body as HttpParams; const body = req.request.body as HttpParams;
expect(body.get('relativePath')).toBe(''); expect(body.get('relativePath')).toBe('');

View File

@ -15,6 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { ApiClientsService } from '../api-clients.service'; import { ApiClientsService } from '../api-clients.service';
import { ActivitiClientModule } from './activiti/activiti-client.module'; import { ActivitiClientModule } from './activiti/activiti-client.module';
@ -22,6 +23,11 @@ import { DiscoveryClientModule } from './discovery/discovery-client.module';
@NgModule({ @NgModule({
imports: [ imports: [
HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: 'CSRF-TOKEN',
headerName: 'X-CSRF-TOKEN'
}),
ActivitiClientModule, ActivitiClientModule,
DiscoveryClientModule DiscoveryClientModule
], ],

View File

@ -18,4 +18,3 @@
export * from './activiti/activiti-client.types'; export * from './activiti/activiti-client.types';
export * from './alfresco-js-clients.module'; export * from './alfresco-js-clients.module';
export * from './discovery/discovery-client.types'; export * from './discovery/discovery-client.types';

View File

@ -0,0 +1,38 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { AlfrescoApiHttpClient } from '@alfresco/adf-core/api';
import { StorageService } from '../common/services/storage.service';
import { AlfrescoApi, AlfrescoApiConfig } from '@alfresco/js-api';
import { Injectable } from '@angular/core';
import { AppConfigService } from '../app-config';
import { AlfrescoApiService } from '../services/alfresco-api.service';
@Injectable()
export class AlfrescoApiServiceWithAngularBasedHttpClient extends AlfrescoApiService {
constructor(
storage: StorageService,
appConfig: AppConfigService,
private readonly alfrescoApiHttpClient: AlfrescoApiHttpClient
) {
super(appConfig, storage);
}
override createInstance(config: AlfrescoApiConfig) {
return new AlfrescoApi(config, this.alfrescoApiHttpClient);
}
}

View File

@ -0,0 +1,62 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { AlfrescoApiConfig } from '@alfresco/js-api';
import { Injectable } from '@angular/core';
import { AppConfigService, AppConfigValues } from '../app-config/app-config.service';
import { OauthConfigModel } from '../auth/models/oauth-config.model';
import { AlfrescoApiService } from '../services/alfresco-api.service';
export function createAlfrescoApiInstance(angularAlfrescoApiService: AlfrescoApiLoaderService) {
return () => angularAlfrescoApiService.init();
}
@Injectable({
providedIn: 'root'
})
export class AlfrescoApiLoaderService {
constructor(private readonly appConfig: AppConfigService, private readonly apiService: AlfrescoApiService) {}
async init(): Promise<any> {
await this.appConfig.load();
return this.initAngularAlfrescoApi();
}
private initAngularAlfrescoApi() {
const oauth: OauthConfigModel = Object.assign({}, this.appConfig.get<OauthConfigModel>(AppConfigValues.OAUTHCONFIG, null));
if (oauth) {
oauth.redirectUri = window.location.origin + window.location.pathname;
oauth.redirectUriLogout = window.location.origin + window.location.pathname;
}
const config = new AlfrescoApiConfig({
provider: this.appConfig.get<string>(AppConfigValues.PROVIDERS),
hostEcm: this.appConfig.get<string>(AppConfigValues.ECMHOST),
hostBpm: this.appConfig.get<string>(AppConfigValues.BPMHOST),
authType: this.appConfig.get<string>(AppConfigValues.AUTHTYPE, 'BASIC'),
contextRootBpm: this.appConfig.get<string>(AppConfigValues.CONTEXTROOTBPM),
contextRoot: this.appConfig.get<string>(AppConfigValues.CONTEXTROOTECM),
disableCsrf: this.appConfig.get<boolean>(AppConfigValues.DISABLECSRF),
withCredentials: this.appConfig.get<boolean>(AppConfigValues.AUTH_WITH_CREDENTIALS, false),
domainPrefix: this.appConfig.get<string>(AppConfigValues.STORAGE_PREFIX),
oauth2: oauth
});
this.apiService.load(config);
}
}

View File

@ -0,0 +1,25 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { AppConfigService, AppConfigValues } from './app-config.service';
import { StorageService } from '../common/services/storage.service';
export function loadAppConfig(appConfigService: AppConfigService, storageService: StorageService) {
return () => appConfigService.load().then(() => {
storageService.prefix = appConfigService.get<string>(AppConfigValues.STORAGE_PREFIX, '');
});
}

View File

@ -192,8 +192,8 @@ export class AppConfigService {
this.http.get(configUrl).subscribe( this.http.get(configUrl).subscribe(
(data: any) => { (data: any) => {
this.status = Status.LOADED; this.status = Status.LOADED;
resolve(data);
this.onDataLoaded(data); this.onDataLoaded(data);
resolve(this.config);
}, },
() => { () => {
resolve(this.config); resolve(this.config);

View File

@ -27,14 +27,12 @@ import { catchError, mergeMap } from 'rxjs/operators';
@Injectable() @Injectable()
export class AuthBearerInterceptor implements HttpInterceptor { export class AuthBearerInterceptor implements HttpInterceptor {
private excludedUrlsRegex: RegExp[]; private excludedUrlsRegex: RegExp[];
private authService: AuthenticationService;
constructor(private injector: Injector) { } constructor(private injector: Injector, private authService: AuthenticationService) { }
private loadExcludedUrlsRegex() { private loadExcludedUrlsRegex() {
const excludedUrls: string[] = this.authService.getBearerExcludedUrls(); const excludedUrls = this.authService.getBearerExcludedUrls();
this.excludedUrlsRegex = excludedUrls.map((urlPattern) => new RegExp(urlPattern, 'gi')) || []; this.excludedUrlsRegex = excludedUrls.map((urlPattern) => new RegExp(urlPattern, 'i')) || [];
} }
intercept(req: HttpRequest<any>, next: HttpHandler): intercept(req: HttpRequest<any>, next: HttpHandler):
@ -51,7 +49,7 @@ export class AuthBearerInterceptor implements HttpInterceptor {
} }
const urlRequest = req.url; const urlRequest = req.url;
const shallPass: boolean = !!this.excludedUrlsRegex.find((regex) => regex.test(urlRequest)); const shallPass: boolean = this.excludedUrlsRegex.some((regex) => regex.test(urlRequest));
if (shallPass) { if (shallPass) {
return next.handle(req) return next.handle(req)
.pipe( .pipe(
@ -73,7 +71,19 @@ export class AuthBearerInterceptor implements HttpInterceptor {
} }
private appendJsonContentType(headers: HttpHeaders): HttpHeaders { private appendJsonContentType(headers: HttpHeaders): HttpHeaders {
return headers.set('Content-Type', 'application/json;charset=UTF-8');
// prevent adding any content type, to properly handle formData with boundary browser generated value,
// as adding any Content-Type its going to break the upload functionality
if (headers.get('Content-Type') === 'multipart/form-data') {
return headers.delete('Content-Type');
}
if (!headers.get('Content-Type')) {
return headers.set('Content-Type', 'application/json;charset=UTF-8');
}
return headers;
} }
} }

View File

@ -16,3 +16,4 @@
*/ */
export * from './public-api'; export * from './public-api';
export * from './oidc/public-api';

View File

@ -0,0 +1,50 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
export const mockAuthConfigImplicitFlow = {
host: 'http://localhost:3000/auth/realms/alfresco',
clientId: 'alfresco',
scope: 'openid profile email',
secret: '',
implicitFlow: true,
silentLogin: true,
redirectSilentIframeUri: 'http://localhost:3000/assets/silent-refresh.html',
redirectUri: '/',
redirectUriLogout: '#/logout',
publicUrls: [
'**/preview/s/*',
'**/settings',
'**/logout'
]
};
export const mockAuthConfigCodeFlow = {
host: 'http://localhost:3000/auth/realms/alfresco',
clientId: 'alfresco',
scope: 'openid profile email',
secret: '',
codeFlow: true,
silentLogin: true,
redirectSilentIframeUri: 'http://localhost:3000/assets/silent-refresh.html',
redirectUri: '/',
redirectUriLogout: '#/logout',
publicUrls: [
'**/preview/s/*',
'**/settings',
'**/logout'
]
};

View File

@ -35,7 +35,7 @@ export class AuthenticationMock extends AuthenticationService {
cookie: CookieService, cookie: CookieService,
logService: LogService logService: LogService
) { ) {
super(appConfig, storageService, alfrescoApi, cookie, logService); super(alfrescoApi, appConfig, cookie, logService, storageService);
} }
login(username: string, password: string): Observable<{ type: string; ticket: any }> { login(username: string, password: string): Observable<{ type: string; ticket: any }> {

View File

@ -20,6 +20,7 @@ export interface OauthConfigModel {
clientId: string; clientId: string;
scope: string; scope: string;
implicitFlow: boolean; implicitFlow: boolean;
codeFlow?: boolean;
redirectUri: string; redirectUri: string;
silentLogin?: boolean; silentLogin?: boolean;
secret?: string; secret?: string;

View File

@ -0,0 +1,84 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { HttpClientTestingModule } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { EMPTY } from 'rxjs';
import { AppConfigService } from '../../app-config/app-config.service';
import { AUTH_MODULE_CONFIG } from './auth-config';
import { mockAuthConfigCodeFlow, mockAuthConfigImplicitFlow } from '../mock/auth-config.service.mock';
import { AuthConfigService } from './auth-config.service';
describe('AuthConfigService', () => {
let service: AuthConfigService;
let appConfigServiceMock;
beforeEach(() => {
appConfigServiceMock = jasmine.createSpyObj(['get'], { onLoad: EMPTY });
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{ provide: AUTH_MODULE_CONFIG, useValue: { useHash: true } },
{ provide: AppConfigService, useValue: appConfigServiceMock }
]
});
service = TestBed.inject(AuthConfigService);
spyOn<any>(service, 'getLocationOrigin').and.returnValue('http://localhost:3000');
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('load auth config using hash', () => {
it('should load configuration if implicit flow is true ', async () => {
appConfigServiceMock.get.and.returnValue(mockAuthConfigImplicitFlow);
const expectedConfig = {
oidc: true,
issuer: 'http://localhost:3000/auth/realms/alfresco',
redirectUri: 'http://localhost:3000/#/view/authentication-confirmation/?',
silentRefreshRedirectUri: 'http://localhost:3000/silent-refresh.html',
postLogoutRedirectUri: 'http://localhost:3000/#/logout',
clientId: 'alfresco',
scope: 'openid profile email',
dummyClientSecret: ''
};
expect(await service.loadConfig()).toEqual(expectedConfig);
});
it('should load configuration if code flow is true ', async () => {
appConfigServiceMock.get.and.returnValue(mockAuthConfigCodeFlow);
const expectedConfig = {
oidc: true,
issuer: 'http://localhost:3000/auth/realms/alfresco',
redirectUri: 'http://localhost:3000/#/view/authentication-confirmation',
silentRefreshRedirectUri: 'http://localhost:3000/silent-refresh.html',
postLogoutRedirectUri: 'http://localhost:3000/#/logout',
clientId: 'alfresco',
scope: 'openid profile email',
responseType: 'code',
dummyClientSecret: ''
};
expect(await service.loadConfig()).toEqual(expectedConfig);
});
});
});

View File

@ -0,0 +1,86 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { Inject, Injectable } from '@angular/core';
import { AuthConfig } from 'angular-oauth2-oidc';
import { take } from 'rxjs/operators';
import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service';
import { OauthConfigModel } from '../models/oauth-config.model';
import { AuthModuleConfig, AUTH_MODULE_CONFIG } from './auth-config';
export function authConfigFactory(authConfigService: AuthConfigService): Promise<AuthConfig> {
return authConfigService.loadConfig();
}
@Injectable({
providedIn: 'root'
})
export class AuthConfigService {
constructor(
private appConfigService: AppConfigService,
@Inject(AUTH_MODULE_CONFIG) private readonly authModuleConfig: AuthModuleConfig
) {}
private _authConfig!: AuthConfig;
get authConfig(): AuthConfig {
return this._authConfig;
}
loadConfig(): Promise<AuthConfig> {
return this.appConfigService.onLoad.pipe(take(1)).toPromise().then(this.loadAppConfig.bind(this));
}
loadAppConfig(): AuthConfig {
const oauth2: OauthConfigModel = Object.assign({}, this.appConfigService.get<OauthConfigModel>(AppConfigValues.OAUTHCONFIG, null));
const origin = this.getLocationOrigin();
const redirectUri = this.getRedirectUri();
const authConfig: AuthConfig = {
oidc: oauth2.implicitFlow || oauth2.codeFlow || false,
issuer: oauth2.host,
redirectUri,
silentRefreshRedirectUri: `${origin}/silent-refresh.html`,
postLogoutRedirectUri: `${origin}/${oauth2.redirectUriLogout}`,
clientId: oauth2.clientId,
scope: oauth2.scope,
dummyClientSecret: oauth2.secret || '',
...(oauth2.codeFlow && { responseType: 'code' })
};
return authConfig;
}
getRedirectUri(): string {
// required for this package as we handle the returned token on this view, with is provided by the AuthModule
const viewUrl = `view/authentication-confirmation`;
const useHash = this.authModuleConfig.useHash;
const redirectUri = useHash
? `${this.getLocationOrigin()}/#/${viewUrl}`
: `${this.getLocationOrigin()}/${viewUrl}`;
const oauth2: OauthConfigModel = Object.assign({}, this.appConfigService.get<OauthConfigModel>(AppConfigValues.OAUTHCONFIG, null));
// 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.implicitFlow && useHash ? `${redirectUri}/?` : redirectUri;
}
private getLocationOrigin() {
return window.location.origin;
}
}

View File

@ -15,19 +15,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { Observable } from 'rxjs'; import { InjectionToken } from '@angular/core';
export interface AbstractAuthentication { export interface AuthModuleConfig {
TYPE: string; readonly useHash: boolean;
alfrescoApi: any;
login(username: string, password: string): Observable<any>;
logout(): Observable<any>;
isLoggedIn(): boolean ;
getTicket(): string;
saveTicket(ticket: any): void;
} }
export const AUTH_MODULE_CONFIG = new InjectionToken<AuthModuleConfig>('AUTH_MODULE_CONFIG');

View File

@ -0,0 +1,30 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthenticationConfirmationComponent } from './view/authentication-confirmation/authentication-confirmation.component';
const routes: Routes = [
{ path: 'view/authentication-confirmation', component: AuthenticationConfirmationComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthRoutingModule {}

View File

@ -0,0 +1,73 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
import { AuthConfig, AUTH_CONFIG, OAuthModule, OAuthService, OAuthStorage } from 'angular-oauth2-oidc';
import { AlfrescoApiServiceWithAngularBasedHttpClient } from '../../api-factories/alfresco-api-service-with-angular-based-http-client';
import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { AuthGuardBpm } from '../guard/auth-guard-bpm.service';
import { AuthGuardEcm } from '../guard/auth-guard-ecm.service';
import { AuthGuard } from '../guard/auth-guard.service';
import { AuthenticationService } from '../services/authentication.service';
import { StorageService } from '../../common/services/storage.service';
import { AuthModuleConfig, AUTH_MODULE_CONFIG } from './auth-config';
import { authConfigFactory, AuthConfigService } from './auth-config.service';
import { AuthRoutingModule } from './auth-routing.module';
import { AuthService } from './auth.service';
import { OidcAuthGuard } from './oidc-auth.guard';
import { OIDCAuthenticationService } from './oidc-authentication.service';
import { RedirectAuthService } from './redirect-auth.service';
import { AuthenticationConfirmationComponent } from './view/authentication-confirmation/authentication-confirmation.component';
export function loginFactory(oAuthService: OAuthService, storage: OAuthStorage, config: AuthConfig) {
const service = new RedirectAuthService(oAuthService, storage, config);
return () => service.init();
}
@NgModule({
declarations: [AuthenticationConfirmationComponent],
imports: [AuthRoutingModule, OAuthModule.forRoot()],
providers: [
{ provide: OAuthStorage, useExisting: StorageService },
{ provide: AuthGuard, useClass: OidcAuthGuard },
{ provide: AuthGuardEcm, useClass: OidcAuthGuard },
{ provide: AuthGuardBpm, useClass: OidcAuthGuard },
{ provide: AuthenticationService, useClass: OIDCAuthenticationService },
{ provide: AlfrescoApiService, useClass: AlfrescoApiServiceWithAngularBasedHttpClient },
{
provide: AUTH_CONFIG,
useFactory: authConfigFactory,
deps: [AuthConfigService]
},
RedirectAuthService,
{ provide: AuthService, useExisting: RedirectAuthService },
{
provide: APP_INITIALIZER,
useFactory: loginFactory,
deps: [OAuthService, OAuthStorage, AUTH_CONFIG],
multi: true
}
]
})
export class AuthModule {
static forRoot(config: AuthModuleConfig = { useHash: false }): ModuleWithProviders<AuthModule> {
return {
ngModule: AuthModule,
providers: [{ provide: AUTH_MODULE_CONFIG, useValue: config }]
};
}
}

View File

@ -0,0 +1,57 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { TokenResponse } from 'angular-oauth2-oidc';
import { Observable } from 'rxjs';
/**
* Provide authentication/authorization through OAuth2/OIDC protocol.
*/
export abstract class AuthService {
/** Subscribe to whether the user has valid Id/Access tokens. */
abstract authenticated$: Observable<boolean>;
/** Get whether the user has valid Id/Access tokens. */
abstract authenticated: boolean;
/** Subscribe to errors reaching the IdP. */
abstract idpUnreachable$: Observable<Error>;
/**
* Initiate the IdP login flow.
*/
abstract login(currentUrl?: string): Promise<void> | void;
abstract baseAuthLogin(username: string, password: string): Observable<TokenResponse> ;
/**
* Disconnect from IdP.
*
* @returns Promise may be returned depending on implementation
*/
abstract logout(): Promise<void> | void;
/**
* Complete the login flow.
*
* In browsers, checks URL for auth and stored state. Call this once the application returns from IdP.
*
* @returns Promise, resolve with stored state, reject if unable to reach IdP
*/
abstract loginCallback(): Promise<string | undefined>;
abstract updateIDPConfiguration(...args: any[]): void;
}

View File

@ -0,0 +1,72 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { AuthService } from './auth.service';
import { OidcAuthGuard } from './oidc-auth.guard';
const state: RouterStateSnapshot = {
root: new ActivatedRouteSnapshot(),
url: 'http://example.com'
};
const routeSnapshot = new ActivatedRouteSnapshot();
describe('OidcAuthGuard', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
providers: [OidcAuthGuard]
});
});
describe('#canActivate', () => {
it('should return false if the user is not authenticated, and call login method', () => {
const authService = { authenticated: false, login: jasmine.createSpy() } as unknown as AuthService;
const authGuard = new OidcAuthGuard(authService);
expect(authGuard.canActivate(routeSnapshot, state)).toEqual(false);
expect(authService.login).toHaveBeenCalled();
});
it('should return true if the user is authenticated', () => {
const authService = { authenticated: true } as unknown as AuthService;
const authGuard = new OidcAuthGuard(authService);
expect(authGuard.canActivate(routeSnapshot, state)).toEqual(true);
});
});
describe('#canActivateChild', () => {
it('should return false if the user is not authenticated, and call login method', () => {
const authService = { authenticated: false, login: jasmine.createSpy() } as unknown as AuthService;
const authGuard = new OidcAuthGuard(authService);
expect(authGuard.canActivateChild(routeSnapshot, state)).toEqual(false);
expect(authService.login).toHaveBeenCalled();
});
it('should return true if the user is authenticated', () => {
const authService = { authenticated: true } as unknown as AuthService;
const authGuard = new OidcAuthGuard(authService);
expect(authGuard.canActivateChild(routeSnapshot, state)).toEqual(true);
});
});
});

View File

@ -0,0 +1,52 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
@Injectable()
export class OidcAuthGuard implements CanActivate {
constructor(private auth: AuthService) {}
canActivate(
_route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this._isAuthenticated(state);
}
canActivateChild(_route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this._isAuthenticated(state);
}
private _isAuthenticated(state: RouterStateSnapshot) {
if (this.auth.authenticated) {
return true;
}
const loginResult = this.auth.login(state.url);
if (loginResult instanceof Promise) {
return loginResult.then(() => true).catch(() => false);
}
return false;
}
}

View File

@ -0,0 +1,130 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { Injectable } from '@angular/core';
import { OAuthService, OAuthStorage } from 'angular-oauth2-oidc';
import { EMPTY, Observable } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service';
import { OauthConfigModel } from '../models/oauth-config.model';
import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { BaseAuthenticationService } from '../../services/base-authentication.service';
import { CookieService } from '../../common/services/cookie.service';
import { JwtHelperService } from '../services/jwt-helper.service';
import { LogService } from '../../common/services/log.service';
import { AuthConfigService } from '../oidc/auth-config.service';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class OIDCAuthenticationService extends BaseAuthenticationService {
readonly supportCodeFlow = true;
constructor(
alfrescoApi: AlfrescoApiService,
appConfig: AppConfigService,
cookie: CookieService,
logService: LogService,
private authStorage: OAuthStorage,
private oauthService: OAuthService,
private readonly authConfig: AuthConfigService,
private readonly auth: AuthService
) {
super(alfrescoApi, appConfig, cookie, logService);
this.alfrescoApi.alfrescoApiInitialized.subscribe(() => {
this.alfrescoApi.getInstance().reply('logged-in', () => {
this.onLogin.next();
});
});
}
isEcmLoggedIn(): boolean {
return this.isLoggedIn();
}
isBpmLoggedIn(): boolean {
return this.isLoggedIn();
}
isLoggedIn(): boolean {
return this.oauthService.hasValidAccessToken() && this.oauthService.hasValidIdToken();
}
isLoggedInWith(_provider?: string): boolean {
return this.isLoggedIn();
}
isOauth(): boolean {
return this.appConfig.get(AppConfigValues.AUTHTYPE) === 'OAUTH';
}
isImplicitFlow() {
const oauth2: OauthConfigModel = Object.assign({}, this.appConfig.get<OauthConfigModel>(AppConfigValues.OAUTHCONFIG, null));
return !!oauth2?.implicitFlow;
}
isAuthCodeFlow() {
const oauth2: OauthConfigModel = Object.assign({}, this.appConfig.get<OauthConfigModel>(AppConfigValues.OAUTHCONFIG, null));
return !!oauth2?.codeFlow;
}
login(username: string, password: string, rememberMe: boolean = false): Observable<{ type: string; ticket: any }> {
return this.auth.baseAuthLogin(username, password).pipe(
map((response) => {
this.saveRememberMeCookie(rememberMe);
this.onLogin.next(response);
return {
type: this.appConfig.get(AppConfigValues.PROVIDERS),
ticket: response
};
}),
catchError((err) => this.handleError(err))
);
}
ssoImplicitLogin() {
this.oauthService.initLoginFlow();
}
ssoCodeFlowLogin() {
this.oauthService.initCodeFlow();
}
isRememberMeSet(): boolean {
return true;
}
logout() {
this.oauthService.logOut();
return EMPTY;
}
getToken(): string {
return this.authStorage.getItem(JwtHelperService.USER_ACCESS_TOKEN);
}
reset(): void {
const config = this.authConfig.loadAppConfig();
this.auth.updateIDPConfiguration(config);
const oauth2: OauthConfigModel = Object.assign({}, this.appConfig.get<OauthConfigModel>(AppConfigValues.OAUTHCONFIG, null));
if (config.oidc && oauth2.silentLogin) {
this.auth.login();
}
}
}

View File

@ -0,0 +1,23 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
export * from './auth-routing.module';
export * from './auth.module';
export * from './auth.service';
export * from './oidc-auth.guard';
export * from './redirect-auth.service';
export * from './view/authentication-confirmation/authentication-confirmation.component';

View File

@ -0,0 +1,162 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { Inject, Injectable } from '@angular/core';
import { AuthConfig, AUTH_CONFIG, OAuthErrorEvent, OAuthService, OAuthStorage, TokenResponse } from 'angular-oauth2-oidc';
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
import { from, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, shareReplay, startWith } from 'rxjs/operators';
import { AuthService } from './auth.service';
const isPromise = <T>(value: T | Promise<T>): value is Promise<T> => value && typeof (value as Promise<T>).then === 'function';
@Injectable()
export class RedirectAuthService extends AuthService {
private _loadDiscoveryDocumentPromise = Promise.resolve(false);
/** Subscribe to whether the user has valid Id/Access tokens. */
authenticated$!: Observable<boolean>;
/** Subscribe to errors reaching the IdP. */
idpUnreachable$!: Observable<Error>;
/** Get whether the user has valid Id/Access tokens. */
get authenticated(): boolean {
return this.oauthService.hasValidIdToken() && this.oauthService.hasValidAccessToken();
}
private authConfig!: AuthConfig | Promise<AuthConfig>;
constructor(
private oauthService: OAuthService,
private _oauthStorage: OAuthStorage,
@Inject(AUTH_CONFIG) authConfig: AuthConfig
) {
super();
this.authConfig = authConfig;
}
init() {
this.oauthService.clearHashAfterLogin = true;
this.authenticated$ = this.oauthService.events.pipe(
startWith(undefined),
map(() => this.authenticated),
distinctUntilChanged(),
shareReplay(1)
);
this.idpUnreachable$ = this.oauthService.events.pipe(
filter((event): event is OAuthErrorEvent => event.type === 'discovery_document_load_error'),
map((event) => event.reason as Error)
);
if (isPromise(this.authConfig)) {
return this.authConfig.then((config) => this.configureAuth(config));
}
return this.configureAuth(this.authConfig);
}
logout() {
this.oauthService.logOut();
}
ensureDiscoveryDocument(): Promise<boolean> {
this._loadDiscoveryDocumentPromise = this._loadDiscoveryDocumentPromise
.catch(() => false)
.then((loaded) => {
if (!loaded) {
return this.oauthService.loadDiscoveryDocument().then(() => true);
}
return true;
});
return this._loadDiscoveryDocumentPromise;
}
login(currentUrl?: string): void {
let stateKey: string | undefined;
if (currentUrl) {
const randomValue = window.crypto.getRandomValues(new Uint32Array(1))[0];
stateKey = `auth_state_${randomValue}${Date.now()}`;
this._oauthStorage.setItem(stateKey, JSON.stringify(currentUrl || {}));
}
// initLoginFlow will initialize the login flow in either code or implicit depending on the configuration
this.ensureDiscoveryDocument().then(() => void this.oauthService.initLoginFlow(stateKey));
}
baseAuthLogin(username: string, password: string): Observable<TokenResponse> {
this.oauthService.useHttpBasicAuth = true;
return from(this.oauthService.fetchTokenUsingPasswordFlow(username, password)).pipe(
map((response) => {
const props = new Map<string, string>();
props.set('id_token', response.id_token);
// for backward compatibility we need to set the response in our storage
this.oauthService['storeAccessTokenResponse'](response.access_token, response.refresh_token, response.expires_in, response.scope, props);
return response;
})
);
}
async loginCallback(): Promise<string | undefined> {
return this.ensureDiscoveryDocument()
.then(() => this.oauthService.tryLogin({ preventClearHashAfterLogin: false }))
.then(() => this._getRedirectUrl());
}
private _getRedirectUrl() {
const DEFAULT_REDIRECT = '/';
const stateKey = this.oauthService.state;
if (stateKey) {
const stateStringified = this._oauthStorage.getItem(stateKey);
if (stateStringified) {
// cleanup state from storage
this._oauthStorage.removeItem(stateKey);
return JSON.parse(stateStringified);
}
}
return DEFAULT_REDIRECT;
}
private configureAuth(config: AuthConfig) {
this.oauthService.configure(config);
this.oauthService.tokenValidationHandler = new JwksValidationHandler();
if (config.sessionChecksEnabled) {
this.oauthService.events.pipe(filter((event) => event.type === 'session_terminated')).subscribe(() => {
this.oauthService.logOut();
});
}
return this.ensureDiscoveryDocument().then(() =>
void this.oauthService.setupAutomaticSilentRefresh()
).catch(() => {
// catch error to prevent the app from crashing when trying to access unprotected routes
});
}
updateIDPConfiguration(config: AuthConfig) {
this.oauthService.configure(config);
}
}

View File

@ -0,0 +1,51 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { MockProvider } from 'ng-mocks';
import { AuthService } from '../../auth.service';
import { AuthenticationConfirmationComponent } from './authentication-confirmation.component';
describe('AuthenticationConfirmationComponent', () => {
let component: AuthenticationConfirmationComponent;
let fixture: ComponentFixture<AuthenticationConfirmationComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AuthenticationConfirmationComponent],
providers: [
MockProvider(AuthService, {
loginCallback() {
return Promise.resolve(undefined);
}
})
],
imports: [RouterTestingModule]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AuthenticationConfirmationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,41 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { ChangeDetectionStrategy, Component } from '@angular/core';
import { Router } from '@angular/router';
import { from, of } from 'rxjs';
import { catchError, first, map } from 'rxjs/operators';
import { AuthService } from '../../auth.service';
const ROUTE_DEFAULT = '/';
@Component({
template: '',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AuthenticationConfirmationComponent {
constructor(private auth: AuthService, private _router: Router) {
const routeStored$ = from(this.auth.loginCallback()).pipe(
map((route) => route || ROUTE_DEFAULT),
catchError(() => of(ROUTE_DEFAULT))
);
routeStored$.pipe(first()).subscribe((route) => {
this._router.navigateByUrl(route, { replaceUrl: true });
});
}
}

View File

@ -15,46 +15,32 @@
* limitations under the License. * limitations under the License.
*/ */
import { Authentication } from '@alfresco/adf-core/auth';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, from, throwError, Observer, ReplaySubject } from 'rxjs'; import { Observable, from } from 'rxjs';
import { AlfrescoApiService } from '../../services/alfresco-api.service'; import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { CookieService } from '../../common/services/cookie.service'; import { CookieService } from '../../common/services/cookie.service';
import { LogService } from '../../common/services/log.service'; import { LogService } from '../../common/services/log.service';
import { RedirectionModel } from '../models/redirection.model';
import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service'; import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service';
import { map, catchError, tap } from 'rxjs/operators'; import { map, catchError, tap } from 'rxjs/operators';
import { HttpHeaders } from '@angular/common/http';
import { JwtHelperService } from './jwt-helper.service'; import { JwtHelperService } from './jwt-helper.service';
import { StorageService } from '../../common/services/storage.service'; import { StorageService } from '../../common/services/storage.service';
import { OauthConfigModel } from '../models/oauth-config.model';
const REMEMBER_ME_COOKIE_KEY = 'ALFRESCO_REMEMBER_ME'; import { BaseAuthenticationService } from '../../services/base-authentication.service';
const REMEMBER_ME_UNTIL = 1000 * 60 * 60 * 24 * 30;
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AuthenticationService extends Authentication { export class AuthenticationService extends BaseAuthenticationService {
private redirectUrl: RedirectionModel = null; readonly supportCodeFlow = false;
private bearerExcludedUrls: string[] = ['auth/realms', 'resources/', 'assets/'];
/**
* Emits login event
*/
onLogin: ReplaySubject<any> = new ReplaySubject<any>(1);
/**
* Emits logout event
*/
onLogout: ReplaySubject<any> = new ReplaySubject<any>(1);
constructor( constructor(
private appConfig: AppConfigService, alfrescoApi: AlfrescoApiService,
private storageService: StorageService, appConfig: AppConfigService,
private alfrescoApi: AlfrescoApiService, cookie: CookieService,
private cookie: CookieService, logService: LogService,
private logService: LogService) { private storageService: StorageService
super(); ) {
super(alfrescoApi, appConfig, cookie, logService);
this.alfrescoApi.alfrescoApiInitialized.subscribe(() => { this.alfrescoApi.alfrescoApiInitialized.subscribe(() => {
this.alfrescoApi.getInstance().reply('logged-in', () => { this.alfrescoApi.getInstance().reply('logged-in', () => {
this.onLogin.next(); this.onLogin.next();
@ -84,15 +70,6 @@ export class AuthenticationService extends Authentication {
} }
} }
/**
* Does kerberos enabled?
*
* @returns True if enabled, false otherwise
*/
isKerberosEnabled(): boolean {
return this.appConfig.get<boolean>(AppConfigValues.AUTH_WITH_CREDENTIALS, false);
}
/** /**
* Does the provider support OAuth? * Does the provider support OAuth?
* *
@ -102,37 +79,6 @@ export class AuthenticationService extends Authentication {
return this.alfrescoApi.getInstance().isOauthConfiguration(); return this.alfrescoApi.getInstance().isOauthConfiguration();
} }
isPublicUrl(): boolean {
return this.alfrescoApi.getInstance().isPublicUrl();
}
/**
* Does the provider support ECM?
*
* @returns True if supported, false otherwise
*/
isECMProvider(): boolean {
return this.alfrescoApi.getInstance().isEcmConfiguration();
}
/**
* Does the provider support BPM?
*
* @returns True if supported, false otherwise
*/
isBPMProvider(): boolean {
return this.alfrescoApi.getInstance().isBpmConfiguration();
}
/**
* Does the provider support both ECM and BPM?
*
* @returns True if both are supported, false otherwise
*/
isALLProvider(): boolean {
return this.alfrescoApi.getInstance().isEcmBpmConfiguration();
}
/** /**
* Logs the user in. * Logs the user in.
* *
@ -142,18 +88,17 @@ export class AuthenticationService extends Authentication {
* @returns Object with auth type ("ECM", "BPM" or "ALL") and auth ticket * @returns Object with auth type ("ECM", "BPM" or "ALL") and auth ticket
*/ */
login(username: string, password: string, rememberMe: boolean = false): Observable<{ type: string; ticket: any }> { login(username: string, password: string, rememberMe: boolean = false): Observable<{ type: string; ticket: any }> {
return from(this.alfrescoApi.getInstance().login(username, password)) return from(this.alfrescoApi.getInstance().login(username, password)).pipe(
.pipe( map((response: any) => {
map((response: any) => { this.saveRememberMeCookie(rememberMe);
this.saveRememberMeCookie(rememberMe); this.onLogin.next(response);
this.onLogin.next(response); return {
return { type: this.appConfig.get(AppConfigValues.PROVIDERS),
type: this.appConfig.get(AppConfigValues.PROVIDERS), ticket: response
ticket: response };
}; }),
}), catchError((err) => this.handleError(err))
catchError((err) => this.handleError(err)) );
);
} }
/** /**
@ -163,31 +108,6 @@ export class AuthenticationService extends Authentication {
this.alfrescoApi.getInstance().implicitLogin(); this.alfrescoApi.getInstance().implicitLogin();
} }
/**
* Saves the "remember me" cookie as either a long-life cookie or a session cookie.
*
* @param rememberMe Enables a long-life cookie
*/
private saveRememberMeCookie(rememberMe: boolean): void {
let expiration = null;
if (rememberMe) {
expiration = new Date();
const time = expiration.getTime();
const expireTime = time + REMEMBER_ME_UNTIL;
expiration.setTime(expireTime);
}
this.cookie.setItem(REMEMBER_ME_COOKIE_KEY, '1', expiration, null);
}
/**
* Checks whether the "remember me" cookie was set or not.
*
* @returns True if set, false otherwise
*/
isRememberMeSet(): boolean {
return (this.cookie.getItem(REMEMBER_ME_COOKIE_KEY) !== null);
}
/** /**
* Logs the user out. * Logs the user out.
@ -195,14 +115,13 @@ export class AuthenticationService extends Authentication {
* @returns Response event called when logout is complete * @returns Response event called when logout is complete
*/ */
logout() { logout() {
return from(this.callApiLogout()) return from(this.callApiLogout()).pipe(
.pipe( tap((response) => {
tap((response) => { this.onLogout.next(response);
this.onLogout.next(response); return response;
return response; }),
}), catchError((err) => this.handleError(err))
catchError((err) => this.handleError(err)) );
);
} }
private callApiLogout(): Promise<any> { private callApiLogout(): Promise<any> {
@ -212,37 +131,6 @@ export class AuthenticationService extends Authentication {
return Promise.resolve(); return Promise.resolve();
} }
/**
* Gets the ECM ticket stored in the Storage.
*
* @returns The ticket or `null` if none was found
*/
getTicketEcm(): string | null {
return this.alfrescoApi.getInstance()?.getTicketEcm();
}
/**
* Gets the BPM ticket stored in the Storage.
*
* @returns The ticket or `null` if none was found
*/
getTicketBpm(): string | null {
return this.alfrescoApi.getInstance()?.getTicketBpm();
}
/**
* Gets the BPM ticket from the Storage in Base 64 format.
*
* @returns The ticket or `null` if none was found
*/
getTicketEcmBase64(): string | null {
const ticket = this.alfrescoApi.getInstance()?.getTicketEcm();
if (ticket) {
return 'Basic ' + btoa(ticket);
}
return null;
}
/** /**
* Checks if the user is logged in on an ECM provider. * Checks if the user is logged in on an ECM provider.
* *
@ -273,67 +161,13 @@ export class AuthenticationService extends Authentication {
return false; return false;
} }
/** isImplicitFlow(): boolean {
* Gets the ECM username. const oauth2: OauthConfigModel = Object.assign({}, this.appConfig.get<OauthConfigModel>(AppConfigValues.OAUTHCONFIG, null));
* return !!oauth2?.implicitFlow;
* @returns The ECM username
*/
getEcmUsername(): string {
return this.alfrescoApi.getInstance().getEcmUsername();
} }
/** isAuthCodeFlow(): boolean {
* Gets the BPM username return false;
*
* @returns The BPM username
*/
getBpmUsername(): string {
return this.alfrescoApi.getInstance().getBpmUsername();
}
/** Sets the URL to redirect to after login.
*
* @param url URL to redirect to
*/
setRedirect(url: RedirectionModel) {
this.redirectUrl = url;
}
/** Gets the URL to redirect to after login.
*
* @returns The redirect URL
*/
getRedirect(): string {
const provider = this.appConfig.get<string>(AppConfigValues.PROVIDERS);
return this.hasValidRedirection(provider) ? this.redirectUrl.url : null;
}
private hasValidRedirection(provider: string): boolean {
return this.redirectUrl && (this.redirectUrl.provider === provider || this.hasSelectedProviderAll(provider));
}
private hasSelectedProviderAll(provider: string): boolean {
return this.redirectUrl && (this.redirectUrl.provider === 'ALL' || provider === 'ALL');
}
/**
* Prints an error message in the console browser
*
* @param error Error message
* @returns Object representing the error message
*/
handleError(error: any): Observable<any> {
this.logService.error('Error when logging in', error);
return throwError(error || 'Server error');
}
/**
* Gets the set of URLs that the token bearer is excluded from.
*
* @returns Array of URL strings
*/
getBearerExcludedUrls(): string[] {
return this.bearerExcludedUrls;
} }
/** /**
@ -345,60 +179,5 @@ export class AuthenticationService extends Authentication {
return this.storageService.getItem(JwtHelperService.USER_ACCESS_TOKEN); return this.storageService.getItem(JwtHelperService.USER_ACCESS_TOKEN);
} }
/** reset() {}
* Adds the auth token to an HTTP header using the 'bearer' scheme.
*
* @param headersArg Header that will receive the token
* @returns The new header with the token added
*/
addTokenToHeader(headersArg?: HttpHeaders): Observable<HttpHeaders> {
return new Observable((observer: Observer<any>) => {
let headers = headersArg;
if (!headers) {
headers = new HttpHeaders();
}
try {
const header = this.getAuthHeaders(headers);
observer.next(header);
observer.complete();
} catch (error) {
observer.error(error);
}
});
}
private getAuthHeaders(header: HttpHeaders): HttpHeaders {
const authType = this.appConfig.get<string>(AppConfigValues.AUTHTYPE, 'BASIC');
switch (authType) {
case 'OAUTH':
return this.addBearerToken(header);
case 'BASIC':
return this.addBasicAuth(header);
default:
return header;
}
}
private addBearerToken(header: HttpHeaders): HttpHeaders {
const token: string = this.getToken();
if (!token) {
return header;
}
return header.set('Authorization', 'bearer ' + token);
}
private addBasicAuth(header: HttpHeaders): HttpHeaders {
const ticket: string = this.getTicketEcmBase64();
if (!ticket) {
return header;
}
return header.set('Authorization', ticket);
}
} }

View File

@ -48,7 +48,6 @@ import { PipeModule } from './pipes/pipe.module';
import { AlfrescoApiService } from './services/alfresco-api.service'; import { AlfrescoApiService } from './services/alfresco-api.service';
import { TranslationService } from './translation/translation.service'; import { TranslationService } from './translation/translation.service';
import { startupServiceFactory } from './services/startup-service-factory';
import { SortingPickerModule } from './sorting-picker/sorting-picker.module'; import { SortingPickerModule } from './sorting-picker/sorting-picker.module';
import { IconModule } from './icon/icon.module'; import { IconModule } from './icon/icon.module';
import { TranslateLoaderService } from './translation/translate-loader.service'; import { TranslateLoaderService } from './translation/translate-loader.service';
@ -64,6 +63,18 @@ import { HttpClientModule, HttpClientXsrfModule, HTTP_INTERCEPTORS } from '@angu
import { AuthenticationService } from './auth/services/authentication.service'; import { AuthenticationService } from './auth/services/authentication.service';
import { MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar'; import { MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar';
import { IdentityUserInfoModule } from './identity-user-info/identity-user-info.module'; import { IdentityUserInfoModule } from './identity-user-info/identity-user-info.module';
import { AuthBearerInterceptor } from './auth/authentication-interceptor/auth-bearer.interceptor';
import { loadAppConfig } from './app-config/app-config.loader';
import { AppConfigService } from './app-config/app-config.service';
import { StorageService } from './common/services/storage.service';
import { AlfrescoApiLoaderService, createAlfrescoApiInstance } from './api-factories/alfresco-api-v2-loader.service';
import { AlfrescoApiServiceWithAngularBasedHttpClient } from './api-factories/alfresco-api-service-with-angular-based-http-client';
interface Config {
readonly useAngularBasedHttpClientInAlfrescoJs: boolean;
}
const defaultConfig: Config = { useAngularBasedHttpClientInAlfrescoJs: false };
@NgModule({ @NgModule({
imports: [ imports: [
@ -145,7 +156,7 @@ import { IdentityUserInfoModule } from './identity-user-info/identity-user-info.
] ]
}) })
export class CoreModule { export class CoreModule {
static forRoot(): ModuleWithProviders<CoreModule> { static forRoot(config: Config = defaultConfig): ModuleWithProviders<CoreModule> {
return { return {
ngModule: CoreModule, ngModule: CoreModule,
providers: [ providers: [
@ -154,11 +165,8 @@ export class CoreModule {
{ provide: TranslateLoader, useClass: TranslateLoaderService }, { provide: TranslateLoader, useClass: TranslateLoaderService },
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: startupServiceFactory, useFactory: loadAppConfig,
deps: [ deps: [ AppConfigService, StorageService ], multi: true
AlfrescoApiService
],
multi: true
}, },
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
@ -173,7 +181,18 @@ export class CoreModule {
useValue: { useValue: {
duration: 10000 duration: 10000
} }
} },
{
provide: APP_INITIALIZER,
useFactory: createAlfrescoApiInstance,
deps: [ AlfrescoApiLoaderService ],
multi: true
},
{ provide: HTTP_INTERCEPTORS, useClass: AuthBearerInterceptor, multi: true },
...(config.useAngularBasedHttpClientInAlfrescoJs
? [{ provide: AlfrescoApiService, useClass: AlfrescoApiServiceWithAngularBasedHttpClient }]
: []
)
] ]
}; };
} }

View File

@ -169,6 +169,30 @@
"ERROR_SINGULAR": "{{ name }} couldn't be deleted", "ERROR_SINGULAR": "{{ name }} couldn't be deleted",
"ERROR_PLURAL": "{{ number }} items couldn't be deleted" "ERROR_PLURAL": "{{ number }} items couldn't be deleted"
}, },
"HOST_SETTINGS": {
"TYPE-AUTH": "Authentication type",
"BASIC": "Basic Authentication",
"SSO": "SSO",
"IMPLICIT-FLOW": "Implicit Flow",
"CODE-FLOW": "Code Flow",
"PROVIDER": "Provider",
"REQUIRED": "This field is required",
"CS_URL_ERROR": "Content Services address doesn't match the URL format",
"PS_URL_ERROR": "Process Services address doesn't match the URL format",
"TITLE": "Settings",
"CS-HOST": "Content Services URL",
"BP-HOST": "Process Services URL",
"BACK": "Back",
"APPLY": "APPLY",
"NOT_VALID": "http(s)://host|ip:port(/path) not recognized, try a different URL.",
"REDIRECT": "Redirect URI",
"REDIRECT_LOGOUT": "Redirect URI Logout",
"SILENT": "Silent Login",
"SCOPE": "Scope",
"CLIENT": "Client ID",
"PUBLIC_URLS": "Public urls silent Login",
"SECRET": "Secret"
},
"CARDVIEW": { "CARDVIEW": {
"KEYVALUEPAIRS": { "KEYVALUEPAIRS": {
"ADD": "Add New", "ADD": "Add New",

View File

@ -43,23 +43,13 @@ export class AlfrescoApiService {
return this.alfrescoApi; return this.alfrescoApi;
} }
constructor( constructor(protected appConfig: AppConfigService, protected storageService: StorageService) {}
protected appConfig: AppConfigService,
protected storageService: StorageService) {
}
async load() { async load(config: AlfrescoApiConfig): Promise<void> {
try { this.currentAppConfig = config;
await this.appConfig.load();
this.storageService.prefix = this.appConfig.get<string>(AppConfigValues.STORAGE_PREFIX, '');
this.getCurrentAppConfig();
if (this.currentAppConfig.authType === 'OAUTH') { if (config.authType === 'OAUTH') {
this.idpConfig = await this.appConfig.loadWellKnown(this.currentAppConfig.oauth2.host);
this.mapAlfrescoApiOpenIdConfig(); this.mapAlfrescoApiOpenIdConfig();
}
} catch {
throw new Error('Something wrong happened when calling the app.config.json');
} }
this.initAlfrescoApiWithConfig(); this.initAlfrescoApiWithConfig();
@ -69,7 +59,6 @@ export class AlfrescoApiService {
async reset() { async reset() {
this.getCurrentAppConfig(); this.getCurrentAppConfig();
if (this.currentAppConfig.authType === 'OAUTH') { if (this.currentAppConfig.authType === 'OAUTH') {
this.idpConfig = await this.appConfig.loadWellKnown(this.currentAppConfig.oauth2.host);
this.mapAlfrescoApiOpenIdConfig(); this.mapAlfrescoApiOpenIdConfig();
} }
this.initAlfrescoApiWithConfig(); this.initAlfrescoApiWithConfig();
@ -84,7 +73,8 @@ export class AlfrescoApiService {
return oauth; return oauth;
} }
private mapAlfrescoApiOpenIdConfig() { private async mapAlfrescoApiOpenIdConfig() {
this.idpConfig = await this.appConfig.loadWellKnown(this.currentAppConfig.oauth2.host);
this.currentAppConfig.oauth2.tokenUrl = this.idpConfig.token_endpoint; this.currentAppConfig.oauth2.tokenUrl = this.idpConfig.token_endpoint;
this.currentAppConfig.oauth2.authorizationUrl = this.idpConfig.authorization_endpoint; this.currentAppConfig.oauth2.authorizationUrl = this.idpConfig.authorization_endpoint;
this.currentAppConfig.oauth2.logoutUrl = this.idpConfig.end_session_endpoint; this.currentAppConfig.oauth2.logoutUrl = this.idpConfig.end_session_endpoint;
@ -117,11 +107,15 @@ export class AlfrescoApiService {
if (this.alfrescoApi && this.isDifferentConfig(this.lastConfig, this.currentAppConfig)) { if (this.alfrescoApi && this.isDifferentConfig(this.lastConfig, this.currentAppConfig)) {
this.alfrescoApi.setConfig(this.currentAppConfig); this.alfrescoApi.setConfig(this.currentAppConfig);
} else { } else {
this.alfrescoApi = new AlfrescoApi(this.currentAppConfig); this.alfrescoApi = this.createInstance(this.currentAppConfig);
} }
this.lastConfig = this.currentAppConfig; this.lastConfig = this.currentAppConfig;
} }
createInstance(config: AlfrescoApiConfig): AlfrescoApi {
return new AlfrescoApi(config);
}
isDifferentConfig(lastConfig: AlfrescoApiConfig, newConfig: AlfrescoApiConfig) { isDifferentConfig(lastConfig: AlfrescoApiConfig, newConfig: AlfrescoApiConfig) {
return JSON.stringify(lastConfig) !== JSON.stringify(newConfig); return JSON.stringify(lastConfig) !== JSON.stringify(newConfig);
} }

View File

@ -0,0 +1,285 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { PeopleApi, UserProfileApi, UserRepresentation } from '@alfresco/js-api';
import { HttpHeaders } from '@angular/common/http';
import { RedirectionModel } from '../auth/models/redirection.model';
import { from, Observable, Observer, ReplaySubject, throwError } from 'rxjs';
import { AppConfigService, AppConfigValues } from '../app-config/app-config.service';
import { AlfrescoApiService } from './alfresco-api.service';
import { CookieService } from '../common/services/cookie.service';
import { LogService } from '../common/services/log.service';
const REMEMBER_ME_COOKIE_KEY = 'ALFRESCO_REMEMBER_ME';
const REMEMBER_ME_UNTIL = 1000 * 60 * 60 * 24 * 30;
export abstract class BaseAuthenticationService {
protected bearerExcludedUrls: readonly string[] = ['resources/', 'assets/', 'auth/realms', 'idp/'];
protected redirectUrl: RedirectionModel = null;
onLogin = new ReplaySubject<any>(1);
onLogout = new ReplaySubject<any>(1);
_peopleApi: PeopleApi;
get peopleApi(): PeopleApi {
this._peopleApi = this._peopleApi ?? new PeopleApi(this.alfrescoApi.getInstance());
return this._peopleApi;
}
_profileApi: UserProfileApi;
get profileApi(): UserProfileApi {
this._profileApi = this._profileApi ?? new UserProfileApi(this.alfrescoApi.getInstance());
return this._profileApi;
}
constructor(
protected alfrescoApi: AlfrescoApiService,
protected appConfig: AppConfigService,
protected cookie: CookieService,
private logService: LogService
) {}
abstract readonly supportCodeFlow: boolean;
abstract getToken(): string;
abstract isLoggedIn(): boolean;
abstract isLoggedInWith(provider: string): boolean;
abstract isOauth(): boolean;
abstract isImplicitFlow(): boolean;
abstract isAuthCodeFlow(): boolean;
abstract login(username: string, password: string, rememberMe?: boolean): Observable<{ type: string; ticket: any }>;
abstract ssoImplicitLogin(): void;
abstract logout(): Observable<any>;
abstract isEcmLoggedIn(): boolean;
abstract isBpmLoggedIn(): boolean;
abstract reset(): void;
getBearerExcludedUrls(): readonly string[] {
return this.bearerExcludedUrls;
}
/**
* Adds the auth token to an HTTP header using the 'bearer' scheme.
*
* @param headersArg Header that will receive the token
* @returns The new header with the token added
*/
addTokenToHeader(headersArg?: HttpHeaders): Observable<HttpHeaders> {
return new Observable((observer: Observer<any>) => {
let headers = headersArg;
if (!headers) {
headers = new HttpHeaders();
}
try {
const header = this.getAuthHeaders(headers);
observer.next(header);
observer.complete();
} catch (error) {
observer.error(error);
}
});
}
private getAuthHeaders(header: HttpHeaders): HttpHeaders {
const authType = this.appConfig.get<string>(AppConfigValues.AUTHTYPE, 'BASIC');
switch (authType) {
case 'OAUTH':
return this.addBearerToken(header);
case 'BASIC':
return this.addBasicAuth(header);
default:
return header;
}
}
private addBearerToken(header: HttpHeaders): HttpHeaders {
const token: string = this.getToken();
if (!token) {
return header;
}
return header.set('Authorization', 'bearer ' + token);
}
private addBasicAuth(header: HttpHeaders): HttpHeaders {
const ticket: string = this.getTicketEcmBase64();
if (!ticket) {
return header;
}
return header.set('Authorization', ticket);
}
/**
* Gets the ECM username.
*
* @returns The ECM username
*/
getEcmUsername(): string {
return this.alfrescoApi.getInstance().getEcmUsername();
}
/**
* Gets the BPM username
*
* @returns The BPM username
*/
getBpmUsername(): string {
return this.alfrescoApi.getInstance().getBpmUsername();
}
isPublicUrl(): boolean {
return this.alfrescoApi.getInstance().isPublicUrl();
}
/**
* Does the provider support ECM?
*
* @returns True if supported, false otherwise
*/
isECMProvider(): boolean {
return this.alfrescoApi.getInstance().isEcmConfiguration();
}
/**
* Does the provider support BPM?
*
* @returns True if supported, false otherwise
*/
isBPMProvider(): boolean {
return this.alfrescoApi.getInstance().isBpmConfiguration();
}
/**
* Does the provider support both ECM and BPM?
*
* @returns True if both are supported, false otherwise
*/
isALLProvider(): boolean {
return this.alfrescoApi.getInstance().isEcmBpmConfiguration();
}
/**
* Gets the ECM ticket stored in the Storage.
*
* @returns The ticket or `null` if none was found
*/
getTicketEcm(): string | null {
return this.alfrescoApi.getInstance()?.getTicketEcm();
}
/**
* Gets the BPM ticket stored in the Storage.
*
* @returns The ticket or `null` if none was found
*/
getTicketBpm(): string | null {
return this.alfrescoApi.getInstance()?.getTicketBpm();
}
/**
* Gets the BPM ticket from the Storage in Base 64 format.
*
* @returns The ticket or `null` if none was found
*/
getTicketEcmBase64(): string | null {
const ticket = this.alfrescoApi.getInstance()?.getTicketEcm();
if (ticket) {
return 'Basic ' + btoa(ticket);
}
return null;
}
/**
* Gets information about the user currently logged into APS.
*
* @returns User information
*/
getBpmLoggedUser(): Observable<UserRepresentation> {
return from(this.profileApi.getProfile());
}
/**
* Prints an error message in the console browser
*
* @param error Error message
* @returns Object representing the error message
*/
handleError(error: any): Observable<any> {
this.logService.error('Error when logging in', error);
return throwError(error || 'Server error');
}
/**
* Does kerberos enabled?
*
* @returns True if enabled, false otherwise
*/
isKerberosEnabled(): boolean {
return this.appConfig.get<boolean>(AppConfigValues.AUTH_WITH_CREDENTIALS, false);
}
/**
* Saves the "remember me" cookie as either a long-life cookie or a session cookie.
*
* @param rememberMe Enables a long-life cookie
*/
saveRememberMeCookie(rememberMe: boolean): void {
let expiration = null;
if (rememberMe) {
expiration = new Date();
const time = expiration.getTime();
const expireTime = time + REMEMBER_ME_UNTIL;
expiration.setTime(expireTime);
}
this.cookie.setItem(REMEMBER_ME_COOKIE_KEY, '1', expiration, null);
}
/**
* Checks whether the "remember me" cookie was set or not.
*
* @returns True if set, false otherwise
*/
isRememberMeSet(): boolean {
return this.cookie.getItem(REMEMBER_ME_COOKIE_KEY) !== null;
}
setRedirect(url?: RedirectionModel) {
this.redirectUrl = url;
}
/** Gets the URL to redirect to after login.
*
* @returns The redirect URL
*/
getRedirect(): string {
const provider = this.appConfig.get<string>(AppConfigValues.PROVIDERS);
return this.hasValidRedirection(provider) ? this.redirectUrl.url : null;
}
private hasValidRedirection(provider: string): boolean {
return this.redirectUrl && (this.redirectUrl.provider === provider || this.hasSelectedProviderAll(provider));
}
private hasSelectedProviderAll(provider: string): boolean {
return this.redirectUrl && (this.redirectUrl.provider === 'ALL' || provider === 'ALL');
}
}

View File

@ -15,9 +15,12 @@
* limitations under the License. * limitations under the License.
*/ */
import { AlfrescoApiService } from './alfresco-api.service'; import { AppConfigService, AppConfigValues } from '../app-config/app-config.service';
import { StorageService } from '../common/services/storage.service';
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions export function loadAppConfig(appConfigService: AppConfigService, storageService: StorageService) {
export function startupServiceFactory(alfrescoApiService: AlfrescoApiService) { return () =>
return () => alfrescoApiService.load(); appConfigService.load().then(() => {
storageService.prefix = appConfigService.get<string>(AppConfigValues.STORAGE_PREFIX, '');
});
} }

View File

@ -21,6 +21,7 @@ import { AlfrescoApiService } from '../services/alfresco-api.service';
import { StorageService } from '../common/services/storage.service'; import { StorageService } from '../common/services/storage.service';
import { UserPreferencesService } from '../common/services/user-preferences.service'; import { UserPreferencesService } from '../common/services/user-preferences.service';
import { DemoForm } from '../mock/form/demo-form.mock'; import { DemoForm } from '../mock/form/demo-form.mock';
import { AuthenticationService } from '../auth/services/authentication.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -29,11 +30,13 @@ export class CoreAutomationService {
public forms = new DemoForm(); public forms = new DemoForm();
constructor(private appConfigService: AppConfigService, constructor(
private alfrescoApiService: AlfrescoApiService, private appConfigService: AppConfigService,
private userPreferencesService: UserPreferencesService, private alfrescoApiService: AlfrescoApiService,
private storageService: StorageService) { private userPreferencesService: UserPreferencesService,
} private storageService: StorageService,
private auth: AuthenticationService
) {}
setup() { setup() {
const adfProxy = window['adf'] || {}; const adfProxy = window['adf'] || {};
@ -72,6 +75,7 @@ export class CoreAutomationService {
adfProxy.apiReset = () => { adfProxy.apiReset = () => {
this.alfrescoApiService.reset(); this.alfrescoApiService.reset();
this.auth.reset();
}; };
window['adf'] = adfProxy; window['adf'] = adfProxy;

View File

@ -59,3 +59,4 @@ export * from './lib/common';
export * from './lib/material.module'; export * from './lib/material.module';
export * from './lib/core.module'; export * from './lib/core.module';
export { AuthModule } from './lib/auth/oidc/auth.module';

30895
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -76,6 +76,8 @@
"@mat-datetimepicker/moment": "^9.0.68", "@mat-datetimepicker/moment": "^9.0.68",
"@ngx-translate/core": "^14.0.0", "@ngx-translate/core": "^14.0.0",
"@storybook/core-server": "6.4.22", "@storybook/core-server": "6.4.22",
"angular-oauth2-oidc": "^13.0.1",
"angular-oauth2-oidc-jwks": "^13.0.1",
"apollo-angular": "^4.0.1", "apollo-angular": "^4.0.1",
"chart.js": "2.9.4", "chart.js": "2.9.4",
"cropperjs": "1.5.13", "cropperjs": "1.5.13",
@ -99,13 +101,14 @@
"@angular-eslint/eslint-plugin-template": "14.3.0", "@angular-eslint/eslint-plugin-template": "14.3.0",
"@angular-eslint/template-parser": "14.0.2", "@angular-eslint/template-parser": "14.0.2",
"@angular/cli": "~14.1.0", "@angular/cli": "~14.1.0",
"@angular-devkit/schematics": "~14.1.0",
"@angular/compiler-cli": "14.1.3", "@angular/compiler-cli": "14.1.3",
"@editorjs/code": "2.8.0", "@editorjs/code": "2.8.0",
"@editorjs/inline-code": "1.4.0", "@editorjs/inline-code": "1.4.0",
"@editorjs/marker": "1.2.2", "@editorjs/marker": "1.2.2",
"@nrwl/angular": "14.8.6", "@nrwl/angular": "14.8.6",
"@nrwl/cli": "14.5.4", "@nrwl/cli": "14.5.4",
"@nrwl/eslint-plugin-nx": "^14.5.4", "@nrwl/eslint-plugin-nx": "14.5.4",
"@nrwl/node": "14.5.4", "@nrwl/node": "14.5.4",
"@nrwl/storybook": "14.5.4", "@nrwl/storybook": "14.5.4",
"@nrwl/workspace": "14.5.4", "@nrwl/workspace": "14.5.4",
@ -159,6 +162,7 @@
"lint-staged": "^13.1.2", "lint-staged": "^13.1.2",
"lite-server": "^2.6.1", "lite-server": "^2.6.1",
"mini-css-extract-plugin": "^1.6.0", "mini-css-extract-plugin": "^1.6.0",
"nconf": "^0.11.1",
"ng-mocks": "^14.2.3", "ng-mocks": "^14.2.3",
"ng-packagr": "14.0.3", "ng-packagr": "14.0.3",
"nx": "14.4.2", "nx": "14.4.2",
@ -177,7 +181,8 @@
"tsconfig-paths": "^4.1.1", "tsconfig-paths": "^4.1.1",
"typescript": "4.7.4", "typescript": "4.7.4",
"webdriver-manager": "12.1.8", "webdriver-manager": "12.1.8",
"webpack-cli": "^5.0.1" "webpack-cli": "^5.0.1",
"webpack": "^5.0.1"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"bundlesize": [ "bundlesize": [

View File

@ -27,6 +27,7 @@
"@alfresco/adf-core/*": ["lib/core/*/public-api.ts"], "@alfresco/adf-core/*": ["lib/core/*/public-api.ts"],
"@alfresco/adf-core/auth": ["lib/core/auth/src/index.ts"], "@alfresco/adf-core/auth": ["lib/core/auth/src/index.ts"],
"@alfresco/adf-core/shell": ["lib/core/shell/src/index.ts"], "@alfresco/adf-core/shell": ["lib/core/shell/src/index.ts"],
"@alfresco/adf-core/api": ["lib/core/api/src/index.ts"],
"@alfresco/adf-extensions": ["lib/extensions"], "@alfresco/adf-extensions": ["lib/extensions"],
"@alfresco/adf-insights": ["lib/insights"], "@alfresco/adf-insights": ["lib/insights"],
"@alfresco/adf-process-services": ["lib/process-services"], "@alfresco/adf-process-services": ["lib/process-services"],