[AAE-12501] move auth in ADF (#8689)

* remove unneeded JS-API dep
move auth in the right place

* [AAE-12501] Replace alfresco api client with AdfHttpClient

* [AAE-12501] Restore get username methods

* [AAE-12501] Get username with authentication service

* [AAE-12501] Create a request options interface with the needed props, remove the import from js-api, return the body from request

* add emitters

* [AAE-12501] Replace Math.random() to fix hospot security issue, fix lint issues

* [AAE-12501] Comment temporary setCsrfToken because is not possible to import app config service from core due to circular dependencies

* [AAE-12501] Get disableCsrf from app config serviice when app configuration is loaded

* [AAE-12501] Remove wrong character

* Pass down the requestUrl for request interception
bring back check from js-api
fixing isLogin issues part1
some fix around emit
Narrow access for methods
fix sso username issue
Switch to dynamic service injection
add emitters
move auth inside ADF

* clean

* fix unit test

* fix lint

* Fix exports

* Fix process-services unit tests

* Fix core unit tests

Fix core unit tests

Fix core unit tests

Fix core unit tests

Fix core unit tests

Fix core unit tests

Fix core unit tests

* Fix content-services unit tests: getEcmUsername from authentication service

Fix content-services unit tests: alfresco api service has been replaced by authentication service

* Fix circular dependecies issue importing AppConfigService outside the api entrypoint dir

* Import AuthModule even in not only canary mode to let the e2es run

* Fix authentication unit tests

* Fix unit test '[ECM] should return a ticket undefined after logout'

* Remove AlfrescoApiService is not used anymore

* Fix unit test '[BPM] should return an BPM ticket after the login done': add Basic suffix to basicAuth

* Fix unit tests core

* Fix login errors with the BASIC authentication

* Fix missing onLogin event

* Temporary skip unit tests to check e2es

* Fix login component doesn't add the authorization header

* Fix prefix is undefined

* Fix image is not showed by the alfresco file viewer because alf_ticket is not added to the content url query params, pass ticketEcm to the alfrescoApi configuration used by alfrescoApiClient.ts getAlfTicket()

* Fix C280012: set app prefix before calling content api

* Revert "Fix image is not showed by the alfresco file viewer because alf_ticket is not added to the content url query params, pass ticketEcm to the alfrescoApi configuration used by alfrescoApiClient.ts getAlfTicket()"

This reverts commit afbf086b98d72835aab8b15d4af433efeaac2d3b.

* try to change adf core autoamtion service init

* go back

* grant type password login

* fix

* remove automatic login in reset try

* fix not silent login

* lint happy

* fix

* Update alfresco-api-v2-loader.service.ts

* fixint

* Revert "Temporary skip unit tests to check e2es"

This reverts commit a0adc7e58a001a54442c82952761bff891caa5cd.

* fix modules

* fix app config stream in storing service
fix app config stream for sub property

* fix identity test to use the real service

* fix unit

* fix unit

* fix unit

* remove test that are probably have never been green

* fix

* fix PC

* fix localstorage

* fix

* fix

* fix

* fix

* fix storybook
move e2e in content for versioning
fix lint

* fix

* fix size

* enable log

* some fix for usernames

* remove log

* fix rebase

* [AAE-12502] Restore isKerberosEnabled into authentication service

* subject onLogin

* fix unit

* Fix lint issue

* fix

* Update error message

* Revert change did by b79c5d37d6\#diff-ad85723e21276e05e577bab652c6ab0d243bd0ad54d4cc70ef6e60dc5e635c33L38

* Refresh the browser to wait for the user to click process cloud page

* Remove e2e, the application list is already tested by the app-list-cloud.component.spec.ts https://github.com/Alfresco/alfresco-ng2-components/blob/dev-eromano-AAE-12501-2/lib/process-services-cloud/src/lib/app/components/app-list-cloud.component.spec.ts\#L147

* [12502] Add getUsername method to the AuthenticationService

* [12501] restore mutlipart/form-data header needed by angular http-client to to fix 415 unsupported media type

* Revert "[12501] restore mutlipart/form-data header needed by angular http-client to to fix 415 unsupported media type"

This reverts commit d8c584b94f649b57859d74157ec0861f2ebddebb.

* [12501] fix unsupported upload file on admin-apa, append json content type only calling alfresco api

* [12501] fix unsupported upload file on admin-apa

[12501] fix unsupported upload file on admin-apa

* Revert "[12501] fix unsupported upload file on admin-apa"

This reverts commit 53cda21d795588d87244c78c5a5347afd04ea2b1.

* Improve getHeaders

* Revert change

* Set application/json content type if request body is not a FormData

* Logout by the authenticationService

* Update returned error message

* Fix lint issues after rebasing

* [12502] Add basic template with data-automation-ad selector to check when is attached to the Dom

* Fix issues after rebase

---------

Co-authored-by: Amedeo Lepore <amedeo.lepore@hyland.com>
Co-authored-by: Andras Popovics <popovics@ndras.hu>
This commit is contained in:
Eugenio Romano
2023-11-06 14:25:27 +01:00
committed by GitHub
parent 057e0bcd7c
commit 08da9ae2c3
111 changed files with 2157 additions and 1417 deletions

View File

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

View File

@@ -321,4 +321,86 @@ describe('AdfHttpClient', () => {
req.flush(null, { status: 200, statusText: 'Ok' });
});
it('should set Content-type to multipart/form-data if contentTypes array contains only multipart/form-data element', () => {
const options: RequestOptions = {
path: '',
httpMethod: 'POST',
contentTypes: ['multipart/form-data'],
queryParams: {
lastModifiedFrom: new Date('2022-08-17T00:00:00.000Z')
}
};
angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch(error =>
fail(error)
);
const req = controller.expectOne('http://example.com?lastModifiedFrom=2022-08-17T00%3A00%3A00.000Z');
expect(req.request.headers.get('Content-Type')).toEqual('multipart/form-data');
req.flush(null, { status: 200, statusText: 'Ok' });
});
it('should set Content-type header to application/json if contentTypes array contains application/json', () => {
const options: RequestOptions = {
path: '',
httpMethod: 'POST',
contentTypes: ['multipart/form-data', 'application/json'],
queryParams: {
lastModifiedFrom: new Date('2022-08-17T00:00:00.000Z')
}
};
angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch(error =>
fail(error)
);
const req = controller.expectOne('http://example.com?lastModifiedFrom=2022-08-17T00%3A00%3A00.000Z');
expect(req.request.headers.get('Content-Type')).toEqual('application/json');
req.flush(null, { status: 200, statusText: 'Ok' });
});
it('should set Content-type to application/json if contentTypes is not passed to the request options', () => {
const options: RequestOptions = {
path: '',
httpMethod: 'POST',
queryParams: {
lastModifiedFrom: new Date('2022-08-17T00:00:00.000Z')
}
};
angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch(error =>
fail(error)
);
const req = controller.expectOne('http://example.com?lastModifiedFrom=2022-08-17T00%3A00%3A00.000Z');
expect(req.request.headers.get('Content-Type')).toEqual('application/json');
req.flush(null, { status: 200, statusText: 'Ok' });
});
it('should set Accept header to application/json if accepts is not passed to the request options', () => {
const options: RequestOptions = {
path: '',
httpMethod: 'POST',
queryParams: {
lastModifiedFrom: new Date('2022-08-17T00:00:00.000Z')
}
};
angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch(error =>
fail(error)
);
const req = controller.expectOne('http://example.com?lastModifiedFrom=2022-08-17T00%3A00%3A00.000Z');
expect(req.request.headers.get('Accept')).toEqual('application/json');
req.flush(null, { status: 200, statusText: 'Ok' });
});
});

View File

@@ -57,17 +57,10 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
on: ee.EmitterMethod;
off: ee.EmitterMethod;
once: ee.EmitterMethod;
_disableCsrf: boolean;
emit: (type: string, ...args: any[]) => void;
private _disableCsrf = false;
private defaultSecurityOptions = {
withCredentials: true,
isBpmRequest: false,
authentications: {},
defaultHeaders: {}
};
get disableCsrf(): boolean {
return this._disableCsrf;
}
@@ -76,8 +69,14 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
this._disableCsrf = disableCsrf;
}
constructor(private httpClient: HttpClient
) {
private defaultSecurityOptions = {
withCredentials: true,
isBpmRequest: false,
authentications: {},
defaultHeaders: {}
};
constructor(private httpClient: HttpClient) {
ee(this);
}
@@ -217,7 +216,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
}
eventEmitter.emit('error', err);
apiClientEmitter.emit('error', err);
apiClientEmitter.emit('error', { ...err, response: { req: err } });
if (err.status === 401) {
eventEmitter.emit('unauthorized');
@@ -232,10 +231,10 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
// for backwards compatibility to handle cases in code where we try read response.error.response.body;
const error = {
response: {...err, body: err.error}
...err, body: err.error
};
const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error.response);
const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error);
return throwError(alfrescoApiError);
}),
takeUntil(abort$)
@@ -252,7 +251,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
}
private static getBody(options: RequestOptions): any {
const contentType = options.contentType;
const contentType = options.contentType ? options.contentType : AdfHttpClient.jsonPreferredMime(options.contentTypes);
const isFormData = contentType === 'multipart/form-data';
const isFormUrlEncoded = contentType === 'application/x-www-form-urlencoded';
const body = options.bodyParam;
@@ -269,20 +268,58 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
}
private getHeaders(options: RequestOptions): HttpHeaders {
const contentType = options.contentType || AdfHttpClient.jsonPreferredMime(options.contentTypes);
const accept = options.accept || AdfHttpClient.jsonPreferredMime(options.accepts);
const optionsHeaders = {
...options.headerParams,
...(options.accept && {Accept: options.accept}),
...((options.contentType) && {'Content-Type': options.contentType})
...(accept && {Accept: accept}),
...((contentType) && {'Content-Type': contentType})
};
if (!this.disableCsrf) {
this.setCsrfToken(optionsHeaders);
}
return new HttpHeaders(optionsHeaders);
}
/**
* Chooses a content type from the given array, with JSON preferred; i.e. return JSON if included, otherwise return the first.
*
* @param contentTypes a contentType array
* @returns The chosen content type, preferring JSON.
*/
private static jsonPreferredMime(contentTypes: readonly string[]): string {
if (!contentTypes?.length) {
return 'application/json';
}
for (let i = 0; i < contentTypes.length; i++) {
if (AdfHttpClient.isJsonMime(contentTypes[i])) {
return contentTypes[i];
}
}
return contentTypes[0];
}
/**
* Checks whether the given content type represents JSON.<br>
* JSON content type examples:<br>
* <ul>
* <li>application/json</li>
* <li>application/json; charset=UTF8</li>
* <li>APPLICATION/JSON</li>
* </ul>
*
* @param contentType The MIME content type to check.
* @returns <code>true</code> if <code>contentType</code> represents JSON, otherwise <code>false</code>.
*/
private static isJsonMime(contentType: string): boolean {
return Boolean(contentType?.match(/^application\/json(;.*)?$/i));
}
private setCsrfToken(optionsHeaders: any) {
const token = this.createCSRFToken();
optionsHeaders['X-CSRF-TOKEN'] = token;

View File

@@ -1,25 +0,0 @@
/*!
* @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 { InjectionToken } from '@angular/core';
import { Constructor } from './types';
export interface ApiClientFactory {
create<T>(apiClass: Constructor<T>): T;
}
export const API_CLIENT_FACTORY_TOKEN = new InjectionToken<ApiClientFactory>('api-client-factory');

View File

@@ -1,66 +0,0 @@
/*!
* @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 { AboutApi } from '@alfresco/js-api';
import { TestBed } from '@angular/core/testing';
import { ApiClientFactory, API_CLIENT_FACTORY_TOKEN } from './api-client.factory';
import { ApiClientsService } from './api-clients.service';
import { Constructor } from './types';
class MockApiClientFactory implements ApiClientFactory {
create<T>(apiClass: Constructor<T>): T {
return new apiClass();
}
}
describe('ApiService', () => {
let apiService: ApiClientsService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
ApiClientsService,
{ provide: API_CLIENT_FACTORY_TOKEN, useClass: MockApiClientFactory }
]
});
apiService = TestBed.inject(ApiClientsService);
});
it('should add api to registry', () => {
apiService.register('ActivitiClient.about', AboutApi);
expect(apiService.get('ActivitiClient.about') instanceof AboutApi).toBeTruthy();
});
it('should throw error if we try to get unregisterd API', () => {
expect(() => apiService.get('ActivitiClient.about')).toThrowError();
apiService.register('ActivitiClient.about', AboutApi);
expect(() => apiService.get('ActivitiClient.about')).not.toThrowError();
});
it('should create only single instance of API', () => {
apiService.register('ActivitiClient.about', AboutApi);
const a = apiService.get('ActivitiClient.about');
const b = apiService.get('ActivitiClient.about');
expect(a).toBe(b);
});
});

View File

@@ -1,66 +0,0 @@
/*!
* @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 { Inject, Injectable } from '@angular/core';
import { ApiClientFactory, API_CLIENT_FACTORY_TOKEN } from './api-client.factory';
import { Constructor, Dictionary } from './types';
/* eslint-disable */
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace AlfrescoCore {
interface ApiRegistry {
}
}
}
/* eslint-enable */
@Injectable()
export class ApiClientsService {
constructor(@Inject(API_CLIENT_FACTORY_TOKEN) private apiCreateFactory: ApiClientFactory) {
}
private registry: Dictionary<Constructor<any>> = {};
private instances: Partial<AlfrescoCore.ApiRegistry> = {};
get<T extends keyof AlfrescoCore.ApiRegistry>(apiName: T): AlfrescoCore.ApiRegistry[T] {
const apiClass = this.registry[apiName];
if (!apiClass) {
throw new Error(`Api not registred: ${apiName}`);
}
return this.instances[apiName] as AlfrescoCore.ApiRegistry[T] ?? this.instantiateApi(apiName);
}
register<T extends keyof AlfrescoCore.ApiRegistry>(apiName: T, api: Constructor<AlfrescoCore.ApiRegistry[T]>): void {
this.registry[apiName] = api;
}
private instantiateApi<T extends keyof AlfrescoCore.ApiRegistry>(apiName: T): AlfrescoCore.ApiRegistry[T] {
const apiClass = this.registry[apiName];
const instance = this.apiCreateFactory.create<AlfrescoCore.ApiRegistry[T]>(apiClass);
this.instances[apiName] = instance;
return instance;
}
}

View File

@@ -1,28 +0,0 @@
/*!
* @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 { AboutApi, SystemPropertiesApi } from '@alfresco/js-api';
import { NgModule } from '@angular/core';
import { ApiClientsService } from '../../api-clients.service';
@NgModule()
export class ActivitiClientModule {
constructor(private apiClientsService: ApiClientsService) {
this.apiClientsService.register('ActivitiClient.about', AboutApi);
this.apiClientsService.register('ActivitiClient.system-properties', SystemPropertiesApi);
}
}

View File

@@ -1,29 +0,0 @@
/*!
* @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 { AboutApi, SystemPropertiesApi } from '@alfresco/js-api';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace AlfrescoCore {
interface ApiRegistry {
['ActivitiClient.about']: AboutApi;
['ActivitiClient.system-properties']: SystemPropertiesApi;
}
}
}

View File

@@ -1,38 +0,0 @@
/*!
* @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 { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ApiClientsService } from '../api-clients.service';
import { ActivitiClientModule } from './activiti/activiti-client.module';
import { DiscoveryClientModule } from './discovery/discovery-client.module';
@NgModule({
imports: [
HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: 'CSRF-TOKEN',
headerName: 'X-CSRF-TOKEN'
}),
ActivitiClientModule,
DiscoveryClientModule
],
providers: [
ApiClientsService
]
})
export class AlfrescoJsClientsModule { }

View File

@@ -1,27 +0,0 @@
/*!
* @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 { DiscoveryApi } from '@alfresco/js-api';
import { NgModule } from '@angular/core';
import { ApiClientsService } from '../../api-clients.service';
@NgModule()
export class DiscoveryClientModule {
constructor(private apiClientsService: ApiClientsService) {
this.apiClientsService.register('DiscoveryClient.discovery', DiscoveryApi);
}
}

View File

@@ -1,27 +0,0 @@
/*!
* @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 { DiscoveryApi } from '@alfresco/js-api';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace AlfrescoCore {
interface ApiRegistry {
['DiscoveryClient.discovery']: DiscoveryApi;
}
}
}

View File

@@ -15,22 +15,41 @@
* limitations under the License.
*/
export interface SecurityOptions {
readonly withCredentials?: boolean;
readonly authentications?: Authentication;
readonly defaultHeaders?: Record<string, string>;
}
export interface Oauth2 {
refreshToken?: string;
accessToken?: string;
}
export interface BasicAuth {
username?: string;
password?: string;
ticket?: string;
}
export interface Authentication {
basicAuth?: BasicAuth;
oauth2?: Oauth2;
cookie?: string;
type?: string;
}
export interface RequestOptions {
httpMethod?: string;
pathParams?: any;
queryParams?: any;
headerParams?: any;
formParams?: any;
bodyParam?: any;
returnType?: any;
responseType?: string;
accepts?: string[];
contentTypes?: string[];
readonly accept?: string;
readonly contentType?: string;
}
export interface SecurityOptions {
readonly isBpmRequest: boolean;
readonly enableCsrf?: boolean;
readonly withCredentials?: boolean;
readonly authentications: any;
readonly defaultHeaders: Record<string, string>;
}

View File

@@ -22,7 +22,7 @@ import { Authentication } from '../authentication';
import { AuthenticationInterceptor, SHOULD_ADD_AUTH_TOKEN } from './authentication.interceptor';
class MockAuthentication extends Authentication {
addTokenToHeader(httpHeaders: HttpHeaders): Observable<HttpHeaders> {
addTokenToHeader(_: string, httpHeaders: HttpHeaders): Observable<HttpHeaders> {
return of(httpHeaders);
}
}

View File

@@ -43,7 +43,7 @@ export class AuthenticationInterceptor implements HttpInterceptor {
Observable<HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> {
if (req.context.get(SHOULD_ADD_AUTH_TOKEN)) {
return this.authService.addTokenToHeader(req.headers).pipe(
return this.authService.addTokenToHeader(req.url, req.headers).pipe(
mergeMap((headersWithBearer) => {
const headerWithContentType = this.appendJsonContentType(headersWithBearer);
const kcReq = req.clone({ headers: headerWithContentType});

View File

@@ -19,5 +19,5 @@ import { HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
export abstract class Authentication {
public abstract addTokenToHeader(headers: HttpHeaders): Observable<HttpHeaders>;
public abstract addTokenToHeader(requestUrl: string, headers: HttpHeaders): Observable<HttpHeaders>;
}

View File

@@ -19,6 +19,8 @@ import { AlfrescoApiConfig } from '@alfresco/js-api';
import { Injectable } from '@angular/core';
import { AppConfigService, AppConfigValues } from '../app-config/app-config.service';
import { AlfrescoApiService } from '../services/alfresco-api.service';
import { StorageService } from '../common/services/storage.service';
import { AuthenticationService, BasicAlfrescoAuthService } from '../auth';
/**
* Create a factory to resolve an api service instance
@@ -34,10 +36,22 @@ export function createAlfrescoApiInstance(angularAlfrescoApiService: AlfrescoApi
providedIn: 'root'
})
export class AlfrescoApiLoaderService {
constructor(private readonly appConfig: AppConfigService, private readonly apiService: AlfrescoApiService) {}
constructor(private readonly appConfig: AppConfigService,
private readonly apiService: AlfrescoApiService,
private readonly basicAlfrescoAuthService: BasicAlfrescoAuthService,
private readonly authService: AuthenticationService,
private storageService: StorageService) {
}
async init(): Promise<any> {
await this.appConfig.load();
this.authService.onLogin.subscribe(async () => {
if (this.authService.isOauth() && (this.authService.isALLProvider() || this.authService.isECMProvider())) {
await this.basicAlfrescoAuthService.requireAlfTicket();
}
});
return this.initAngularAlfrescoApi();
}
@@ -59,6 +73,8 @@ export class AlfrescoApiLoaderService {
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),
ticketEcm: this.storageService.getItem(AppConfigValues.CONTENT_TICKET_STORAGE_LABEL),
ticketBpm: this.storageService.getItem(AppConfigValues.PROCESS_TICKET_STORAGE_LABEL),
oauth2: oauth
});

View File

@@ -1,29 +0,0 @@
/*!
* @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 { ApiClientFactory, Constructor } from '@alfresco/adf-core/api';
import { Injectable } from '@angular/core';
import { AlfrescoApiService } from '../services/alfresco-api.service';
@Injectable()
export class LegacyClientFactory implements ApiClientFactory {
constructor(private alfrescoApiService: AlfrescoApiService) { }
create<T>(apiClass: Constructor<T>): T {
return new apiClass(this.alfrescoApiService.getInstance());
}
}

View File

@@ -1,28 +0,0 @@
/*!
* @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 { NgModule } from '@angular/core';
import { API_CLIENT_FACTORY_TOKEN } from '@alfresco/adf-core/api';
import { LegacyClientFactory } from './legacy-api-client.factory';
@NgModule({
providers: [
{ provide: API_CLIENT_FACTORY_TOKEN, useClass: LegacyClientFactory }
]
})
export class LegacyApiClientModule { }

View File

@@ -28,8 +28,14 @@ import { AdfHttpClient } from '@alfresco/adf-core/api';
* @returns factory function
*/
export function loadAppConfig(appConfigService: AppConfigService, storageService: StorageService, adfHttpClient: AdfHttpClient) {
return () => appConfigService.load().then(() => {
const init = () => {
adfHttpClient.disableCsrf = appConfigService.get<boolean>(AppConfigValues.DISABLECSRF, true);
storageService.prefix = appConfigService.get<string>(AppConfigValues.STORAGE_PREFIX, '');
});
}
appConfigService.select(AppConfigValues.STORAGE_PREFIX).subscribe((property) => {
storageService.prefix = property;
});
};
return () => appConfigService.load(init);
};

View File

@@ -188,4 +188,14 @@ describe('AppConfigService', () => {
expect(appConfigService.get('files.excluded')[0]).toBe('excluded');
});
it('should execute callback function if is passed to the load method', async () => {
const fakeCallBack = jasmine.createSpy('fakeCallBack');
fakeCallBack.and.returnValue(()=>{});
await appConfigService.load(fakeCallBack);
expect(fakeCallBack).toHaveBeenCalled();
});
});

View File

@@ -18,13 +18,14 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ObjectUtils } from '../common/utils/object-utils';
import { Observable, Subject } from 'rxjs';
import { Observable, ReplaySubject } from 'rxjs';
import { map, distinctUntilChanged, take } from 'rxjs/operators';
import { ExtensionConfig, ExtensionService, mergeObjects } from '@alfresco/adf-extensions';
import { OpenidConfiguration } from '../auth/interfaces/openid-configuration.interface';
import { OauthConfigModel } from '../auth/models/oauth-config.model';
/* spellchecker: disable */
// eslint-disable-next-line no-shadow
export enum AppConfigValues {
APP_CONFIG_LANGUAGES_KEY = 'languages',
@@ -44,7 +45,9 @@ export enum AppConfigValues {
AUTH_WITH_CREDENTIALS = 'auth.withCredentials',
APPLICATION = 'application',
STORAGE_PREFIX = 'application.storagePrefix',
NOTIFY_DURATION = 'notificationDefaultDuration'
NOTIFY_DURATION = 'notificationDefaultDuration',
CONTENT_TICKET_STORAGE_LABEL = 'ticket-ECM',
PROCESS_TICKET_STORAGE_LABEL = 'ticket-BPM'
}
// eslint-disable-next-line no-shadow
@@ -71,11 +74,15 @@ export class AppConfigService {
};
status: Status = Status.INIT;
protected onLoadSubject: Subject<any>;
protected onLoadSubject: ReplaySubject<any>;
onLoad: Observable<any>;
get isLoaded() {
return this.status === Status.LOADED;
}
constructor(protected http: HttpClient, protected extensionService: ExtensionService) {
this.onLoadSubject = new Subject();
this.onLoadSubject = new ReplaySubject();
this.onLoad = this.onLoadSubject.asObservable();
extensionService.setup$.subscribe((config) => {
@@ -92,7 +99,7 @@ export class AppConfigService {
select(property: string): Observable<any> {
return this.onLoadSubject
.pipe(
map((config) => config[property]),
map((config) => ObjectUtils.getValue(config, property)),
distinctUntilChanged()
);
}
@@ -160,8 +167,7 @@ export class AppConfigService {
this.onLoadSubject.next(this.config);
}
protected onDataLoaded(data: any) {
this.config = Object.assign({}, this.config, data || {});
protected onDataLoaded() {
this.onLoadSubject.next(this.config);
this.extensionService.setup$
@@ -182,9 +188,10 @@ export class AppConfigService {
/**
* Loads the config file.
*
* @param callback an optional callback to execute when configuration is loaded
* @returns Notification when loading is complete
*/
load(): Promise<any> {
load(callback?: (...args: any[]) => any): Promise<any> {
return new Promise((resolve) => {
const configUrl = `app.config.json?v=${Date.now()}`;
@@ -193,8 +200,10 @@ export class AppConfigService {
this.http.get(configUrl).subscribe(
(data: any) => {
this.status = Status.LOADED;
this.config = Object.assign({}, this.config, data || {});
callback?.();
resolve(data);
this.onDataLoaded(data);
this.onDataLoaded();
},
() => {
// eslint-disable-next-line no-console
@@ -227,6 +236,8 @@ export class AppConfigService {
resolve(res);
},
error: (err: any) => {
// eslint-disable-next-line no-console
console.error('hostIdp not correctly configured or unreachable');
reject(err);
}
});
@@ -262,4 +273,5 @@ export class AppConfigService {
return result;
}
}

View File

@@ -17,9 +17,10 @@
import { HttpClient, HttpHandler, HttpRequest } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { Observable, of } from 'rxjs';
import { EMPTY, Observable, of } from 'rxjs';
import { AuthBearerInterceptor } from './auth-bearer.interceptor';
import { AuthenticationService } from '../services/authentication.service';
import { RedirectAuthService } from '../oidc/redirect-auth.service';
const mockNext: HttpHandler = {
handle: () => new Observable(subscriber => {
@@ -40,7 +41,8 @@ describe('AuthBearerInterceptor', () => {
HttpClient,
HttpHandler,
AuthBearerInterceptor,
AuthenticationService
AuthenticationService,
{ provide: RedirectAuthService, useValue: { onLogin: EMPTY } }
]
});
@@ -85,7 +87,7 @@ describe('AuthBearerInterceptor', () => {
});
it('should interceptor add auth token to every URL if excluded URLs array is empty', () => {
spyOn(authService, 'getBearerExcludedUrls').and.returnValue([]);
spyOnProperty<any>(interceptor, 'bearerExcludedUrls').and.returnValue([]);
const mockUrls = [
'http://example.com/auth/realms/testpath',

View File

@@ -16,40 +16,37 @@
*/
import { throwError as observableThrowError, Observable } from 'rxjs';
import { Injectable, Injector } from '@angular/core';
import { Injectable } from '@angular/core';
import {
HttpHandler, HttpInterceptor, HttpRequest,
HttpSentEvent, HttpHeaderResponse, HttpProgressEvent, HttpResponse, HttpUserEvent, HttpHeaders
} from '@angular/common/http';
import { AuthenticationService } from '../services/authentication.service';
import { catchError, mergeMap } from 'rxjs/operators';
import { AuthenticationService } from '../services/authentication.service';
@Injectable()
export class AuthBearerInterceptor implements HttpInterceptor {
private excludedUrlsRegex: RegExp[];
private _bearerExcludedUrls: readonly string[] = ['resources/', 'assets/', 'auth/realms', 'idp/'];
constructor(private injector: Injector, private authService: AuthenticationService) { }
private excludedUrlsRegex: RegExp[];
constructor(private authenticationService: AuthenticationService) { }
private loadExcludedUrlsRegex() {
const excludedUrls = this.authService.getBearerExcludedUrls();
const excludedUrls = this.bearerExcludedUrls;
this.excludedUrlsRegex = excludedUrls.map((urlPattern) => new RegExp(`^https?://[^/]+/${urlPattern}`, 'i')) || [];
}
intercept(req: HttpRequest<any>, next: HttpHandler):
Observable<HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> {
this.authService = this.injector.get(AuthenticationService);
if (!this.authService?.getBearerExcludedUrls()) {
return next.handle(req);
}
if (!this.excludedUrlsRegex) {
this.loadExcludedUrlsRegex();
}
const urlRequest = req.url;
const shallPass: boolean = this.excludedUrlsRegex.some((regex) => regex.test(urlRequest));
const requestUrl = req.url;
const shallPass: boolean = this.excludedUrlsRegex.some((regex) => regex.test(requestUrl));
if (shallPass) {
return next.handle(req)
.pipe(
@@ -57,10 +54,10 @@ export class AuthBearerInterceptor implements HttpInterceptor {
);
}
return this.authService.addTokenToHeader(req.headers)
return this.authenticationService.addTokenToHeader(requestUrl, req.headers)
.pipe(
mergeMap((headersWithBearer) => {
const headerWithContentType = this.appendJsonContentType(headersWithBearer);
const headerWithContentType = this.appendJsonContentType(headersWithBearer, req.body);
const kcReq = req.clone({ headers: headerWithContentType});
return next.handle(kcReq)
.pipe(
@@ -70,7 +67,7 @@ export class AuthBearerInterceptor implements HttpInterceptor {
);
}
private appendJsonContentType(headers: HttpHeaders): HttpHeaders {
private appendJsonContentType(headers: HttpHeaders, reqBody: any): HttpHeaders {
// 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
@@ -79,11 +76,15 @@ export class AuthBearerInterceptor implements HttpInterceptor {
return headers.delete('Content-Type');
}
if (!headers.get('Content-Type')) {
if (!headers.get('Content-Type') && !(reqBody instanceof FormData)) {
return headers.set('Content-Type', 'application/json;charset=UTF-8');
}
return headers;
}
protected get bearerExcludedUrls(): readonly string[] {
return this._bearerExcludedUrls;
}
}

View File

@@ -0,0 +1,374 @@
/*!
* @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 { Injectable } from '@angular/core';
import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service';
import { Authentication } from '../interfaces/authentication.interface';
import { CookieService } from '../../common/services/cookie.service';
import { ContentAuth } from './content-auth';
import { ProcessAuth } from './process-auth';
import { catchError, map } from 'rxjs/operators';
import { from, Observable } from 'rxjs';
import { RedirectionModel } from '../models/redirection.model';
import { BaseAuthenticationService } from '../services/base-authentication.service';
import { LogService } from '../../common';
import { HttpHeaders } from '@angular/common/http';
const REMEMBER_ME_COOKIE_KEY = 'ALFRESCO_REMEMBER_ME';
const REMEMBER_ME_UNTIL = 1000 * 60 * 60 * 24 * 30;
@Injectable({
providedIn: 'root'
})
export class BasicAlfrescoAuthService extends BaseAuthenticationService {
protected redirectUrl: RedirectionModel = null;
authentications: Authentication = {
basicAuth: {
ticket: ''
},
type: 'basic'
};
constructor(
logService: LogService,
appConfig: AppConfigService,
cookie: CookieService,
private contentAuth: ContentAuth,
private processAuth: ProcessAuth
) {
super(appConfig, cookie, logService);
this.appConfig.onLoad
.subscribe(() => {
if (!this.isOauth() && this.isLoggedIn()) {
this.onLogin.next('logged-in');
}
});
this.contentAuth.onLogout.pipe(map((event) => {
this.onLogout.next(event);
}));
this.contentAuth.onLogin.pipe(map((event) => {
this.onLogin.next(event);
}));
this.contentAuth.onError.pipe(map((event) => {
this.onError.next(event);
}));
this.processAuth.onLogout.pipe(map((event) => {
this.onLogout.next(event);
}));
this.processAuth.onLogin.pipe(map((event) => {
this.onLogin.next(event);
}));
this.processAuth.onError.pipe(map((event) => {
this.onError.next(event);
}));
}
/**
* Logs the user in.
*
* @param username Username for the login
* @param password Password for the login
* @param rememberMe Stores the user's login details if true
* @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 }> {
return from(this.executeLogin(username, password)).pipe(
map((response: any) => {
this.saveRememberMeCookie(rememberMe);
this.onLogin.next(response);
return {
type: this.appConfig.get(AppConfigValues.PROVIDERS),
ticket: response
};
}),
catchError((err) => this.handleError(err))
);
}
/**
* login Alfresco API
*
* @param username username to login
* @param password password to login
* @returns A promise that returns {new authentication ticket} if resolved and {error} if rejected.
*/
async executeLogin(username: string, password: string): Promise<any> {
if (!this.isCredentialValid(username) || !this.isCredentialValid(password)) {
return Promise.reject(new Error('missing username or password'));
}
if (username) {
username = username.trim();
}
if (this.isBPMProvider()) {
try {
return await this.processAuth.login(username, password);
} catch (e) {
return Promise.reject(e);
}
} else if (this.isECMProvider()) {
try {
return await this.contentAuth.login(username, password);
} catch (e) {
return Promise.reject(e);
}
} else if (this.isALLProvider()) {
return this.loginBPMECM(username, password);
} else {
return Promise.reject(new Error('Unknown configuration'));
}
}
private loginBPMECM(username: string, password: string): Promise<any> {
const contentPromise = this.contentAuth.login(username, password);
const processPromise = this.processAuth.login(username, password);
return new Promise((resolve, reject) => {
Promise.all([contentPromise, processPromise]).then(
(data) => {
this.onLogin.next('success');
resolve(data);
},
(error) => {
this.contentAuth.invalidateSession();
this.processAuth.invalidateSession();
if (error.status === 401) {
this.onError.next('unauthorized');
}
this.onError.next('error');
reject(error);
});
});
}
/**
* 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;
}
/**
* 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);
}
isCredentialValid(credential: string): boolean {
return credential !== undefined && credential !== null && credential !== '';
}
getToken(): string {
if (this.isBPMProvider()) {
return this.processAuth.getToken();
} else if (this.isECMProvider()) {
return this.contentAuth.getToken();
} else if (this.isALLProvider()) {
return this.contentAuth.getToken();
} else {
return '';
}
}
/**
* @deprecated
* @returns content auth token
*/
getTicketEcm(): string {
return this.contentAuth.getToken();
}
/**
* @deprecated
* @returns process auth token
*/
getTicketBpm(): string {
return this.processAuth.getToken();
}
isBpmLoggedIn(): boolean {
return this.processAuth.isLoggedIn();
}
isEcmLoggedIn(): boolean {
return this.contentAuth.isLoggedIn();
}
isLoggedIn(): boolean {
const authWithCredentials = this.isKerberosEnabled();
if (this.isBPMProvider()) {
return this.processAuth.isLoggedIn();
} else if (this.isECMProvider()) {
return authWithCredentials ? true : this.contentAuth.isLoggedIn();
} else if (this.isALLProvider()) {
return authWithCredentials ? true : (this.contentAuth.isLoggedIn() && this.processAuth.isLoggedIn());
} else {
return false;
}
}
/**
* logout Alfresco API
*/
async logout(): Promise<any> {
if (this.isBPMProvider()) {
return this.processAuth.logout();
} else if (this.isECMProvider()) {
return this.contentAuth.logout();
} else if (this.isALLProvider()) {
return this.logoutBPMECM();
}
return Promise.resolve();
}
private logoutBPMECM(): Promise<any> {
const contentPromise = this.contentAuth.logout();
const processPromise = this.processAuth.logout();
return new Promise((resolve, reject) => {
Promise.all([contentPromise, processPromise]).then(
() => {
this.contentAuth.ticket = undefined;
this.processAuth.ticket = undefined;
this.onLogout.next('logout');
resolve('logout');
},
(error) => {
if (error.status === 401) {
this.onError.next('unauthorized');
}
this.onError.next('error');
reject(error);
});
});
}
reset(): void {
}
/**
* 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;
}
setRedirect(url?: RedirectionModel) {
this.redirectUrl = url;
}
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');
}
getBpmUsername(): string {
return this.processAuth.getUsername();
}
getEcmUsername(): string {
return this.contentAuth.getUsername();
}
getUsername(): string {
if (this.isBPMProvider()) {
return this.processAuth.getUsername();
} else if (this.isECMProvider()) {
return this.contentAuth.getUsername();
} else {
return this.contentAuth.getUsername();
}
}
/**
* Does kerberos enabled?
*
* @returns True if enabled, false otherwise
*/
isKerberosEnabled(): boolean {
return this.appConfig.get<boolean>(AppConfigValues.AUTH_WITH_CREDENTIALS, false);
}
getAuthHeaders(requestUrl: string, header: HttpHeaders): HttpHeaders {
return this.addBasicAuth(requestUrl, header);
}
private addBasicAuth(requestUrl: string, header: HttpHeaders): HttpHeaders {
const ticket = this.getTicketEcmBase64(requestUrl);
if (!ticket) {
return header;
}
return header.set('Authorization', ticket);
}
async requireAlfTicket(): Promise<void> {
return this.contentAuth.requireAlfTicket();
}
/**
* Gets the BPM ticket from the Storage in Base 64 format.
*
* @param requestUrl the request url
* @returns The ticket or `null` if none was found
*/
private getTicketEcmBase64(requestUrl: string): string | null {
let ticket = null;
const contextRootBpm = this.appConfig.get<string>(AppConfigValues.CONTEXTROOTBPM) || 'activiti-app';
const contextRoot = this.appConfig.get<string>(AppConfigValues.CONTEXTROOTECM) || 'alfresco';
if (contextRoot && requestUrl.indexOf(contextRoot) !== -1) {
ticket = 'Basic ' + btoa(this.contentAuth.getToken());
} else if (contextRootBpm && requestUrl.indexOf(contextRootBpm) !== -1) {
ticket = 'Basic ' + this.processAuth.getToken();
}
return ticket;
}
}

View File

@@ -0,0 +1,220 @@
/*!
* @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 { Injectable } from '@angular/core';
import { AdfHttpClient } from '@alfresco/adf-core/api';
import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service';
import { StorageService } from '../../common/services/storage.service';
import { ReplaySubject, Subject } from 'rxjs';
import { Authentication } from '../interfaces/authentication.interface';
export interface TicketBody {
userId?: string;
password?: string;
}
export interface TicketEntry {
entry: {
id?: string;
userId?: string;
};
}
@Injectable({
providedIn: 'root'
})
export class ContentAuth {
onLogin = new ReplaySubject<any>(1);
onLogout = new ReplaySubject<any>(1);
onError = new Subject<any>();
ticket: string;
config = {
ticketEcm: null
};
authentications: Authentication = {
basicAuth: {
ticket: ''
},
type: 'basic'
};
get basePath(): string {
const contextRootEcm = this.appConfigService.get<string>(AppConfigValues.CONTEXTROOTECM) || 'alfresco';
return this.appConfigService.get<string>(AppConfigValues.ECMHOST) + '/' + contextRootEcm + '/api/-default-/public/authentication/versions/1';
}
constructor(private appConfigService: AppConfigService,
private adfHttpClient: AdfHttpClient,
private storageService: StorageService) {
this.appConfigService.onLoad.subscribe(() => {
this.setConfig();
});
}
private setConfig() {
if (this.storageService.getItem(AppConfigValues.CONTENT_TICKET_STORAGE_LABEL)) {
this.setTicket(this.storageService.getItem(AppConfigValues.CONTENT_TICKET_STORAGE_LABEL));
}
}
saveUsername(username: string) {
this.storageService.setItem('ACS_USERNAME', username);
}
getUsername() {
return this.storageService.getItem('ACS_USERNAME');
}
/**
* login Alfresco API
*
* @param username username to login
* @param password password to login
* @returns A promise that returns {new authentication ticket} if resolved and {error} if rejected.
*/
login(username: string, password: string): Promise<any> {
this.authentications.basicAuth.username = username;
this.authentications.basicAuth.password = password;
const loginRequest: any = {};
loginRequest.userId = this.authentications.basicAuth.username;
loginRequest.password = this.authentications.basicAuth.password;
return new Promise((resolve, reject) => {
this.createTicket(loginRequest)
.then((data: any) => {
this.saveUsername(username);
this.setTicket(data.entry.id);
this.adfHttpClient.emit('success');
this.onLogin.next('success');
resolve(data.entry.id);
})
.catch((error) => {
this.saveUsername('');
if (error.status === 401) {
this.adfHttpClient.emit('unauthorized', error);
this.onError.next('unauthorized');
} else if (error.status === 403) {
this.adfHttpClient.emit('forbidden', error);
this.onError.next('forbidden');
} else {
this.adfHttpClient.emit('error', error);
this.onError.next('error');
}
reject(error);
});
});
}
/**
* logout Alfresco API
*
* @returns A promise that returns { authentication ticket} if resolved and {error} if rejected.
*/
logout(): Promise<any> {
this.saveUsername('');
return new Promise((resolve, reject) => {
this.deleteTicket().then(
() => {
this.invalidateSession();
this.adfHttpClient.emit('logout');
this.onLogout.next('logout');
resolve('logout');
},
(error) => {
if (error.status === 401) {
this.adfHttpClient.emit('unauthorized');
this.onError.next('unauthorized');
}
this.adfHttpClient.emit('error');
this.onError.next('error');
reject(error);
});
});
}
/**
* Set the current Ticket
*
* @param ticket a string representing the ticket
*/
setTicket(ticket: string) {
this.authentications.basicAuth.username = 'ROLE_TICKET';
this.authentications.basicAuth.password = ticket;
this.config.ticketEcm = ticket;
this.storageService.setItem(AppConfigValues.CONTENT_TICKET_STORAGE_LABEL, ticket);
this.ticket = ticket;
}
/**
* @returns the current Ticket
*/
getToken(): string {
if (!this.ticket) {
this.onError.next('error');
}
return this.ticket;
}
invalidateSession() {
this.storageService.removeItem(AppConfigValues.CONTENT_TICKET_STORAGE_LABEL);
this.authentications.basicAuth.username = null;
this.authentications.basicAuth.password = null;
this.config.ticketEcm = null;
this.ticket = null;
}
/**
* @returns If the client is logged in return true
*/
isLoggedIn(): boolean {
return !!this.ticket;
}
/**
* @returns return the Authentication
*/
getAuthentication() {
return this.authentications;
}
createTicket(ticketBodyCreate: TicketBody): Promise<TicketEntry> {
if (ticketBodyCreate === null || ticketBodyCreate === undefined) {
this.onError.next((`Missing param ticketBodyCreate`));
throw new Error(`Missing param ticketBodyCreate`);
}
return this.adfHttpClient.post(this.basePath + '/tickets', {bodyParam: ticketBodyCreate});
}
async requireAlfTicket(): Promise<void> {
const ticket = await this.adfHttpClient.get(this.basePath + '/tickets/-me-');
this.setTicket(ticket.entry.id);
}
deleteTicket(): Promise<any> {
return this.adfHttpClient.delete(this.basePath + '/tickets/-me-');
}
}

View File

@@ -0,0 +1,210 @@
/*!
* @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 { Injectable } from '@angular/core';
import { AdfHttpClient } from '@alfresco/adf-core/api';
import { Authentication } from '../interfaces/authentication.interface';
import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service';
import { StorageService } from '../../common/services/storage.service';
import { ReplaySubject, Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ProcessAuth {
onLogin = new ReplaySubject<any>(1);
onLogout = new ReplaySubject<any>(1);
onError = new Subject<any>();
ticket: string;
config = {
ticketBpm: null
};
authentications: Authentication = {
basicAuth: {ticket: ''}, type: 'activiti'
};
get basePath(): string {
const contextRootBpm = this.appConfigService.get<string>(AppConfigValues.CONTEXTROOTBPM) || 'activiti-app';
return this.appConfigService.get<string>(AppConfigValues.BPMHOST) + '/' + contextRootBpm;
}
constructor(private appConfigService: AppConfigService,
private adfHttpClient: AdfHttpClient,
private storageService: StorageService) {
this.appConfigService.onLoad.subscribe(() => {
this.setConfig();
});
}
private setConfig() {
this.ticket = undefined;
this.setTicket(this.storageService.getItem(AppConfigValues.PROCESS_TICKET_STORAGE_LABEL));
}
saveUsername(username: string) {
this.storageService.setItem('APS_USERNAME', username);
}
getUsername() {
return this.storageService.getItem('APS_USERNAME');
}
/**
* login Activiti API
*
* @param username Username to login
* @param password Password to login
* @returns A promise that returns {new authentication ticket} if resolved and {error} if rejected.
*/
login(username: string, password: string): Promise<any> {
this.authentications.basicAuth.username = username;
this.authentications.basicAuth.password = password;
const options = {
headerParams: {
'Content-Type': 'application/x-www-form-urlencoded',
'Cache-Control': 'no-cache'
},
formParams: {
j_username: this.authentications.basicAuth.username,
j_password: this.authentications.basicAuth.password,
_spring_security_remember_me: true,
submit: 'Login'
},
contentType: 'application/x-www-form-urlencoded',
accept: 'application/json'
};
const promise: any = new Promise((resolve, reject) => {
this.adfHttpClient.post(this.basePath + '/app/authentication', options).then(
() => {
this.saveUsername(username);
const ticket = this.basicAuth(this.authentications.basicAuth.username, this.authentications.basicAuth.password);
this.setTicket(ticket);
this.onLogin.next('success');
this.adfHttpClient.emit('success');
this.adfHttpClient.emit('logged-in');
resolve(ticket);
},
(error) => {
this.saveUsername('');
if (error.status === 401) {
this.adfHttpClient.emit('unauthorized', error);
this.onError.next('unauthorized');
} else if (error.status === 403) {
this.adfHttpClient.emit('forbidden', error);
this.onError.next('forbidden');
} else {
this.adfHttpClient.emit('error', error);
this.onError.next('error');
}
reject(error);
});
});
return promise;
}
/**
* logout Alfresco API
*
* @returns A promise that returns {new authentication ticket} if resolved and {error} if rejected.
*/
async logout(): Promise<any> {
this.saveUsername('');
return new Promise((resolve, reject) => {
this.adfHttpClient.get(this.basePath + `/app/logout`, {}).then(
() => {
this.invalidateSession();
this.onLogout.next('logout');
this.adfHttpClient.emit('logout');
resolve('logout');
},
(error) => {
if (error.status === 401) {
this.adfHttpClient.emit('unauthorized');
this.onError.next('unauthorized');
}
this.adfHttpClient.emit('error');
this.onError.next('error');
reject(error);
});
});
}
basicAuth(username: string, password: string): string {
const str: any = username + ':' + password;
let base64;
if (typeof Buffer === 'function') {
base64 = Buffer.from(str.toString(), 'binary').toString('base64');
} else {
base64 = btoa(str);
}
return `Basic ${base64}`;
}
/**
* Set the current Ticket
*
* @param ticket a string representing the ticket
*/
setTicket(ticket: string) {
if (ticket && ticket !== 'null') {
this.authentications.basicAuth.ticket = ticket;
this.authentications.basicAuth.password = null;
this.config.ticketBpm = ticket;
this.storageService.setItem(AppConfigValues.PROCESS_TICKET_STORAGE_LABEL, ticket);
this.ticket = ticket;
}
}
invalidateSession() {
this.storageService.removeItem(AppConfigValues.PROCESS_TICKET_STORAGE_LABEL);
this.authentications.basicAuth.ticket = null;
this.authentications.basicAuth.password = null;
this.authentications.basicAuth.username = null;
this.config.ticketBpm = null;
this.ticket = null;
}
/**
* @returns the current Ticket
*/
getToken(): string {
if (!this.ticket) {
this.onError.next('error');
return null;
}
return this.ticket;
}
/**
* @returns If the client is logged in return true
*/
isLoggedIn(): boolean {
return !!this.ticket;
}
}

View File

@@ -17,24 +17,35 @@
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, UrlTree } from '@angular/router';
import { AuthenticationService } from '../services/authentication.service';
import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service';
import {
AppConfigService,
AppConfigValues
} from '../../app-config/app-config.service';
import { OauthConfigModel } from '../models/oauth-config.model';
import { MatDialog } from '@angular/material/dialog';
import { StorageService } from '../../common/services/storage.service';
import { Observable } from 'rxjs';
import { inject } from '@angular/core';
import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service';
import { OidcAuthenticationService } from '../services/oidc-authentication.service';
export abstract class AuthGuardBase implements CanActivate, CanActivateChild {
protected authenticationService = inject(AuthenticationService);
protected router = inject(Router);
protected appConfigService = inject(AppConfigService);
protected dialog = inject(MatDialog);
private storageService = inject(StorageService);
protected get withCredentials(): boolean {
return this.appConfigService.get<boolean>('auth.withCredentials', false);
}
constructor(
protected authenticationService: AuthenticationService,
protected basicAlfrescoAuthService: BasicAlfrescoAuthService,
protected oidcAuthenticationService: OidcAuthenticationService,
protected router: Router,
protected appConfigService: AppConfigService,
protected dialog: MatDialog,
private storageService: StorageService
) {
}
abstract checkLogin(
activeRoute: ActivatedRouteSnapshot,
redirectUrl: string
@@ -78,15 +89,17 @@ export abstract class AuthGuardBase implements CanActivate, CanActivateChild {
let urlToRedirect = `/${this.getLoginRoute()}`;
if (!this.authenticationService.isOauth()) {
this.authenticationService.setRedirect({
this.basicAlfrescoAuthService.setRedirect({
provider: this.getProvider(),
url
});
urlToRedirect = `${urlToRedirect}?redirectUrl=${url}`;
return this.navigate(urlToRedirect);
} else if (this.getOauthConfig().silentLogin && !this.authenticationService.isPublicUrl()) {
this.authenticationService.ssoImplicitLogin();
} else if (this.getOauthConfig().silentLogin && !this.oidcAuthenticationService.isPublicUrl()) {
if (!this.oidcAuthenticationService.hasValidIdToken() || !this.oidcAuthenticationService.hasValidAccessToken()) {
this.oidcAuthenticationService.ssoImplicitLogin();
}
} else {
return this.navigate(urlToRedirect);
}
@@ -101,7 +114,13 @@ export abstract class AuthGuardBase implements CanActivate, CanActivateChild {
}
protected getOauthConfig(): OauthConfigModel {
return this.appConfigService.oauth2;
return (
this.appConfigService &&
this.appConfigService.get<OauthConfigModel>(
AppConfigValues.OAUTHCONFIG,
null
)
);
}
protected getLoginRoute(): string {
@@ -113,12 +132,21 @@ export abstract class AuthGuardBase implements CanActivate, CanActivateChild {
}
protected isOAuthWithoutSilentLogin(): boolean {
const oauth = this.appConfigService.oauth2;
return this.authenticationService.isOauth() && !!oauth && !oauth.silentLogin;
const oauth = this.appConfigService.get<OauthConfigModel>(
AppConfigValues.OAUTHCONFIG,
null
);
return (
this.authenticationService.isOauth() && !!oauth && !oauth.silentLogin
);
}
protected isSilentLogin(): boolean {
const oauth = this.appConfigService.oauth2;
const oauth = this.appConfigService.get<OauthConfigModel>(
AppConfigValues.OAUTHCONFIG,
null
);
return this.authenticationService.isOauth() && oauth && oauth.silentLogin;
}
}

View File

@@ -23,11 +23,16 @@ import { RouterStateSnapshot, Router } from '@angular/router';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { MatDialog } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service';
import { OidcAuthenticationService } from '../services/oidc-authentication.service';
describe('AuthGuardService BPM', () => {
let authGuard: AuthGuardBpm;
let authService: AuthenticationService;
let basicAlfrescoAuthService: BasicAlfrescoAuthService;
let oidcAuthenticationService: OidcAuthenticationService;
let router: Router;
let appConfigService: AppConfigService;
@@ -36,9 +41,21 @@ describe('AuthGuardService BPM', () => {
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
providers: [
{
provide: OidcAuthenticationService, useValue: {
ssoImplicitLogin: () => { },
isPublicUrl: () => false,
hasValidIdToken: () => false,
isLoggedIn: () => false
}
}
]
});
localStorage.clear();
basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService);
oidcAuthenticationService = TestBed.inject(OidcAuthenticationService);
authService = TestBed.inject(AuthenticationService);
authGuard = TestBed.inject(AuthGuardBpm);
router = TestBed.inject(Router);
@@ -53,8 +70,8 @@ describe('AuthGuardService BPM', () => {
spyOn(router, 'navigateByUrl').and.stub();
spyOn(authService, 'isBpmLoggedIn').and.returnValue(false);
spyOn(authService, 'isOauth').and.returnValue(true);
spyOn(authService, 'isPublicUrl').and.returnValue(false);
spyOn(authService, 'ssoImplicitLogin').and.stub();
spyOn(oidcAuthenticationService, 'isPublicUrl').and.returnValue(false);
spyOn(oidcAuthenticationService, 'ssoImplicitLogin').and.stub();
appConfigService.config.oauth2 = {
silentLogin: true,
@@ -69,7 +86,7 @@ describe('AuthGuardService BPM', () => {
const route = { url: 'abc' } as RouterStateSnapshot;
expect(await authGuard.canActivate(null, route)).toBeFalsy();
expect(authService.ssoImplicitLogin).toHaveBeenCalledTimes(1);
expect(oidcAuthenticationService.ssoImplicitLogin).toHaveBeenCalledTimes(1);
});
it('if the alfresco js api is logged in should canActivate be true', async () => {
@@ -130,53 +147,53 @@ describe('AuthGuardService BPM', () => {
});
it('should set redirect url', () => {
spyOn(authService, 'setRedirect').and.callThrough();
spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough();
spyOn(router, 'navigateByUrl').and.stub();
const route = { url: 'some-url' } as RouterStateSnapshot;
authGuard.canActivate(null, route);
expect(authService.setRedirect).toHaveBeenCalledWith({
expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({
provider: 'BPM', url: 'some-url'
});
expect(authService.getRedirect()).toEqual('some-url');
expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url');
});
it('should set redirect navigation commands with query params', () => {
spyOn(authService, 'setRedirect').and.callThrough();
spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough();
spyOn(router, 'navigateByUrl').and.stub();
const route = { url: 'some-url;q=123' } as RouterStateSnapshot;
authGuard.canActivate(null, route);
expect(authService.setRedirect).toHaveBeenCalledWith({
expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({
provider: 'BPM', url: 'some-url;q=123'
});
expect(authService.getRedirect()).toEqual('some-url;q=123');
expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url;q=123');
});
it('should set redirect navigation commands with query params', () => {
spyOn(authService, 'setRedirect').and.callThrough();
spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough();
spyOn(router, 'navigateByUrl').and.stub();
const route = { url: '/' } as RouterStateSnapshot;
authGuard.canActivate(null, route);
expect(authService.setRedirect).toHaveBeenCalledWith({
expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({
provider: 'BPM', url: '/'
});
expect(authService.getRedirect()).toEqual('/');
expect(basicAlfrescoAuthService.getRedirect()).toEqual('/');
});
it('should get redirect url from config if there is one configured', () => {
appConfigService.config.loginRoute = 'fakeLoginRoute';
spyOn(authService, 'setRedirect').and.callThrough();
spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough();
spyOn(router, 'navigateByUrl').and.stub();
const route = { url: 'some-url' } as RouterStateSnapshot;
authGuard.canActivate(null, route);
expect(authService.setRedirect).toHaveBeenCalledWith({
expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({
provider: 'BPM', url: 'some-url'
});
expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl('/fakeLoginRoute?redirectUrl=some-url'));
@@ -187,13 +204,13 @@ describe('AuthGuardService BPM', () => {
spyOn(materialDialog, 'closeAll');
spyOn(authService, 'setRedirect').and.callThrough();
spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough();
spyOn(router, 'navigateByUrl').and.stub();
const route = { url: 'some-url' } as RouterStateSnapshot;
authGuard.canActivate(null, route);
expect(authService.setRedirect).toHaveBeenCalledWith({
expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({
provider: 'BPM', url: 'some-url'
});

View File

@@ -16,13 +16,30 @@
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, UrlTree } from '@angular/router';
import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router';
import { AppConfigService } from '../../app-config/app-config.service';
import { AuthenticationService } from '../services/authentication.service';
import { AuthGuardBase } from './auth-guard-base';
import { MatDialog } from '@angular/material/dialog';
import { StorageService } from '../../common/services/storage.service';
import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service';
import { OidcAuthenticationService } from '../services/oidc-authentication.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuardBpm extends AuthGuardBase {
constructor(authenticationService: AuthenticationService,
basicAlfrescoAuthService: BasicAlfrescoAuthService,
oidcAuthenticationService: OidcAuthenticationService,
router: Router,
appConfigService: AppConfigService,
dialog: MatDialog,
storageService: StorageService) {
super(authenticationService,basicAlfrescoAuthService, oidcAuthenticationService,router, appConfigService, dialog, storageService);
}
async checkLogin(_: ActivatedRouteSnapshot, redirectUrl: string): Promise<boolean | UrlTree> {
if (this.authenticationService.isBpmLoggedIn() || this.withCredentials) {
return true;

View File

@@ -23,11 +23,15 @@ import { RouterStateSnapshot, Router } from '@angular/router';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { MatDialog } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
import { OidcAuthenticationService } from '../services/oidc-authentication.service';
import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service';
describe('AuthGuardService ECM', () => {
let authGuard: AuthGuardEcm;
let authService: AuthenticationService;
let basicAlfrescoAuthService: BasicAlfrescoAuthService;
let oidcAuthenticationService: OidcAuthenticationService;
let router: Router;
let appConfigService: AppConfigService;
@@ -36,9 +40,21 @@ describe('AuthGuardService ECM', () => {
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
providers: [
{
provide: OidcAuthenticationService, useValue: {
ssoImplicitLogin: () => { },
isPublicUrl: () => false,
hasValidIdToken: () => false,
isLoggedIn: () => false
}
}
]
});
localStorage.clear();
oidcAuthenticationService = TestBed.inject(OidcAuthenticationService);
basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService);
authService = TestBed.inject(AuthenticationService);
authGuard = TestBed.inject(AuthGuardEcm);
router = TestBed.inject(Router);
@@ -98,8 +114,8 @@ describe('AuthGuardService ECM', () => {
it('should redirect url if the alfresco js api is NOT logged in and isOAuth with silentLogin', async () => {
spyOn(authService, 'isEcmLoggedIn').and.returnValue(false);
spyOn(authService, 'isOauth').and.returnValue(true);
spyOn(authService, 'isPublicUrl').and.returnValue(false);
spyOn(authService, 'ssoImplicitLogin').and.stub();
spyOn(oidcAuthenticationService, 'isPublicUrl').and.returnValue(false);
spyOn(oidcAuthenticationService, 'ssoImplicitLogin').and.stub();
appConfigService.config.oauth2 = {
silentLogin: true,
@@ -113,7 +129,7 @@ describe('AuthGuardService ECM', () => {
const route = {url : 'abc'} as RouterStateSnapshot;
expect(await authGuard.canActivate(null, route)).toBeFalsy();
expect(authService.ssoImplicitLogin).toHaveBeenCalledTimes(1);
expect(oidcAuthenticationService.ssoImplicitLogin).toHaveBeenCalledTimes(1);
});
it('should not redirect url if NOT logged in and isOAuth but no silentLogin configured', async () => {
@@ -128,53 +144,53 @@ describe('AuthGuardService ECM', () => {
});
it('should set redirect navigation commands', () => {
spyOn(authService, 'setRedirect').and.callThrough();
spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough();
spyOn(router, 'navigateByUrl').and.stub();
const route = { url: 'some-url' } as RouterStateSnapshot;
authGuard.canActivate(null, route);
expect(authService.setRedirect).toHaveBeenCalledWith({
expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({
provider: 'ECM', url: 'some-url'
});
expect(authService.getRedirect()).toEqual('some-url');
expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url');
});
it('should set redirect navigation commands with query params', () => {
spyOn(authService, 'setRedirect').and.callThrough();
spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough();
spyOn(router, 'navigateByUrl').and.stub();
const route = { url: 'some-url;q=123' } as RouterStateSnapshot;
authGuard.canActivate(null, route);
expect(authService.setRedirect).toHaveBeenCalledWith({
expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({
provider: 'ECM', url: 'some-url;q=123'
});
expect(authService.getRedirect()).toEqual('some-url;q=123');
expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url;q=123');
});
it('should set redirect navigation commands with query params', () => {
spyOn(authService, 'setRedirect').and.callThrough();
spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough();
spyOn(router, 'navigateByUrl').and.stub();
const route = { url: '/' } as RouterStateSnapshot;
authGuard.canActivate(null, route);
expect(authService.setRedirect).toHaveBeenCalledWith({
expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({
provider: 'ECM', url: '/'
});
expect(authService.getRedirect()).toEqual('/');
expect(basicAlfrescoAuthService.getRedirect()).toEqual('/');
});
it('should get redirect url from config if there is one configured', () => {
appConfigService.config.loginRoute = 'fakeLoginRoute';
spyOn(authService, 'setRedirect').and.callThrough();
spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough();
spyOn(router, 'navigateByUrl').and.stub();
const route = { url: 'some-url' } as RouterStateSnapshot;
authGuard.canActivate(null, route);
expect(authService.setRedirect).toHaveBeenCalledWith({
expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({
provider: 'ECM', url: 'some-url'
});
expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl('/fakeLoginRoute?redirectUrl=some-url'));
@@ -185,13 +201,13 @@ describe('AuthGuardService ECM', () => {
spyOn(materialDialog, 'closeAll');
spyOn(authService, 'setRedirect').and.callThrough();
spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough();
spyOn(router, 'navigateByUrl').and.stub();
const route = { url: 'some-url' } as RouterStateSnapshot;
authGuard.canActivate(null, route);
expect(authService.setRedirect).toHaveBeenCalledWith({
expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({
provider: 'ECM', url: 'some-url'
});

View File

@@ -16,13 +16,33 @@
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, UrlTree } from '@angular/router';
import {
ActivatedRouteSnapshot, Router, UrlTree
} from '@angular/router';
import { AuthenticationService } from '../services/authentication.service';
import { AppConfigService } from '../../app-config/app-config.service';
import { AuthGuardBase } from './auth-guard-base';
import { MatDialog } from '@angular/material/dialog';
import { StorageService } from '../../common/services/storage.service';
import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service';
import { OidcAuthenticationService } from '../services/oidc-authentication.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuardEcm extends AuthGuardBase {
constructor(authenticationService: AuthenticationService,
basicAlfrescoAuthService: BasicAlfrescoAuthService,
oidcAuthenticationService: OidcAuthenticationService,
router: Router,
appConfigService: AppConfigService,
dialog: MatDialog,
storageService: StorageService) {
super(authenticationService, basicAlfrescoAuthService, oidcAuthenticationService, router, appConfigService, dialog, storageService);
}
async checkLogin(_: ActivatedRouteSnapshot, redirectUrl: string): Promise<boolean | UrlTree> {
if (this.authenticationService.isEcmLoggedIn() || this.withCredentials) {
return true;

View File

@@ -23,6 +23,8 @@ import { AuthenticationService } from '../services/authentication.service';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { StorageService } from '../../common/services/storage.service';
import { OidcAuthenticationService } from '../services/oidc-authentication.service';
import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service';
describe('AuthGuardService', () => {
let state;
@@ -31,17 +33,30 @@ describe('AuthGuardService', () => {
let authGuard: AuthGuard;
let storageService: StorageService;
let appConfigService: AppConfigService;
let basicAlfrescoAuthService: BasicAlfrescoAuthService;
let oidcAuthenticationService: OidcAuthenticationService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
providers: [
{
provide: OidcAuthenticationService, useValue: {
ssoImplicitLogin: () => { },
isPublicUrl: () => false,
hasValidIdToken: () => false
}
}
]
});
localStorage.clear();
state = { url: '' };
authService = TestBed.inject(AuthenticationService);
basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService);
oidcAuthenticationService = TestBed.inject(OidcAuthenticationService);
router = TestBed.inject(Router);
authGuard = TestBed.inject(AuthGuard);
appConfigService = TestBed.inject(AppConfigService);
@@ -110,13 +125,13 @@ describe('AuthGuardService', () => {
});
it('should NOT redirect url if the User is NOT logged in and isOAuth but with silentLogin configured', async () => {
spyOn(authService, 'ssoImplicitLogin').and.stub();
spyOn(oidcAuthenticationService, 'ssoImplicitLogin').and.stub();
spyOn(authService, 'isLoggedIn').and.returnValue(false);
spyOn(authService, 'isOauth').and.returnValue(true);
appConfigService.config.oauth2.silentLogin = true;
expect(await authGuard.canActivate(null, state)).toBeFalsy();
expect(authService.ssoImplicitLogin).toHaveBeenCalledTimes(1);
expect(oidcAuthenticationService.ssoImplicitLogin).toHaveBeenCalledTimes(1);
});
it('should set redirect url', async () => {
@@ -124,11 +139,11 @@ describe('AuthGuardService', () => {
appConfigService.config.loginRoute = 'login';
spyOn(router, 'navigateByUrl');
spyOn(authService, 'setRedirect');
spyOn(basicAlfrescoAuthService, 'setRedirect');
await authGuard.canActivate(null, state);
expect(authService.setRedirect).toHaveBeenCalledWith({
expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({
provider: 'ALL', url: 'some-url'
});
expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl('/login?redirectUrl=some-url'));
@@ -140,11 +155,11 @@ describe('AuthGuardService', () => {
appConfigService.config.provider = 'ALL';
spyOn(router, 'navigateByUrl');
spyOn(authService, 'setRedirect');
spyOn(basicAlfrescoAuthService, 'setRedirect');
await authGuard.canActivate(null, state);
expect(authService.setRedirect).toHaveBeenCalledWith({
expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({
provider: 'ALL', url: 'some-url;q=query'
});
expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl('/login?redirectUrl=some-url;q=query'));
@@ -155,11 +170,11 @@ describe('AuthGuardService', () => {
appConfigService.config.loginRoute = 'fakeLoginRoute';
spyOn(router, 'navigateByUrl');
spyOn(authService, 'setRedirect');
spyOn(basicAlfrescoAuthService, 'setRedirect');
await authGuard.canActivate(null, state);
expect(authService.setRedirect).toHaveBeenCalledWith({
expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({
provider: 'ALL', url: 'some-url'
});
expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl('/fakeLoginRoute?redirectUrl=some-url'));
@@ -169,11 +184,11 @@ describe('AuthGuardService', () => {
state.url = '/';
spyOn(router, 'navigateByUrl');
spyOn(authService, 'setRedirect');
spyOn(basicAlfrescoAuthService, 'setRedirect');
await authGuard.canActivate(null, state);
expect(authService.setRedirect).toHaveBeenCalledWith({
expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({
provider: 'ALL', url: '/'
});
});

View File

@@ -16,9 +16,16 @@
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, UrlTree } from '@angular/router';
import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router';
import { AuthenticationService } from '../services/authentication.service';
import { AppConfigService } from '../../app-config/app-config.service';
import { AuthGuardBase } from './auth-guard-base';
import { JwtHelperService } from '../services/jwt-helper.service';
import { MatDialog } from '@angular/material/dialog';
import { StorageService } from '../../common/services/storage.service';
import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service';
import { OidcAuthenticationService } from '../services/oidc-authentication.service';
@Injectable({
providedIn: 'root'
@@ -27,8 +34,15 @@ export class AuthGuard extends AuthGuardBase {
ticketChangeBind: any;
constructor(private jwtHelperService: JwtHelperService) {
super();
constructor(private jwtHelperService: JwtHelperService,
authenticationService: AuthenticationService,
basicAlfrescoAuthService: BasicAlfrescoAuthService,
oidcAuthenticationService: OidcAuthenticationService,
router: Router,
appConfigService: AppConfigService,
dialog: MatDialog,
storageService: StorageService) {
super(authenticationService, basicAlfrescoAuthService, oidcAuthenticationService, router, appConfigService, dialog, storageService);
this.ticketChangeBind = this.ticketChange.bind(this);
window.addEventListener('storage', this.ticketChangeBind);

View File

@@ -0,0 +1,60 @@
/*!
* @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 { HttpHeaders } from '@angular/common/http';
import ee from 'event-emitter';
import { Observable } from 'rxjs';
export interface AuthenticationServiceInterface {
onError: any;
onLogin: any;
onLogout: any;
on: ee.EmitterMethod;
off: ee.EmitterMethod;
once: ee.EmitterMethod;
emit: (type: string, ...args: any[]) => void;
getToken(): string;
isLoggedIn(): boolean;
isOauth(): boolean;
logout(): any;
isEcmLoggedIn(): boolean;
isBpmLoggedIn(): boolean;
isECMProvider(): boolean;
isBPMProvider(): boolean;
isALLProvider(): boolean;
getEcmUsername(): string;
getBpmUsername(): string;
getAuthHeaders(requestUrl: string, header: HttpHeaders): HttpHeaders;
addTokenToHeader(requestUrl: string, headersArg?: HttpHeaders): Observable<HttpHeaders>;
reset(): void;
}

View File

@@ -15,6 +15,14 @@
* limitations under the License.
*/
export * from './activiti/activiti-client.types';
export * from './alfresco-js-clients.module';
export * from './discovery/discovery-client.types';
export interface Authentication {
basicAuth?: BasicAuth;
cookie?: string;
type?: string;
}
export interface BasicAuth {
username?: string;
password?: string;
ticket?: string;
}

View File

@@ -19,17 +19,12 @@ import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
import { AuthConfig, AUTH_CONFIG, OAuthModule, OAuthService, OAuthStorage } from 'angular-oauth2-oidc';
import { AlfrescoApiNoAuthService } from '../../api-factories/alfresco-api-no-auth.service';
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';
@@ -51,10 +46,10 @@ export function loginFactory(oAuthService: OAuthService, storage: OAuthStorage,
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: AuthGuard, useClass: OidcAuthGuard },
// { provide: AuthGuardEcm, useClass: OidcAuthGuard },
// { provide: AuthGuardBpm, useClass: OidcAuthGuard },
{ provide: AuthenticationService},
{ provide: AlfrescoApiService, useClass: AlfrescoApiNoAuthService },
{
provide: AUTH_CONFIG,

View File

@@ -22,6 +22,8 @@ import { Observable } from 'rxjs';
* Provide authentication/authorization through OAuth2/OIDC protocol.
*/
export abstract class AuthService {
abstract onLogin: Observable<any>;
/** Subscribe to whether the user has valid Id/Access tokens. */
abstract authenticated$: Observable<boolean>;

View File

@@ -1,134 +0,0 @@
/*!
* @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 { Injectable, inject } from '@angular/core';
import { OAuthEvent, OAuthService, OAuthStorage } from 'angular-oauth2-oidc';
import { EMPTY, Observable } from 'rxjs';
import { catchError, filter, map } from 'rxjs/operators';
import { AppConfigValues } from '../../app-config/app-config.service';
import { BaseAuthenticationService } from '../services/base-authentication.service';
import { JwtHelperService } from '../services/jwt-helper.service';
import { AuthConfigService } from '../oidc/auth-config.service';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class OIDCAuthenticationService extends BaseAuthenticationService {
private authStorage = inject(OAuthStorage);
private oauthService = inject(OAuthService);
private readonly authConfig = inject(AuthConfigService);
private readonly auth = inject(AuthService);
readonly supportCodeFlow = true;
constructor() {
super();
this.alfrescoApi.alfrescoApiInitialized.subscribe(() => {
this.oauthService.events.pipe(
filter((event)=> event.type === 'token_received')
).subscribe(()=>{
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(): boolean {
return !!this.appConfig.oauth2?.implicitFlow;
}
isAuthCodeFlow(): boolean {
return !!this.appConfig.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))
);
}
getEcmUsername(): string {
return (this.oauthService.getIdentityClaims() as any).preferred_username;
}
getBpmUsername(): string {
return (this.oauthService.getIdentityClaims() as any).preferred_username;
}
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 = this.appConfig.oauth2;
if (config.oidc && oauth2.silentLogin) {
this.auth.login();
}
}
once(event: string): Observable<OAuthEvent> {
return this.oauthService.events.pipe(filter(_event => _event.type === event));
}
}

View File

@@ -19,13 +19,16 @@ 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 { distinctUntilChanged, filter, map, shareReplay } 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 {
onLogin: Observable<any>;
private _loadDiscoveryDocumentPromise = Promise.resolve(false);
/** Subscribe to whether the user has valid Id/Access tokens. */
@@ -52,29 +55,32 @@ export class RedirectAuthService extends AuthService {
) {
super();
this.authConfig = authConfig;
}
init() {
this.oauthService.clearHashAfterLogin = true;
this.authenticated$ = this.oauthService.events.pipe(
startWith(undefined),
map(() => this.authenticated),
distinctUntilChanged(),
shareReplay(1)
);
this.onLogin = this.authenticated$.pipe(
filter((authenticated) => authenticated),
map(() => undefined)
);
this.idpUnreachable$ = this.oauthService.events.pipe(
filter((event): event is OAuthErrorEvent => event.type === 'discovery_document_load_error'),
map((event) => event.reason as Error)
);
}
init() {
if (isPromise(this.authConfig)) {
return this.authConfig.then((config) => this.configureAuth(config));
}
return this.configureAuth(this.authConfig);
}
logout() {

View File

@@ -24,7 +24,7 @@ import { AuthService } from '../../auth.service';
const ROUTE_DEFAULT = '/';
@Component({
template: '',
template: '<div data-automation-id="auth-confirmation"></div>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AuthenticationConfirmationComponent {

View File

@@ -31,6 +31,10 @@ export * from './services/jwt-helper.service';
export * from './services/oauth2.service';
export * from './services/user-access.service';
export * from './basic-auth/basic-alfresco-auth.service';
export * from './basic-auth/process-auth';
export * from './basic-auth/content-auth';
export * from './interfaces/identity-user.service.interface';
export * from './interfaces/identity-group.interface';
export * from './interfaces/openid-configuration.interface';

View File

@@ -16,21 +16,23 @@
*/
import { fakeAsync, TestBed } from '@angular/core/testing';
import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { AuthenticationService } from './authentication.service';
import { CookieService } from '../../common/services/cookie.service';
import { AppConfigService } from '../../app-config/app-config.service';
import { setupTestBed } from '../../testing/setup-test-bed';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service';
import { OidcAuthenticationService } from './oidc-authentication.service';
declare let jasmine: any;
describe('AuthenticationService', () => {
let apiService: AlfrescoApiService;
let authService: AuthenticationService;
let basicAlfrescoAuthService: BasicAlfrescoAuthService;
let appConfigService: AppConfigService;
let cookie: CookieService;
let oidcAuthenticationService: OidcAuthenticationService;
setupTestBed({
imports: [
@@ -42,8 +44,9 @@ describe('AuthenticationService', () => {
beforeEach(() => {
sessionStorage.clear();
localStorage.clear();
apiService = TestBed.inject(AlfrescoApiService);
authService = TestBed.inject(AuthenticationService);
basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService);
oidcAuthenticationService = TestBed.inject(OidcAuthenticationService);
cookie = TestBed.inject(CookieService);
cookie.clear();
@@ -73,6 +76,23 @@ describe('AuthenticationService', () => {
});
appConfigService.load();
});
it('should kerberos be disabled if is oauth', () => {
spyOn(authService, 'isOauth').and.returnValue(true);
expect(authService.isKerberosEnabled()).toEqual(false);
});
it('should kerberos not enabled if is oauth is false and basic auth return false', () => {
spyOn(authService, 'isOauth').and.returnValue(false);
spyOn(basicAlfrescoAuthService, 'isKerberosEnabled').and.returnValue(false);
expect(authService.isKerberosEnabled()).toEqual(false);
});
it('should kerberos be enabled if is oauth is false and basic auth return true', () => {
spyOn(authService, 'isOauth').and.returnValue(false);
spyOn(basicAlfrescoAuthService, 'isKerberosEnabled').and.returnValue(true);
expect(authService.isKerberosEnabled()).toEqual(true);
});
});
describe('when the setting is ECM', () => {
@@ -83,40 +103,30 @@ describe('AuthenticationService', () => {
appConfigService.config.auth = { withCredentials: false };
appConfigService.config.providers = 'ECM';
appConfigService.load();
apiService.reset();
});
it('should not require cookie service enabled for ECM check', () => {
spyOn(cookie, 'isEnabled').and.returnValue(false);
spyOn(authService, 'isRememberMeSet').and.returnValue(false);
spyOn(basicAlfrescoAuthService, 'isRememberMeSet').and.returnValue(false);
spyOn(authService, 'isECMProvider').and.returnValue(true);
spyOn(authService, 'isOauth').and.returnValue(false);
spyOn(apiService, 'getInstance').and.callThrough();
expect(authService.isEcmLoggedIn()).toBeFalsy();
expect(apiService.getInstance).toHaveBeenCalled();
});
it('should check if loggedin on ECM in case the provider is ECM', () => {
spyOn(authService, 'isEcmLoggedIn').and.returnValue(true);
expect(authService.isLoggedInWith('ECM')).toBe(true);
});
it('should require remember me set for ECM check', () => {
spyOn(cookie, 'isEnabled').and.returnValue(true);
spyOn(authService, 'isRememberMeSet').and.returnValue(false);
spyOn(basicAlfrescoAuthService, 'isRememberMeSet').and.returnValue(false);
spyOn(authService, 'isECMProvider').and.returnValue(true);
spyOn(authService, 'isOauth').and.returnValue(false);
spyOn(apiService, 'getInstance').and.callThrough();
expect(authService.isEcmLoggedIn()).toBeFalsy();
expect(apiService.getInstance).not.toHaveBeenCalled();
});
it('[ECM] should return an ECM ticket after the login done', (done) => {
const disposableLogin = authService.login('fake-username', 'fake-password').subscribe(() => {
const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe(() => {
expect(authService.isLoggedIn()).toBe(true);
expect(authService.getTicketEcm()).toEqual('fake-post-ticket');
expect(authService.getToken()).toEqual('fake-post-ticket');
expect(authService.isEcmLoggedIn()).toBe(true);
disposableLogin.unsubscribe();
done();
@@ -130,7 +140,7 @@ describe('AuthenticationService', () => {
});
it('[ECM] should login in the ECM if no provider are defined calling the login', fakeAsync(() => {
const disposableLogin = authService.login('fake-username', 'fake-password').subscribe((loginResponse) => {
const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe((loginResponse) => {
expect(loginResponse).toEqual(fakeECMLoginResponse);
disposableLogin.unsubscribe();
});
@@ -143,10 +153,10 @@ describe('AuthenticationService', () => {
}));
it('[ECM] should return a ticket undefined after logout', fakeAsync(() => {
const disposableLogin = authService.login('fake-username', 'fake-password').subscribe(() => {
const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe(() => {
const disposableLogout = authService.logout().subscribe(() => {
expect(authService.isLoggedIn()).toBe(false);
expect(authService.getTicketEcm()).toBe(null);
expect(authService.getToken()).toBe(null);
expect(authService.isEcmLoggedIn()).toBe(false);
disposableLogin.unsubscribe();
disposableLogout.unsubscribe();
@@ -170,21 +180,21 @@ describe('AuthenticationService', () => {
});
it('[ECM] should set/get redirectUrl when provider is ECM', () => {
authService.setRedirect({ provider: 'ECM', url: 'some-url' });
basicAlfrescoAuthService.setRedirect({ provider: 'ECM', url: 'some-url' });
expect(authService.getRedirect()).toEqual('some-url');
expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url');
});
it('[ECM] should set/get redirectUrl when provider is BPM', () => {
authService.setRedirect({ provider: 'BPM', url: 'some-url' });
basicAlfrescoAuthService.setRedirect({ provider: 'BPM', url: 'some-url' });
expect(authService.getRedirect()).toBeNull();
expect(basicAlfrescoAuthService.getRedirect()).toBeNull();
});
it('[ECM] should return null as redirectUrl when redirectUrl field is not set', () => {
authService.setRedirect(null);
basicAlfrescoAuthService.setRedirect(null);
expect(authService.getRedirect()).toBeNull();
expect(basicAlfrescoAuthService.getRedirect()).toBeNull();
});
it('[ECM] should return isECMProvider true', () => {
@@ -209,40 +219,30 @@ describe('AuthenticationService', () => {
beforeEach(() => {
appConfigService.config.providers = 'BPM';
appConfigService.load();
apiService.reset();
});
it('should require remember me set for BPM check', () => {
spyOn(cookie, 'isEnabled').and.returnValue(true);
spyOn(authService, 'isRememberMeSet').and.returnValue(false);
spyOn(basicAlfrescoAuthService, 'isRememberMeSet').and.returnValue(false);
spyOn(authService, 'isBPMProvider').and.returnValue(true);
spyOn(authService, 'isOauth').and.returnValue(false);
spyOn(apiService, 'getInstance').and.callThrough();
expect(authService.isBpmLoggedIn()).toBeFalsy();
expect(apiService.getInstance).not.toHaveBeenCalled();
});
it('should check if loggedin on BPM in case the provider is BPM', () => {
spyOn(authService, 'isBpmLoggedIn').and.returnValue(true);
expect(authService.isLoggedInWith('BPM')).toBe(true);
});
it('should not require cookie service enabled for BPM check', () => {
spyOn(cookie, 'isEnabled').and.returnValue(false);
spyOn(authService, 'isRememberMeSet').and.returnValue(false);
spyOn(basicAlfrescoAuthService, 'isRememberMeSet').and.returnValue(false);
spyOn(authService, 'isBPMProvider').and.returnValue(true);
spyOn(apiService, 'getInstance').and.callThrough();
expect(authService.isBpmLoggedIn()).toBeFalsy();
expect(apiService.getInstance).toHaveBeenCalled();
});
it('[BPM] should return an BPM ticket after the login done', (done) => {
const disposableLogin = authService.login('fake-username', 'fake-password').subscribe(() => {
const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe(() => {
expect(authService.isLoggedIn()).toBe(true);
// cspell: disable-next
expect(authService.getTicketBpm()).toEqual('Basic ZmFrZS11c2VybmFtZTpmYWtlLXBhc3N3b3Jk');
expect(authService.getToken()).toEqual('Basic ZmFrZS11c2VybmFtZTpmYWtlLXBhc3N3b3Jk');
expect(authService.isBpmLoggedIn()).toBe(true);
disposableLogin.unsubscribe();
done();
@@ -255,10 +255,10 @@ describe('AuthenticationService', () => {
});
it('[BPM] should return a ticket undefined after logout', (done) => {
const disposableLogin = authService.login('fake-username', 'fake-password').subscribe(() => {
const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe(() => {
const disposableLogout = authService.logout().subscribe(() => {
expect(authService.isLoggedIn()).toBe(false);
expect(authService.getTicketBpm()).toBe(null);
expect(authService.getToken()).toBe(null);
expect(authService.isBpmLoggedIn()).toBe(false);
disposableLogout.unsubscribe();
disposableLogin.unsubscribe();
@@ -281,7 +281,7 @@ describe('AuthenticationService', () => {
},
(err: any) => {
expect(err).toBeDefined();
expect(authService.getTicketBpm()).toBe(undefined);
expect(authService.getToken()).toBe(null);
done();
});
@@ -291,21 +291,21 @@ describe('AuthenticationService', () => {
});
it('[BPM] should set/get redirectUrl when provider is BPM', () => {
authService.setRedirect({ provider: 'BPM', url: 'some-url' });
basicAlfrescoAuthService.setRedirect({ provider: 'BPM', url: 'some-url' });
expect(authService.getRedirect()).toEqual('some-url');
expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url');
});
it('[BPM] should set/get redirectUrl when provider is ECM', () => {
authService.setRedirect({ provider: 'ECM', url: 'some-url' });
basicAlfrescoAuthService.setRedirect({ provider: 'ECM', url: 'some-url' });
expect(authService.getRedirect()).toBeNull();
expect(basicAlfrescoAuthService.getRedirect()).toBeNull();
});
it('[BPM] should return null as redirectUrl when redirectUrl field is not set', () => {
authService.setRedirect(null);
basicAlfrescoAuthService.setRedirect(null);
expect(authService.getRedirect()).toBeNull();
expect(basicAlfrescoAuthService.getRedirect()).toBeNull();
});
it('[BPM] should return isECMProvider false', () => {
@@ -326,11 +326,10 @@ describe('AuthenticationService', () => {
beforeEach(() => {
appConfigService.config.providers = 'ECM';
appConfigService.load();
apiService.reset();
});
it('[ECM] should save the remember me cookie as a session cookie after successful login', (done) => {
const disposableLogin = authService.login('fake-username', 'fake-password', false).subscribe(() => {
const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password', false).subscribe(() => {
expect(cookie['ALFRESCO_REMEMBER_ME']).not.toBeUndefined();
expect(cookie['ALFRESCO_REMEMBER_ME'].expiration).toBeNull();
disposableLogin.unsubscribe();
@@ -345,7 +344,7 @@ describe('AuthenticationService', () => {
});
it('[ECM] should save the remember me cookie as a persistent cookie after successful login', (done) => {
const disposableLogin = authService.login('fake-username', 'fake-password', true).subscribe(() => {
const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password', true).subscribe(() => {
expect(cookie['ALFRESCO_REMEMBER_ME']).not.toBeUndefined();
expect(cookie['ALFRESCO_REMEMBER_ME'].expiration).not.toBeNull();
disposableLogin.unsubscribe();
@@ -361,7 +360,7 @@ describe('AuthenticationService', () => {
});
it('[ECM] should not save the remember me cookie after failed login', (done) => {
const disposableLogin = authService.login('fake-username', 'fake-password').subscribe(
const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe(
() => {},
() => {
expect(cookie['ALFRESCO_REMEMBER_ME']).toBeUndefined();
@@ -390,15 +389,14 @@ describe('AuthenticationService', () => {
beforeEach(() => {
appConfigService.config.providers = 'ALL';
appConfigService.load();
apiService.reset();
});
it('[ALL] should return both ECM and BPM tickets after the login done', (done) => {
const disposableLogin = authService.login('fake-username', 'fake-password').subscribe(() => {
const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe(() => {
expect(authService.isLoggedIn()).toBe(true);
expect(authService.getTicketEcm()).toEqual('fake-post-ticket');
expect(basicAlfrescoAuthService.getTicketEcm()).toEqual('fake-post-ticket');
// cspell: disable-next
expect(authService.getTicketBpm()).toEqual('Basic ZmFrZS11c2VybmFtZTpmYWtlLXBhc3N3b3Jk');
expect(basicAlfrescoAuthService.getTicketBpm()).toEqual('Basic ZmFrZS11c2VybmFtZTpmYWtlLXBhc3N3b3Jk');
expect(authService.isBpmLoggedIn()).toBe(true);
expect(authService.isEcmLoggedIn()).toBe(true);
disposableLogin.unsubscribe();
@@ -417,13 +415,13 @@ describe('AuthenticationService', () => {
});
it('[ALL] should return login fail if only ECM call fail', (done) => {
const disposableLogin = authService.login('fake-username', 'fake-password').subscribe(
const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe(
() => {},
() => {
expect(authService.isLoggedIn()).toBe(false, 'isLoggedIn');
expect(authService.getTicketEcm()).toBe(null, 'getTicketEcm');
expect(authService.getToken()).toBe(null, 'getTicketEcm');
// cspell: disable-next
expect(authService.getTicketBpm()).toBe(null, 'getTicketBpm');
expect(authService.getToken()).toBe(null, 'getTicketBpm');
expect(authService.isEcmLoggedIn()).toBe(false, 'isEcmLoggedIn');
disposableLogin.unsubscribe();
done();
@@ -439,12 +437,12 @@ describe('AuthenticationService', () => {
});
it('[ALL] should return login fail if only BPM call fail', (done) => {
const disposableLogin = authService.login('fake-username', 'fake-password').subscribe(
const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe(
() => {},
() => {
expect(authService.isLoggedIn()).toBe(false);
expect(authService.getTicketEcm()).toBe(null);
expect(authService.getTicketBpm()).toBe(null);
expect(authService.getToken()).toBe(null);
expect(authService.getToken()).toBe(null);
expect(authService.isBpmLoggedIn()).toBe(false);
disposableLogin.unsubscribe();
done();
@@ -462,12 +460,12 @@ describe('AuthenticationService', () => {
});
it('[ALL] should return ticket undefined when the credentials are wrong', (done) => {
const disposableLogin = authService.login('fake-username', 'fake-password').subscribe(
const disposableLogin = basicAlfrescoAuthService.login('fake-username', 'fake-password').subscribe(
() => {},
() => {
expect(authService.isLoggedIn()).toBe(false);
expect(authService.getTicketEcm()).toBe(null);
expect(authService.getTicketBpm()).toBe(null);
expect(authService.getToken()).toBe(null);
expect(authService.getToken()).toBe(null);
expect(authService.isBpmLoggedIn()).toBe(false);
expect(authService.isEcmLoggedIn()).toBe(false);
disposableLogin.unsubscribe();
@@ -483,30 +481,6 @@ describe('AuthenticationService', () => {
});
});
it('[ALL] should set/get redirectUrl when provider is ALL', () => {
authService.setRedirect({ provider: 'ALL', url: 'some-url' });
expect(authService.getRedirect()).toEqual('some-url');
});
it('[ALL] should set/get redirectUrl when provider is BPM', () => {
authService.setRedirect({ provider: 'BPM', url: 'some-url' });
expect(authService.getRedirect()).toEqual('some-url');
});
it('[ALL] should set/get redirectUrl when provider is ECM', () => {
authService.setRedirect({ provider: 'ECM', url: 'some-url' });
expect(authService.getRedirect()).toEqual('some-url');
});
it('[ALL] should return null as redirectUrl when redirectUrl field is not set', () => {
authService.setRedirect(null);
expect(authService.getRedirect()).toBeNull();
});
it('[ALL] should return isECMProvider false', () => {
expect(authService.isECMProvider()).toBe(false);
});
@@ -519,4 +493,22 @@ describe('AuthenticationService', () => {
expect(authService.isALLProvider()).toBe(true);
});
});
describe('getUsername', () => {
it('should get the username of the authenticated user if isOAuth is true', () => {
spyOn(authService, 'isOauth').and.returnValue(true);
spyOn(oidcAuthenticationService, 'getUsername').and.returnValue('mike.portnoy');
const username = authService.getUsername();
expect(username).toEqual('mike.portnoy');
});
it('should get the username of the authenticated user if isOAuth is false', () => {
spyOn(authService, 'isOauth').and.returnValue(false);
spyOn(oidcAuthenticationService, 'getUsername').and.returnValue('mike.portnoy');
spyOn(basicAlfrescoAuthService, 'getUsername').and.returnValue('john.petrucci');
const username = authService.getUsername();
expect(username).toEqual('john.petrucci');
});
});
});

View File

@@ -15,184 +15,195 @@
* limitations under the License.
*/
import { Injectable, inject } from '@angular/core';
import { Observable, from } from 'rxjs';
import { AppConfigValues } from '../../app-config/app-config.service';
import { map, catchError, tap } from 'rxjs/operators';
import { JwtHelperService } from './jwt-helper.service';
import { StorageService } from '../../common/services/storage.service';
import { BaseAuthenticationService } from './base-authentication.service';
import { Injectable, Injector } from '@angular/core';
import { OidcAuthenticationService } from './oidc-authentication.service';
import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service';
import { Observable, Subject, from } from 'rxjs';
import { HttpHeaders } from '@angular/common/http';
import { AuthenticationServiceInterface } from '../interfaces/authentication-service.interface';
import ee from 'event-emitter';
import { RedirectAuthService } from '../oidc/redirect-auth.service';
@Injectable({
providedIn: 'root'
})
export class AuthenticationService extends BaseAuthenticationService {
private storageService = inject(StorageService);
readonly supportCodeFlow = false;
export class AuthenticationService implements AuthenticationServiceInterface, ee.Emitter {
constructor() {
super();
this.alfrescoApi.alfrescoApiInitialized.subscribe(() => {
this.alfrescoApi.getInstance().reply('logged-in', () => {
this.onLogin.next();
});
});
}
onLogin: Subject<any> = new Subject<any>();
onLogout: Subject<any> = new Subject<any>();
/**
* Checks if the user logged in.
*
* @returns True if logged in, false otherwise
*/
isLoggedIn(): boolean {
if (!this.isOauth() && this.cookie.isEnabled() && !this.isRememberMeSet()) {
return false;
}
return this.alfrescoApi.getInstance().isLoggedIn();
}
constructor(
private injector: Injector,
private redirectAuthService: RedirectAuthService
) {
this.redirectAuthService.onLogin.subscribe(
(value) => this.onLogin.next(value)
);
isLoggedInWith(provider: string): boolean {
if (provider === 'BPM') {
return this.isBpmLoggedIn();
} else if (provider === 'ECM') {
return this.isEcmLoggedIn();
this.basicAlfrescoAuthService.onLogin.subscribe(
(value) => this.onLogin.next(value)
);
if (this.isOauth()) {
this.oidcAuthenticationService.onLogout.subscribe(
(value) => this.onLogout.next(value)
);
} else {
return this.isLoggedIn();
this.basicAlfrescoAuthService.onLogout.subscribe(
(value) => this.onLogout.next(value)
);
}
}
/**
* Does the provider support OAuth?
*
* @returns True if supported, false otherwise
*/
isOauth(): boolean {
return this.alfrescoApi.getInstance().isOauthConfiguration();
get on(): ee.EmitterMethod {
return this.isOauth() ? this.oidcAuthenticationService.on : this.basicAlfrescoAuthService.on;
}
/**
* Logs the user in.
*
* @param username Username for the login
* @param password Password for the login
* @param rememberMe Stores the user's login details if true
* @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 }> {
return from(this.alfrescoApi.getInstance().login(username, password)).pipe(
map((response: any) => {
this.saveRememberMeCookie(rememberMe);
this.onLogin.next(response);
return {
type: this.appConfig.get(AppConfigValues.PROVIDERS),
ticket: response
};
}),
catchError((err) => this.handleError(err))
);
get off(): ee.EmitterMethod {
return this.isOauth() ? this.oidcAuthenticationService.off : this.basicAlfrescoAuthService.off;
}
/**
* Logs the user in with SSO
*/
ssoImplicitLogin() {
this.alfrescoApi.getInstance().implicitLogin();
get once(): ee.EmitterMethod {
return this.isOauth() ? this.oidcAuthenticationService.once : this.basicAlfrescoAuthService.once;
}
/**
* Logs the user out.
*
* @returns Response event called when logout is complete
*/
logout() {
return from(this.callApiLogout()).pipe(
tap((response) => {
this.onLogout.next(response);
return response;
}),
catchError((err) => this.handleError(err))
);
get emit(): (type: string, ...args: any[]) => void {
return this.isOauth() ? this.oidcAuthenticationService.emit : this.basicAlfrescoAuthService.emit;
}
private callApiLogout(): Promise<any> {
if (this.alfrescoApi.getInstance()) {
return this.alfrescoApi.getInstance().logout();
get onError(): Observable<any> {
return this.isOauth() ? this.oidcAuthenticationService.onError : this.basicAlfrescoAuthService.onError;
}
addTokenToHeader(requestUrl: string, headersArg?: HttpHeaders): Observable<HttpHeaders> {
return this.isOauth() ? this.oidcAuthenticationService.addTokenToHeader(requestUrl, headersArg) : this.basicAlfrescoAuthService.addTokenToHeader(requestUrl, headersArg);
}
isECMProvider(): boolean {
return this.isOauth() ? this.oidcAuthenticationService.isECMProvider() : this.basicAlfrescoAuthService.isECMProvider();
}
isBPMProvider(): boolean {
return this.isOauth() ? this.oidcAuthenticationService.isBPMProvider() : this.basicAlfrescoAuthService.isBPMProvider();
}
isALLProvider(): boolean {
return this.isOauth() ? this.oidcAuthenticationService.isALLProvider() : this.basicAlfrescoAuthService.isALLProvider();
}
private get oidcAuthenticationService(): OidcAuthenticationService {
return this.injector.get(OidcAuthenticationService);
}
private get basicAlfrescoAuthService(): BasicAlfrescoAuthService {
return this.injector.get(BasicAlfrescoAuthService);
}
getToken(): string {
if (this.isOauth()) {
return this.oidcAuthenticationService.getToken();
} else {
return this.basicAlfrescoAuthService.getToken();
}
}
isLoggedIn(): boolean {
if (this.isOauth()) {
return this.oidcAuthenticationService.isLoggedIn();
} else {
return this.basicAlfrescoAuthService.isLoggedIn();
}
}
logout(): Observable<any> {
if (this.isOauth()) {
return this.oidcAuthenticationService.logout();
} else {
return from(this.basicAlfrescoAuthService.logout());
}
return Promise.resolve();
}
/**
* Checks if the user is logged in on an ECM provider.
*
* @returns True if logged in, false otherwise
*/
isEcmLoggedIn(): boolean {
if (this.isECMProvider() || this.isALLProvider()) {
if (!this.isOauth() && this.cookie.isEnabled() && !this.isRememberMeSet()) {
return false;
}
return this.alfrescoApi.getInstance().isEcmLoggedIn();
if (this.isOauth()) {
return this.oidcAuthenticationService.isEcmLoggedIn();
} else {
return this.basicAlfrescoAuthService.isEcmLoggedIn();
}
return false;
}
/**
* Checks if the user is logged in on a BPM provider.
*
* @returns True if logged in, false otherwise
*/
isBpmLoggedIn(): boolean {
if (this.isBPMProvider() || this.isALLProvider()) {
if (!this.isOauth() && this.cookie.isEnabled() && !this.isRememberMeSet()) {
return false;
}
return this.alfrescoApi.getInstance().isBpmLoggedIn();
if (this.isOauth()) {
return this.oidcAuthenticationService.isBpmLoggedIn();
} else {
return this.basicAlfrescoAuthService.isBpmLoggedIn();
}
}
reset(): void {
if (this.isOauth()) {
return this.oidcAuthenticationService.reset();
} else {
return this.basicAlfrescoAuthService.reset();
}
}
login(username: string, password: string, rememberMe?: boolean): Observable<{ type: string; ticket: any }> {
if (this.isOauth()) {
return this.oidcAuthenticationService.loginWithPassword(username, password);
} else {
return this.basicAlfrescoAuthService.login(username, password, rememberMe);
}
return false;
}
/**
* Gets the ECM username.
*
* @returns The ECM username
* @returns the username of the authenticated user
*/
getUsername(): string {
if (this.isOauth()) {
return this.oidcAuthenticationService.getUsername();
} else {
return this.basicAlfrescoAuthService.getUsername();
}
}
/**
* @deprecated
* @returns the logged username
*/
getEcmUsername(): string {
return this.alfrescoApi.getInstance().getEcmUsername();
if (this.isOauth()) {
return this.oidcAuthenticationService.getUsername();
} else {
return this.basicAlfrescoAuthService.getEcmUsername();
}
}
/**
* Gets the BPM username
*
* @returns The BPM username
* @deprecated
* @returns the logged username
*/
getBpmUsername(): string {
return this.alfrescoApi.getInstance().getBpmUsername();
if (this.isOauth()) {
return this.oidcAuthenticationService.getUsername();
} else {
return this.basicAlfrescoAuthService.getBpmUsername();
}
}
isImplicitFlow(): boolean {
return !!this.appConfig.oauth2?.implicitFlow;
getAuthHeaders(requestUrl: string, headers: HttpHeaders): HttpHeaders {
if (this.isOauth()) {
return this.oidcAuthenticationService.getAuthHeaders(requestUrl, headers);
} else {
return this.basicAlfrescoAuthService.getAuthHeaders(requestUrl, headers);
}
}
isAuthCodeFlow(): boolean {
return false;
isOauth(): boolean {
return this.basicAlfrescoAuthService.isOauth();
}
/**
* Gets the auth token.
*
* @returns Auth token string
*/
getToken(): string {
return this.storageService.getItem(JwtHelperService.USER_ACCESS_TOKEN);
isKerberosEnabled(): boolean {
return !this.isOauth() ? this.basicAlfrescoAuthService.isKerberosEnabled() : false;
}
reset() { }
once(event: string): Observable<any> {
const alfrescoApiEvent = event === 'token_received' ? 'token_issued' : event;
return new Observable((subscriber) => {
this.alfrescoApi.getInstance().oauth2Auth.once(alfrescoApiEvent, () => subscriber.next());
});
}
}

View File

@@ -15,76 +15,70 @@
* limitations under the License.
*/
import { PeopleApi, UserProfileApi } from '@alfresco/js-api';
import { HttpHeaders } from '@angular/common/http';
import { RedirectionModel } from '../models/redirection.model';
import { Observable, Observer, ReplaySubject, throwError } from 'rxjs';
import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service';
import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { CookieService } from '../../common/services/cookie.service';
import { LogService } from '../../common/services/log.service';
import { inject } from '@angular/core';
import { AuthenticationServiceInterface } from '../interfaces/authentication-service.interface';
import ee from 'event-emitter';
const REMEMBER_ME_COOKIE_KEY = 'ALFRESCO_REMEMBER_ME';
const REMEMBER_ME_UNTIL = 1000 * 60 * 60 * 24 * 30;
export abstract class BaseAuthenticationService implements AuthenticationServiceInterface, ee.Emitter {
export abstract class BaseAuthenticationService {
protected alfrescoApi = inject(AlfrescoApiService);
protected appConfig = inject(AppConfigService);
protected cookie = inject(CookieService);
private logService = inject(LogService);
on: ee.EmitterMethod;
off: ee.EmitterMethod;
once: ee.EmitterMethod;
emit: (type: string, ...args: any[]) => void;
protected bearerExcludedUrls: readonly string[] = ['resources/', 'assets/', 'auth/realms', 'idp/'];
protected redirectUrl: RedirectionModel = null;
onError = new ReplaySubject<any>(1);
onLogin = new ReplaySubject<any>(1);
onLogout = new ReplaySubject<any>(1);
private _peopleApi: PeopleApi;
get peopleApi(): PeopleApi {
this._peopleApi = this._peopleApi ?? new PeopleApi(this.alfrescoApi.getInstance());
return this._peopleApi;
constructor(
protected appConfig: AppConfigService,
protected cookie: CookieService,
private logService: LogService
) {
ee(this);
}
private _profileApi: UserProfileApi;
get profileApi(): UserProfileApi {
this._profileApi = this._profileApi ?? new UserProfileApi(this.alfrescoApi.getInstance());
return this._profileApi;
}
abstract getAuthHeaders(requestUrl: string, header: HttpHeaders): HttpHeaders;
abstract readonly supportCodeFlow: boolean;
abstract getToken(): string;
abstract isLoggedIn(): boolean;
abstract isLoggedInWith(provider: string): boolean;
abstract isOauth(): 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 getEcmUsername(): string;
abstract getBpmUsername(): string;
abstract reset(): void;
abstract once(event: string): Observable<any>;
getBearerExcludedUrls(): readonly string[] {
return this.bearerExcludedUrls;
}
abstract isLoggedIn(): boolean;
abstract logout(): any;
abstract isEcmLoggedIn(): boolean;
abstract isBpmLoggedIn(): boolean;
abstract reset(): void;
abstract getEcmUsername(): string;
abstract getBpmUsername(): string;
/**
* Adds the auth token to an HTTP header using the 'bearer' scheme.
*
* @param requestUrl the request url
* @param headersArg Header that will receive the token
* @returns The new header with the token added
*/
addTokenToHeader(headersArg?: HttpHeaders): Observable<HttpHeaders> {
addTokenToHeader(requestUrl: string, headersArg?: HttpHeaders): Observable<HttpHeaders> {
return new Observable((observer: Observer<any>) => {
let headers = headersArg;
if (!headers) {
headers = new HttpHeaders();
}
try {
const header = this.getAuthHeaders(headers);
const header = this.getAuthHeaders(requestUrl, headers);
observer.next(header);
observer.complete();
@@ -94,50 +88,9 @@ export abstract class BaseAuthenticationService {
});
}
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);
}
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();
const provider = this.appConfig.get('providers') as string;
return provider && provider.toUpperCase() === 'ECM';
}
/**
@@ -146,7 +99,12 @@ export abstract class BaseAuthenticationService {
* @returns True if supported, false otherwise
*/
isBPMProvider(): boolean {
return this.alfrescoApi.getInstance().isBpmConfiguration();
const provider = this.appConfig.get('providers');
if (provider && (typeof provider === 'string' || provider instanceof String)) {
return provider.toUpperCase() === 'BPM';
} else {
return false;
}
}
/**
@@ -155,38 +113,13 @@ export abstract class BaseAuthenticationService {
* @returns True if both are supported, false otherwise
*/
isALLProvider(): boolean {
return this.alfrescoApi.getInstance().isEcmBpmConfiguration();
const provider = this.appConfig.get('providers') as string;
return provider && provider.toUpperCase() === 'ALL';
}
/**
* 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;
isOauthConfiguration(): boolean {
const authType = this.appConfig.get('authType') as string;
return authType === 'OAUTH';
}
/**
@@ -196,64 +129,12 @@ export abstract class BaseAuthenticationService {
* @returns Object representing the error message
*/
handleError(error: any): Observable<any> {
this.onError.next(error || 'Server error');
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');
isOauth(): boolean {
return this.appConfig.get(AppConfigValues.AUTHTYPE) === 'OAUTH';
}
}

View File

@@ -35,6 +35,7 @@ import { IdentityRoleModel } from '../models/identity-role.model';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { AdfHttpClient } from '../../../../api/src';
import { StorageService } from '../../common/services/storage.service';
describe('IdentityUserService', () => {
@@ -46,6 +47,7 @@ describe('IdentityUserService', () => {
{ id: 'id-5', name: 'MOCK-ROLE-2'}
];
let storageService: StorageService;
let service: IdentityUserService;
let adfHttpClient: AdfHttpClient;
let requestSpy: jasmine.Spy;
@@ -57,18 +59,14 @@ describe('IdentityUserService', () => {
CoreTestingModule
]
});
storageService = TestBed.inject(StorageService);
service = TestBed.inject(IdentityUserService);
adfHttpClient = TestBed.inject(AdfHttpClient);
requestSpy = spyOn(adfHttpClient, 'request');
const store = {};
spyOn(localStorage, 'getItem').and.callFake( (key: string): string => store[key] || null);
spyOn(localStorage, 'setItem').and.callFake((key: string, value: string): string => store[key] = value);
});
it('should fetch identity user info from Jwt id token', () => {
localStorage.setItem(JwtHelperService.USER_ID_TOKEN, mockToken);
storageService.setItem(JwtHelperService.USER_ID_TOKEN, mockToken);
const user = service.getCurrentUserInfo();
expect(user).toBeDefined();
expect(user.firstName).toEqual('John');
@@ -78,7 +76,7 @@ describe('IdentityUserService', () => {
});
it('should fallback on Jwt access token for identity user info', () => {
localStorage.setItem(JwtHelperService.USER_ACCESS_TOKEN, mockToken);
storageService.setItem(JwtHelperService.USER_ACCESS_TOKEN, mockToken);
const user = service.getCurrentUserInfo();
expect(user).toBeDefined();
expect(user.firstName).toEqual('John');

View File

@@ -0,0 +1,194 @@
/*!
* @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 { Injectable } from '@angular/core';
import { OAuthService, OAuthStorage } from 'angular-oauth2-oidc';
import { EMPTY, Observable, defer } 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 { BaseAuthenticationService } from './base-authentication.service';
import { CookieService } from '../../common/services/cookie.service';
import { JwtHelperService } from './jwt-helper.service';
import { LogService } from '../../common/services/log.service';
import { AuthConfigService } from '../oidc/auth-config.service';
import { AuthService } from '../oidc/auth.service';
import { Minimatch } from 'minimatch';
import { HttpHeaders } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class OidcAuthenticationService extends BaseAuthenticationService {
constructor(
appConfig: AppConfigService,
cookie: CookieService,
logService: LogService,
private jwtHelperService: JwtHelperService,
private authStorage: OAuthStorage,
private oauthService: OAuthService,
private readonly authConfig: AuthConfigService,
private readonly auth: AuthService
) {
super(appConfig, cookie, logService);
}
isEcmLoggedIn(): boolean {
if (this.isECMProvider() || this.isALLProvider()) {
return this.isLoggedIn();
}
return false;
}
isBpmLoggedIn(): boolean {
if (this.isBPMProvider() || this.isALLProvider()) {
return this.isLoggedIn();
}
return false;
}
isLoggedIn(): boolean {
return this.oauthService.hasValidAccessToken() && this.oauthService.hasValidIdToken();
}
hasValidAccessToken(): boolean {
return this.oauthService.hasValidAccessToken();
}
hasValidIdToken(): boolean {
return this.oauthService.hasValidIdToken();
}
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): Observable<{ type: string; ticket: any }> {
return this.auth.baseAuthLogin(username, password).pipe(
map((response) => {
this.onLogin.next(response);
return {
type: this.appConfig.get(AppConfigValues.PROVIDERS),
ticket: response
};
}),
catchError((err) => this.handleError(err))
);
}
loginWithPassword(username: string, password: string): Observable<{ type: string; ticket: any }> {
return defer(async () => {
try {
await this.authConfig.loadConfig();
await this.oauthService.loadDiscoveryDocument();
await this.oauthService.fetchTokenUsingPasswordFlowAndLoadUserProfile(username, password);
await this.oauthService.refreshToken();
const accessToken = this.oauthService.getAccessToken();
this.onLogin.next(accessToken);
return {
type: this.appConfig.get(AppConfigValues.PROVIDERS) as string,
ticket: accessToken
};
} catch (err) {
throw this.handleError(err);
}
});
}
getUsername(){
return this.jwtHelperService.getValueFromLocalToken<string>(JwtHelperService.USER_PREFERRED_USERNAME);
}
/**
* @deprecated
* @returns the logged username
*/
getEcmUsername(): string {
return this.getUsername();
}
/**
* @deprecated
* @returns the logged username
*/
getBpmUsername(): string {
return this.getUsername();
}
ssoImplicitLogin() {
this.auth.login();
}
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);
}
isPublicUrl(): boolean {
const oauth2 = this.appConfig.get<OauthConfigModel>(AppConfigValues.OAUTHCONFIG, null);
if (Array.isArray(oauth2.publicUrls)) {
return oauth2.publicUrls.length > 0 &&
oauth2.publicUrls.some((urlPattern: string) => {
const minimatch = new Minimatch(urlPattern);
return minimatch.match(window.location.href);
});
}
return false;
}
getAuthHeaders(_requestUrl: string, header: HttpHeaders): HttpHeaders {
return this.addBearerToken(header);
}
private addBearerToken(header: HttpHeaders): HttpHeaders {
const token: string = this.getToken();
if (!token) {
return header;
}
return header.set('Authorization', 'bearer ' + token);
}
}

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { CardViewKeyValuePairsItemModel } from '../../models/card-view-keyvaluepairs.model';
import { CardViewKeyValuePairsItemComponent } from './card-view-keyvaluepairsitem.component';
@@ -122,57 +122,5 @@ describe('CardViewKeyValuePairsItemComponent', () => {
expect(cardViewUpdateService.update).toHaveBeenCalled();
expect(component.property.value.length).toBe(0);
});
it('should update property on input blur', waitForAsync(() => {
spyOn(cardViewUpdateService, 'update');
component.ngOnChanges();
fixture.detectChanges();
const addButton = fixture.debugElement.query(By.css('.adf-card-view__key-value-pairs__add-btn'));
addButton.triggerEventHandler('click', null);
fixture.detectChanges();
const nameInput = fixture.debugElement.query(By.css(`[data-automation-id="card-${component.property.key}-name-input-0"]`));
const valueInput = fixture.debugElement.query(By.css(`[data-automation-id="card-${component.property.key}-value-input-0"]`));
nameInput.nativeElement.value = mockData[0].name;
nameInput.nativeElement.dispatchEvent(new Event('input'));
valueInput.nativeElement.value = mockData[0].value;
valueInput.nativeElement.dispatchEvent(new Event('input'));
fixture.whenStable().then(() => {
fixture.detectChanges();
valueInput.triggerEventHandler('blur', null);
fixture.detectChanges();
expect(cardViewUpdateService.update).toHaveBeenCalled();
expect(JSON.stringify(component.property.value)).toBe(JSON.stringify(mockData));
});
}));
it('should not update property if at least one input is empty on blur', waitForAsync(() => {
spyOn(cardViewUpdateService, 'update');
component.ngOnChanges();
fixture.detectChanges();
const addButton = fixture.debugElement.query(By.css('.adf-card-view__key-value-pairs__add-btn'));
addButton.triggerEventHandler('click', null);
fixture.detectChanges();
const valueInput = fixture.debugElement.query(By.css(`[data-automation-id="card-${component.property.key}-value-input-0"]`));
valueInput.nativeElement.value = mockData[0].value;
valueInput.nativeElement.dispatchEvent(new Event('input'));
fixture.whenStable().then(() => {
fixture.detectChanges();
valueInput.triggerEventHandler('blur', null);
fixture.detectChanges();
expect(cardViewUpdateService.update).not.toHaveBeenCalled();
});
}));
});
});

View File

@@ -19,12 +19,14 @@ import { Injectable } from '@angular/core';
import { AppConfigService, Status } from '../../app-config/app-config.service';
import { HttpClient } from '@angular/common/http';
import { ExtensionService } from '@alfresco/adf-extensions';
@Injectable()
export class AppConfigServiceMock extends AppConfigService {
config: any = {
application: {
name: 'Alfresco ADF Application'
name: 'Alfresco ADF Application',
storagePrefix: 'ADF_APP'
},
ecmHost: 'http://{hostname}{:port}/ecm',
bpmHost: 'http://{hostname}{:port}/bpm',
@@ -35,11 +37,12 @@ export class AppConfigServiceMock extends AppConfigService {
super(http, extensionService);
}
load(): Promise<any> {
load(callback?: (...args: any[]) => any): Promise<any> {
return new Promise((resolve) => {
this.status = Status.LOADED;
this.onDataLoaded(this.config);
callback?.();
resolve(this.config);
this.onDataLoaded();
});
}
}

View File

@@ -21,7 +21,6 @@ import { StorageService } from '../../common/services/storage.service';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { AppConfigServiceMock } from '../mock/app-config.service.mock';
import { TranslateModule } from '@ngx-translate/core';
import { CoreModule } from '../../core.module';
describe('StorageService', () => {
@@ -34,8 +33,6 @@ describe('StorageService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
CoreModule.forRoot(),
CoreTestingModule
]
});
@@ -87,6 +84,7 @@ describe('StorageService', () => {
]
});
appConfig = TestBed.inject(AppConfigService);
appConfig.config = {
application: {
storagePrefix: ''

View File

@@ -81,7 +81,7 @@ export class StorageService {
*/
removeItem(key: string) {
if (this.useLocalStorage) {
localStorage.removeItem(this.prefix + key);
localStorage.removeItem(`${this.prefix}` + key);
} else {
delete this.memoryStore[this.prefix + key];
}

View File

@@ -26,7 +26,7 @@ export class ObjectUtils {
*/
static getValue(target: any, key: string): any {
if (!target) {
if (!target || !key) {
return undefined;
}

View File

@@ -53,9 +53,8 @@ import { ExtensionsModule } from '@alfresco/adf-extensions';
import { directionalityConfigFactory } from './common/services/directionality-config-factory';
import { DirectionalityConfigService } from './common/services/directionality-config.service';
import { SearchTextModule } from './search-text/search-text-input.module';
import { AdfHttpClient, AlfrescoJsClientsModule } from '@alfresco/adf-core/api';
import { AdfHttpClient } from '@alfresco/adf-core/api';
import { AuthenticationInterceptor, Authentication } from '@alfresco/adf-core/auth';
import { LegacyApiClientModule } from './api-factories/legacy-api-client.module';
import { HttpClientModule, HttpClientXsrfModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthenticationService } from './auth/services/authentication.service';
import { MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar';
@@ -101,8 +100,6 @@ import { AdfDateTimeFnsAdapter } from './common/utils/datetime-fns-adapter';
NotificationHistoryModule,
SearchTextModule,
BlankPageModule,
LegacyApiClientModule,
AlfrescoJsClientsModule,
HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: 'CSRF-TOKEN',

View File

@@ -37,7 +37,7 @@ export class LogoutDirective implements OnInit {
private renderer: Renderer2,
private router: Router,
private appConfig: AppConfigService,
private auth: AuthenticationService) {
private authenticationService: AuthenticationService) {
}
ngOnInit() {
@@ -58,14 +58,14 @@ export class LogoutDirective implements OnInit {
}
logout() {
this.auth.logout().subscribe(
this.authenticationService.logout().subscribe(
() => this.redirectToUri(),
() => this.redirectToUri()
);
}
redirectToUri() {
if (this.enableRedirect && !this.auth.isOauth()) {
if (this.enableRedirect && !this.authenticationService.isOauth()) {
const redirectRoute = this.getRedirectUri();
this.router.navigate([redirectRoute]);
}

View File

@@ -16,11 +16,12 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AuthenticationService } from '../../auth/services/authentication.service';
import { LoginDialogPanelComponent } from './login-dialog-panel.component';
import { of } from 'rxjs';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { BasicAlfrescoAuthService } from '../../auth/basic-auth/basic-alfresco-auth.service';
import { OidcAuthenticationService } from '../../auth/services/oidc-authentication.service';
describe('LoginDialogPanelComponent', () => {
let component: LoginDialogPanelComponent;
@@ -28,19 +29,23 @@ describe('LoginDialogPanelComponent', () => {
let element: HTMLElement;
let usernameInput: HTMLInputElement;
let passwordInput: HTMLInputElement;
let authService: AuthenticationService;
let basicAlfrescoAuthService: BasicAlfrescoAuthService;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
providers: [
{ provide: OidcAuthenticationService, useValue: {}}
]
});
fixture = TestBed.createComponent(LoginDialogPanelComponent);
basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService);
element = fixture.nativeElement;
component = fixture.componentInstance;
authService = TestBed.inject(AuthenticationService);
fixture.detectChanges();
await fixture.whenStable();
@@ -76,7 +81,7 @@ describe('LoginDialogPanelComponent', () => {
expect(event.token.ticket).toBe('ticket');
done();
});
spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' }));
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' }));
loginWithCredentials('fake-username', 'fake-password');
});

View File

@@ -25,10 +25,11 @@ import { AuthenticationService } from '../../auth/services/authentication.servic
import { LoginErrorEvent } from '../models/login-error.event';
import { LoginSuccessEvent } from '../models/login-success.event';
import { LoginComponent } from './login.component';
import { of, throwError } from 'rxjs';
import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { EMPTY, of, throwError } from 'rxjs';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { LogService } from '../../common/services/log.service';
import { BasicAlfrescoAuthService } from '../../auth/basic-auth/basic-alfresco-auth.service';
import { OidcAuthenticationService } from '../../auth/services/oidc-authentication.service';
describe('LoginComponent', () => {
let component: LoginComponent;
@@ -38,7 +39,7 @@ describe('LoginComponent', () => {
let router: Router;
let userPreferences: UserPreferencesService;
let appConfigService: AppConfigService;
let alfrescoApiService: AlfrescoApiService;
let basicAlfrescoAuthService: BasicAlfrescoAuthService;
let usernameInput;
let passwordInput;
@@ -60,6 +61,16 @@ describe('LoginComponent', () => {
TestBed.configureTestingModule({
imports: [
CoreTestingModule
],
providers: [
{
provide: OidcAuthenticationService, useValue: {
ssoImplicitLogin: () => { },
isPublicUrl: () => false,
hasValidIdToken: () => false,
isLoggedIn: () => false
}
}
]
});
fixture = TestBed.createComponent(LoginComponent);
@@ -69,11 +80,11 @@ describe('LoginComponent', () => {
component.showRememberMe = true;
component.showLoginActions = true;
basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService);
authService = TestBed.inject(AuthenticationService);
router = TestBed.inject(Router);
userPreferences = TestBed.inject(UserPreferencesService);
appConfigService = TestBed.inject(AppConfigService);
alfrescoApiService = TestBed.inject(AlfrescoApiService);
const logService = TestBed.inject(LogService);
spyOn(logService, 'error');
@@ -111,7 +122,7 @@ describe('LoginComponent', () => {
});
it('should redirect to route on successful login', () => {
spyOn(authService, 'login').and.returnValue(
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(
of({ type: 'type', ticket: 'ticket' })
);
const redirect = '/home';
@@ -161,10 +172,10 @@ describe('LoginComponent', () => {
appConfigService.config = {};
appConfigService.config.providers = 'ECM';
spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' }));
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' }));
const redirect = '/home';
component.successRoute = redirect;
authService.setRedirect({ provider: 'ECM', url: 'some-route' });
basicAlfrescoAuthService.setRedirect({ provider: 'ECM', url: 'some-route' });
spyOn(router, 'navigateByUrl');
@@ -174,8 +185,7 @@ describe('LoginComponent', () => {
it('should update user preferences upon login', async () => {
spyOn(userPreferences, 'setStoragePrefix').and.callThrough();
spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' }));
spyOn(alfrescoApiService.getInstance(), 'login').and.returnValue(Promise.resolve());
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' }));
component.success.subscribe(() => {
expect(userPreferences.setStoragePrefix).toHaveBeenCalledWith('fake-username');
@@ -206,14 +216,14 @@ describe('LoginComponent', () => {
});
it('should be changed back to the default after a failed login attempt', () => {
spyOn(authService, 'login').and.returnValue(throwError('Fake server error'));
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(throwError('Fake server error'));
loginWithCredentials('fake-wrong-username', 'fake-wrong-password');
expect(getLoginButtonText()).toEqual('LOGIN.BUTTON.LOGIN');
});
it('should be changed to the "welcome key" after a successful login attempt', () => {
spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' }));
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' }));
loginWithCredentials('fake-username', 'fake-password');
expect(getLoginButtonText()).toEqual('LOGIN.BUTTON.WELCOME');
@@ -295,12 +305,12 @@ describe('LoginComponent', () => {
});
it('should be taken into consideration during login attempt', fakeAsync(() => {
spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' }));
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' }));
component.rememberMe = false;
loginWithCredentials('fake-username', 'fake-password');
expect(authService.login).toHaveBeenCalledWith('fake-username', 'fake-password', false);
expect(basicAlfrescoAuthService.login).toHaveBeenCalledWith('fake-username', 'fake-password', false);
}));
});
@@ -469,7 +479,7 @@ describe('LoginComponent', () => {
});
it('should return error with a wrong username', (done) => {
spyOn(alfrescoApiService.getInstance(), 'login').and.returnValue(Promise.reject(new Error('login error')));
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(throwError(new Error()));
component.error.subscribe(() => {
fixture.detectChanges();
@@ -484,7 +494,7 @@ describe('LoginComponent', () => {
});
it('should return error with a wrong password', (done) => {
spyOn(alfrescoApiService.getInstance(), 'login').and.returnValue(Promise.reject(new Error('login error')));
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(throwError(new Error()));
component.error.subscribe(() => {
fixture.detectChanges();
@@ -500,7 +510,7 @@ describe('LoginComponent', () => {
});
it('should return error with a wrong username and password', (done) => {
spyOn(alfrescoApiService.getInstance(), 'login').and.returnValue(Promise.reject(new Error('login error')));
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(throwError(new Error()));
component.error.subscribe(() => {
fixture.detectChanges();
@@ -516,7 +526,7 @@ describe('LoginComponent', () => {
});
it('should return CORS error when server CORS error occurs', (done) => {
spyOn(authService, 'login').and.returnValue(throwError({
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(throwError({
error: {
crossDomain: true,
message: 'ERROR: the network is offline, Origin is not allowed by Access-Control-Allow-Origin'
@@ -537,7 +547,7 @@ describe('LoginComponent', () => {
});
it('should return CSRF error when server CSRF error occurs', fakeAsync(() => {
spyOn(authService, 'login')
spyOn(basicAlfrescoAuthService, 'login')
.and.returnValue(throwError({ message: 'ERROR: Invalid CSRF-token', status: 403 }));
component.error.subscribe(() => {
@@ -552,7 +562,7 @@ describe('LoginComponent', () => {
}));
it('should return ECM read-only error when error occurs', fakeAsync(() => {
spyOn(authService, 'login')
spyOn(basicAlfrescoAuthService, 'login')
.and.returnValue(
throwError(
{
@@ -600,7 +610,7 @@ describe('LoginComponent', () => {
});
it('should return success event after the login have succeeded', (done) => {
spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' }));
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' }));
expect(component.isError).toBe(false);
@@ -616,7 +626,7 @@ describe('LoginComponent', () => {
});
it('should emit success event after the login has succeeded and discard password', fakeAsync(() => {
spyOn(authService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' }));
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(of({ type: 'type', ticket: 'ticket' }));
component.success.subscribe((event) => {
fixture.detectChanges();
@@ -631,7 +641,7 @@ describe('LoginComponent', () => {
}));
it('should emit error event after the login has failed', fakeAsync(() => {
spyOn(authService, 'login').and.returnValue(throwError('Fake server error'));
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(throwError('Fake server error'));
component.error.subscribe((error) => {
fixture.detectChanges();
@@ -668,7 +678,7 @@ describe('LoginComponent', () => {
});
it('should emit only the username and not the password as part of the executeSubmit', fakeAsync(() => {
spyOn(alfrescoApiService.getInstance(), 'login').and.returnValue(Promise.resolve());
spyOn(basicAlfrescoAuthService, 'login').and.returnValue(EMPTY);
component.executeSubmit.subscribe((res) => {
fixture.detectChanges();
@@ -688,7 +698,6 @@ describe('LoginComponent', () => {
beforeEach(() => {
appConfigService.config.oauth2 = { implicitFlow: true, silentLogin: false };
appConfigService.load();
alfrescoApiService.reset();
});
it('should not show login username and password if SSO implicit flow is active', fakeAsync(() => {

View File

@@ -29,6 +29,8 @@ import { AppConfigService, AppConfigValues } from '../../app-config/app-config.s
import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { BasicAlfrescoAuthService } from '../../auth/basic-auth/basic-alfresco-auth.service';
import { OidcAuthenticationService } from '../../auth/services/oidc-authentication.service';
// eslint-disable-next-line no-shadow
enum LoginSteps {
@@ -52,7 +54,7 @@ interface LoginFormValues {
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
encapsulation: ViewEncapsulation.None,
host: { class: 'adf-login' }
host: {class: 'adf-login'}
})
export class LoginComponent implements OnInit, OnDestroy {
isPasswordShow: boolean = false;
@@ -129,6 +131,8 @@ export class LoginComponent implements OnInit, OnDestroy {
constructor(
private _fb: UntypedFormBuilder,
private authService: AuthenticationService,
private basicAlfrescoAuthService: BasicAlfrescoAuthService,
private oidcAuthenticationService: OidcAuthenticationService,
private translateService: TranslationService,
private router: Router,
private appConfig: AppConfigService,
@@ -160,7 +164,7 @@ export class LoginComponent implements OnInit, OnDestroy {
const url = params['redirectUrl'];
const provider = this.appConfig.get<string>(AppConfigValues.PROVIDERS);
this.authService.setRedirect({ provider, url });
this.basicAlfrescoAuthService.setRedirect({provider, url});
});
}
@@ -181,7 +185,7 @@ export class LoginComponent implements OnInit, OnDestroy {
}
redirectToImplicitLogin() {
this.authService.ssoImplicitLogin();
this.oidcAuthenticationService.ssoImplicitLogin();
}
/**
@@ -193,12 +197,13 @@ export class LoginComponent implements OnInit, OnDestroy {
this.disableError();
const args = new LoginSubmitEvent({
controls: { username: this.form.controls.username }
controls: {username: this.form.controls.username}
});
this.executeSubmit.emit(args);
if (!args.defaultPrevented) {
this.actualLoginStep = LoginSteps.Checking;
this.performLogin(values);
}
}
@@ -207,7 +212,7 @@ export class LoginComponent implements OnInit, OnDestroy {
if (this.authService.isLoggedIn()) {
this.router.navigate([this.successRoute]);
}
this.authService.ssoImplicitLogin();
this.oidcAuthenticationService.ssoImplicitLogin();
}
/**
@@ -237,30 +242,31 @@ export class LoginComponent implements OnInit, OnDestroy {
}
}
performLogin(values: LoginFormValues) {
this.authService.login(values.username, values.password, this.rememberMe).subscribe(
(token) => {
const redirectUrl = this.authService.getRedirect();
performLogin(values: { username: string; password: string }) {
this.authService.login(values.username, values.password, this.rememberMe)
.subscribe(
async (token: any) => {
const redirectUrl = this.basicAlfrescoAuthService.getRedirect();
this.actualLoginStep = LoginSteps.Welcome;
this.userPreferences.setStoragePrefix(values.username);
values.password = null;
this.success.emit(new LoginSuccessEvent(token, values.username, null));
this.actualLoginStep = LoginSteps.Welcome;
this.userPreferences.setStoragePrefix(values.username);
values.password = null;
this.success.emit(new LoginSuccessEvent(token, values.username, null));
if (redirectUrl) {
this.authService.setRedirect(null);
this.router.navigateByUrl(redirectUrl);
} else if (this.successRoute) {
this.router.navigate([this.successRoute]);
if (redirectUrl) {
this.basicAlfrescoAuthService.setRedirect(null);
await this.router.navigateByUrl(redirectUrl);
} else if (this.successRoute) {
await this.router.navigate([this.successRoute]);
}
},
(err: any) => {
this.actualLoginStep = LoginSteps.Landing;
this.displayErrorMessage(err);
this.isError = true;
this.error.emit(new LoginErrorEvent(err));
}
},
(err: any) => {
this.actualLoginStep = LoginSteps.Landing;
this.displayErrorMessage(err);
this.isError = true;
this.error.emit(new LoginErrorEvent(err));
}
);
);
}
/**

View File

@@ -20,6 +20,7 @@ import { LoginComponent } from '../components/login.component';
import { LoginFooterDirective } from './login-footer.directive';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { OidcAuthenticationService } from '../../auth/services/oidc-authentication.service';
describe('LoginFooterDirective', () => {
let fixture: ComponentFixture<LoginComponent>;
@@ -31,6 +32,11 @@ describe('LoginFooterDirective', () => {
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
providers: [
{
provide: OidcAuthenticationService, useValue: {}
}
]
});
fixture = TestBed.createComponent(LoginComponent);

View File

@@ -20,6 +20,7 @@ import { LoginComponent } from '../components/login.component';
import { LoginHeaderDirective } from './login-header.directive';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { OidcAuthenticationService } from '../../auth/services/oidc-authentication.service';
describe('LoginHeaderDirective', () => {
let fixture: ComponentFixture<LoginComponent>;
@@ -31,6 +32,9 @@ describe('LoginHeaderDirective', () => {
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
providers: [
{ provide: OidcAuthenticationService, useValue: {} }
]
});
fixture = TestBed.createComponent(LoginComponent);

View File

@@ -16,12 +16,12 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SnackbarContentComponent } from '@alfresco/adf-core';
import { MatIcon, MatIconModule } from '@angular/material/icon';
import { MAT_SNACK_BAR_DATA, MatSnackBarModule, MatSnackBarRef } from '@angular/material/snack-bar';
import { MatButtonModule } from '@angular/material/button';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { SnackbarContentComponent } from './snackbar-content.component';
describe('SnackbarContentComponent', () => {
let component: SnackbarContentComponent;

View File

@@ -36,7 +36,8 @@ export class CoreAutomationService {
private userPreferencesService: UserPreferencesService,
private storageService: StorageService,
private auth: AuthenticationService
) {}
) {
}
setup() {
const adfProxy = window['adf'] || {};

View File

@@ -20,9 +20,11 @@ import { CoreModule } from '../core.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { provideTranslations } from '../translation/translation.service';
import { AuthModule } from '../../../src/lib/auth/oidc/auth.module';
@NgModule({
imports: [
AuthModule.forRoot(),
TranslateModule.forRoot(),
CoreModule.forRoot(),
BrowserAnimationsModule

View File

@@ -32,9 +32,11 @@ import { CookieServiceMock } from '../mock/cookie.service.mock';
import { HttpClientModule } from '@angular/common/http';
import { directionalityConfigFactory } from '../common/services/directionality-config-factory';
import { DirectionalityConfigService } from '../common/services/directionality-config.service';
import { AuthModule } from '../auth';
@NgModule({
imports: [
AuthModule.forRoot({ useHash: true }),
NoopAnimationsModule,
RouterTestingModule,
HttpClientModule,

View File

@@ -20,6 +20,7 @@ import { TranslateLoaderService } from './translate-loader.service';
import { TranslationService } from './translation.service';
import { TranslateModule } from '@ngx-translate/core';
import { CoreModule } from '../core.module';
import { AuthModule } from '../auth';
declare let jasmine: any;
@@ -30,6 +31,7 @@ describe('TranslateLoader', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
AuthModule.forRoot({ useHash: true }),
TranslateModule.forRoot(),
CoreModule.forRoot()
],

View File

@@ -17,10 +17,12 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CoreTestingModule, DownloadPromptDialogComponent, DownloadPromptActions } from '@alfresco/adf-core';
import { By } from '@angular/platform-browser';
import { MatDialogRef } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
import { DownloadPromptDialogComponent } from './download-prompt-dialog.component';
import { CoreTestingModule } from '../../../testing/core.testing.module';
import { DownloadPromptActions } from '../../models/download-prompt.actions';
const mockDialog = {
close: jasmine.createSpy('close')