mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[MNT-24614] Fixed APS basic auth login issue with ADF (#10364)
* [MNT-24614] Fixed APS basic auth login issue with ADF * [MNT-24614] Addressed code review findings - Using includes api, and removed unneeded functions. Added missing return type to functions * [MNT-24614] Added unit tests * [MNT-24614] Added unit tests * [MNT-24614] Fixed casing of unit test titles
This commit is contained in:
@@ -387,4 +387,36 @@ describe('AdfHttpClient', () => {
|
|||||||
|
|
||||||
req.flush(null, { status: 200, statusText: 'Ok' });
|
req.flush(null, { status: 200, statusText: 'Ok' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set X-CSRF-TOKEN header if CSRF is enabled', () => {
|
||||||
|
const options: RequestOptions = {
|
||||||
|
path: '',
|
||||||
|
httpMethod: 'GET'
|
||||||
|
};
|
||||||
|
angularHttpClient.disableCsrf = false;
|
||||||
|
|
||||||
|
angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch((error) => fail(error));
|
||||||
|
|
||||||
|
const req = controller.expectOne('http://example.com');
|
||||||
|
|
||||||
|
expect(req.request.headers.get('X-CSRF-TOKEN')).toBeDefined();
|
||||||
|
|
||||||
|
req.flush(null, { status: 200, statusText: 'Ok' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set X-CSRF-TOKEN header if CSRF is disabled', () => {
|
||||||
|
const options: RequestOptions = {
|
||||||
|
path: '',
|
||||||
|
httpMethod: 'GET'
|
||||||
|
};
|
||||||
|
angularHttpClient.disableCsrf = true;
|
||||||
|
|
||||||
|
angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch((error) => fail(error));
|
||||||
|
|
||||||
|
const req = controller.expectOne('http://example.com');
|
||||||
|
|
||||||
|
expect(req.request.headers.get('X-CSRF-TOKEN')).toBeNull();
|
||||||
|
|
||||||
|
req.flush(null, { status: 200, statusText: 'Ok' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -17,15 +17,7 @@
|
|||||||
|
|
||||||
import { SHOULD_ADD_AUTH_TOKEN } from '@alfresco/adf-core/auth';
|
import { SHOULD_ADD_AUTH_TOKEN } from '@alfresco/adf-core/auth';
|
||||||
import { Emitters as JsApiEmitters, HttpClient as JsApiHttpClient } from '@alfresco/js-api';
|
import { Emitters as JsApiEmitters, HttpClient as JsApiHttpClient } from '@alfresco/js-api';
|
||||||
import {
|
import { HttpClient, HttpContext, HttpErrorResponse, HttpEvent, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http';
|
||||||
HttpClient,
|
|
||||||
HttpContext,
|
|
||||||
HttpErrorResponse,
|
|
||||||
HttpEvent,
|
|
||||||
HttpHeaders,
|
|
||||||
HttpParams,
|
|
||||||
HttpResponse
|
|
||||||
} from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable, of, Subject, throwError } from 'rxjs';
|
import { Observable, of, Subject, throwError } from 'rxjs';
|
||||||
import { catchError, map, takeUntil } from 'rxjs/operators';
|
import { catchError, map, takeUntil } from 'rxjs/operators';
|
||||||
@@ -52,8 +44,7 @@ export interface Emitters {
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
|
export class AdfHttpClient implements ee.Emitter, JsApiHttpClient {
|
||||||
|
|
||||||
on: ee.EmitterMethod;
|
on: ee.EmitterMethod;
|
||||||
off: ee.EmitterMethod;
|
off: ee.EmitterMethod;
|
||||||
once: ee.EmitterMethod;
|
once: ee.EmitterMethod;
|
||||||
@@ -107,44 +98,40 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
|
|||||||
const params = getQueryParamsWithCustomEncoder(options.queryParams, new AlfrescoApiParamEncoder());
|
const params = getQueryParamsWithCustomEncoder(options.queryParams, new AlfrescoApiParamEncoder());
|
||||||
const responseType = AdfHttpClient.getResponseType(options);
|
const responseType = AdfHttpClient.getResponseType(options);
|
||||||
const context = new HttpContext().set(SHOULD_ADD_AUTH_TOKEN, true);
|
const context = new HttpContext().set(SHOULD_ADD_AUTH_TOKEN, true);
|
||||||
const security: SecurityOptions = {...this.defaultSecurityOptions, ...sc};
|
const security: SecurityOptions = { ...this.defaultSecurityOptions, ...sc };
|
||||||
const headers = this.getHeaders(options);
|
const headers = this.getHeaders(options);
|
||||||
if (!emitters) {
|
if (!emitters) {
|
||||||
emitters = this.getEventEmitters();
|
emitters = this.getEventEmitters();
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = this.httpClient.request(
|
const request = this.httpClient.request(options.httpMethod, url, {
|
||||||
options.httpMethod,
|
|
||||||
url,
|
|
||||||
{
|
|
||||||
context,
|
context,
|
||||||
...(body && {body}),
|
...(body && { body }),
|
||||||
...(responseType && {responseType}),
|
...(responseType && { responseType }),
|
||||||
...security,
|
...security,
|
||||||
...(params && {params}),
|
...(params && { params }),
|
||||||
headers,
|
headers,
|
||||||
observe: 'events',
|
observe: 'events',
|
||||||
reportProgress: true
|
reportProgress: true
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return this.requestWithLegacyEventEmitters<T>(request, emitters, options.returnType);
|
return this.requestWithLegacyEventEmitters<T>(request, emitters, options.returnType);
|
||||||
}
|
}
|
||||||
|
|
||||||
post<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
|
post<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
|
||||||
return this.request<T>(url, {...options, httpMethod: 'POST'}, sc, emitters);
|
return this.request<T>(url, { ...options, httpMethod: 'POST' }, sc, emitters);
|
||||||
}
|
}
|
||||||
|
|
||||||
put<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
|
put<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
|
||||||
return this.request<T>(url, {...options, httpMethod: 'PUT'}, sc, emitters);
|
return this.request<T>(url, { ...options, httpMethod: 'PUT' }, sc, emitters);
|
||||||
}
|
}
|
||||||
|
|
||||||
get<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
|
get<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
|
||||||
return this.request<T>(url, {...options, httpMethod: 'GET'}, sc, emitters);
|
return this.request<T>(url, { ...options, httpMethod: 'GET' }, sc, emitters);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete<T = void>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
|
delete<T = void>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
|
||||||
return this.request<T>(url, {...options, httpMethod: 'DELETE'}, sc, emitters);
|
return this.request<T>(url, { ...options, httpMethod: 'DELETE' }, sc, emitters);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addPromiseListeners<T = any>(promise: Promise<T>, eventEmitter: any) {
|
private addPromiseListeners<T = any>(promise: Promise<T>, eventEmitter: any) {
|
||||||
@@ -189,15 +176,15 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private requestWithLegacyEventEmitters<T = any>(request$: Observable<HttpEvent<T>>, emitters: JsApiEmitters, returnType: any): Promise<T> {
|
private requestWithLegacyEventEmitters<T = any>(request$: Observable<HttpEvent<T>>, emitters: JsApiEmitters, returnType: any): Promise<T> {
|
||||||
|
|
||||||
const abort$ = new Subject<void>();
|
const abort$ = new Subject<void>();
|
||||||
const {eventEmitter, apiClientEmitter} = emitters;
|
const { eventEmitter, apiClientEmitter } = emitters;
|
||||||
|
|
||||||
const promise = request$.pipe(
|
const promise = request$
|
||||||
|
.pipe(
|
||||||
map((res) => {
|
map((res) => {
|
||||||
if (isHttpUploadProgressEvent(res)) {
|
if (isHttpUploadProgressEvent(res)) {
|
||||||
const percent = Math.round((res.loaded / res.total) * 100);
|
const percent = Math.round((res.loaded / res.total) * 100);
|
||||||
eventEmitter.emit('progress', {loaded: res.loaded, total: res.total, percent});
|
eventEmitter.emit('progress', { loaded: res.loaded, total: res.total, percent });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHttpResponseEvent(res)) {
|
if (isHttpResponseEvent(res)) {
|
||||||
@@ -206,7 +193,6 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError((err: HttpErrorResponse): Observable<AlfrescoApiResponseError> => {
|
catchError((err: HttpErrorResponse): Observable<AlfrescoApiResponseError> => {
|
||||||
|
|
||||||
// since we can't always determinate ahead of time if the response is going to be xml or plain text response
|
// since we can't always determinate ahead of time if the response is going to be xml or plain text response
|
||||||
// we need to handle false positive cases here.
|
// we need to handle false positive cases here.
|
||||||
|
|
||||||
@@ -231,16 +217,18 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
|
|||||||
// for backwards compatibility to handle cases in code where we try read response.error.response.body;
|
// for backwards compatibility to handle cases in code where we try read response.error.response.body;
|
||||||
|
|
||||||
const error = {
|
const error = {
|
||||||
...err, body: err.error
|
...err,
|
||||||
|
body: err.error
|
||||||
};
|
};
|
||||||
|
|
||||||
const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error);
|
const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error);
|
||||||
return throwError(alfrescoApiError);
|
return throwError(alfrescoApiError);
|
||||||
}),
|
}),
|
||||||
takeUntil(abort$)
|
takeUntil(abort$)
|
||||||
).toPromise();
|
)
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
(promise as any).abort = function() {
|
(promise as any).abort = function () {
|
||||||
eventEmitter.emit('abort');
|
eventEmitter.emit('abort');
|
||||||
abort$.next();
|
abort$.next();
|
||||||
abort$.complete();
|
abort$.complete();
|
||||||
@@ -261,7 +249,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isFormUrlEncoded) {
|
if (isFormUrlEncoded) {
|
||||||
return new HttpParams({fromObject: removeNilValues(options.formParams)});
|
return new HttpParams({ fromObject: removeNilValues(options.formParams) });
|
||||||
}
|
}
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
@@ -273,8 +261,8 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
|
|||||||
|
|
||||||
const optionsHeaders = {
|
const optionsHeaders = {
|
||||||
...options.headerParams,
|
...options.headerParams,
|
||||||
...(accept && {Accept: accept}),
|
...(accept && { Accept: accept }),
|
||||||
...((contentType) && {'Content-Type': contentType})
|
...(contentType && { 'Content-Type': contentType })
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.disableCsrf) {
|
if (!this.disableCsrf) {
|
||||||
@@ -319,7 +307,6 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
|
|||||||
return Boolean(contentType?.match(/^application\/json(;.*)?$/i));
|
return Boolean(contentType?.match(/^application\/json(;.*)?$/i));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private setCsrfToken(optionsHeaders: any) {
|
private setCsrfToken(optionsHeaders: any) {
|
||||||
const token = this.createCSRFToken();
|
const token = this.createCSRFToken();
|
||||||
optionsHeaders['X-CSRF-TOKEN'] = token;
|
optionsHeaders['X-CSRF-TOKEN'] = token;
|
||||||
@@ -332,12 +319,16 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createCSRFToken(a?: any): string {
|
private createCSRFToken(a?: any): string {
|
||||||
const randomValue = window.crypto.getRandomValues(new Uint32Array(1))[0];
|
const randomValue = AdfHttpClient.getSecureRandomValue();
|
||||||
return a ? (a ^ ((randomValue * 16) >> (a / 4))).toString(16) : ([1e16] + (1e16).toString()).replace(/[01]/g, this.createCSRFToken);
|
return a ? (a ^ ((randomValue * 16) >> (a / 4))).toString(16) : ([1e16] + (1e16).toString()).replace(/[01]/g, this.createCSRFToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getResponseType(options: RequestOptions): 'blob' | 'json' | 'text' {
|
private static getSecureRandomValue(): number {
|
||||||
|
const max = Math.pow(2, 32);
|
||||||
|
return window.crypto.getRandomValues(new Uint32Array(1))[0] / max;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getResponseType(options: RequestOptions): 'blob' | 'json' | 'text' {
|
||||||
const isBlobType = options.returnType?.toString().toLowerCase() === 'blob' || options.responseType?.toString().toLowerCase() === 'blob';
|
const isBlobType = options.returnType?.toString().toLowerCase() === 'blob' || options.responseType?.toString().toLowerCase() === 'blob';
|
||||||
|
|
||||||
if (isBlobType) {
|
if (isBlobType) {
|
||||||
@@ -359,7 +350,6 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
|
|||||||
* @returns deserialized object
|
* @returns deserialized object
|
||||||
*/
|
*/
|
||||||
private static deserialize<T>(response: HttpResponse<T>, returnType?: Constructor<unknown> | 'blob'): any {
|
private static deserialize<T>(response: HttpResponse<T>, returnType?: Constructor<unknown> | 'blob'): any {
|
||||||
|
|
||||||
if (response === null) {
|
if (response === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -390,9 +380,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
|
|||||||
return new returnType(body);
|
return new returnType(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static deserializeBlobResponse(response: HttpResponse<Blob>) {
|
private static deserializeBlobResponse(response: HttpResponse<Blob>) {
|
||||||
return new Blob([response.body], {type: response.headers.get('Content-Type')});
|
return new Blob([response.body], { type: response.headers.get('Content-Type') });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,62 @@
|
|||||||
|
/*!
|
||||||
|
* @license
|
||||||
|
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { BasicAlfrescoAuthService } from './basic-alfresco-auth.service';
|
||||||
|
import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service';
|
||||||
|
import { ProcessAuth } from './process-auth';
|
||||||
|
import { ContentAuth } from './content-auth';
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
|
|
||||||
|
describe('BasicAlfrescoAuthService', () => {
|
||||||
|
let basicAlfrescoAuthService: BasicAlfrescoAuthService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [BasicAlfrescoAuthService]
|
||||||
|
});
|
||||||
|
basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService);
|
||||||
|
spyOn(TestBed.inject(ProcessAuth), 'getToken').and.returnValue('Mock Process Auth ticket');
|
||||||
|
spyOn(TestBed.inject(ContentAuth), 'getToken').and.returnValue('Mock Content Auth ticket');
|
||||||
|
const appConfigSpy = spyOn(TestBed.inject(AppConfigService), 'get');
|
||||||
|
appConfigSpy.withArgs(AppConfigValues.CONTEXTROOTBPM).and.returnValue('activiti-app');
|
||||||
|
appConfigSpy.withArgs(AppConfigValues.CONTEXTROOTECM).and.returnValue('alfresco');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return content services ticket when requestUrl contains ECM context root', () => {
|
||||||
|
const ticket = basicAlfrescoAuthService.getTicketEcmBase64('http://www.exmple.com/alfresco/mock-api-url');
|
||||||
|
const base64Segment = ticket.split('Basic ')[1];
|
||||||
|
expect(atob(base64Segment)).toEqual('Mock Content Auth ticket');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return process services ticket when requestUrl contains ECM context root', () => {
|
||||||
|
const ticket = basicAlfrescoAuthService.getTicketEcmBase64('http://www.example.com/activiti-app/mock-api-url');
|
||||||
|
expect(ticket).toEqual('Basic Mock Process Auth ticket');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return content services ticket when requestUrl contains both ECM and BPM context root, but ECM context root comes before', () => {
|
||||||
|
const ticket = basicAlfrescoAuthService.getTicketEcmBase64('http://www.exmple.com/alfresco/activiti-app/mock-api-url');
|
||||||
|
const base64Segment = ticket.split('Basic ')[1];
|
||||||
|
expect(atob(base64Segment)).toEqual('Mock Content Auth ticket');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return process services ticket when requestUrl contains both ECM and BPM context root, but BPM context root comes before', () => {
|
||||||
|
const ticket = basicAlfrescoAuthService.getTicketEcmBase64('http://www.example.com/activiti-app/alfresco/mock-api-url');
|
||||||
|
expect(ticket).toEqual('Basic Mock Process Auth ticket');
|
||||||
|
});
|
||||||
|
});
|
@@ -373,15 +373,24 @@ export class BasicAlfrescoAuthService extends BaseAuthenticationService {
|
|||||||
getTicketEcmBase64(requestUrl: string): string | null {
|
getTicketEcmBase64(requestUrl: string): string | null {
|
||||||
let ticket = null;
|
let ticket = null;
|
||||||
|
|
||||||
const contextRootBpm = this.appConfig.get<string>(AppConfigValues.CONTEXTROOTBPM) || 'activiti-app';
|
const bpmRoot = `/${this.appConfig.get<string>(AppConfigValues.CONTEXTROOTBPM) || 'activiti-app'}/`;
|
||||||
const contextRoot = this.appConfig.get<string>(AppConfigValues.CONTEXTROOTECM) || 'alfresco';
|
const ecmRoot = `/${this.appConfig.get<string>(AppConfigValues.CONTEXTROOTECM) || 'alfresco'}/`;
|
||||||
|
|
||||||
if (contextRoot && requestUrl.indexOf(contextRoot) !== -1) {
|
if (requestUrl?.includes(ecmRoot) && !requestUrl.includes(bpmRoot)) {
|
||||||
ticket = 'Basic ' + btoa(this.contentAuth.getToken());
|
ticket = this.getContentServicesTicket();
|
||||||
} else if (contextRootBpm && requestUrl.indexOf(contextRootBpm) !== -1) {
|
} else if (requestUrl?.includes(bpmRoot) && !requestUrl.includes(ecmRoot)) {
|
||||||
ticket = 'Basic ' + this.processAuth.getToken();
|
ticket = this.getProcessServicesTicket();
|
||||||
|
} else if (requestUrl?.includes(ecmRoot) && requestUrl.includes(bpmRoot)) {
|
||||||
|
ticket = requestUrl.indexOf(ecmRoot) < requestUrl.indexOf(bpmRoot) ? this.getContentServicesTicket() : this.getProcessServicesTicket();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ticket;
|
return ticket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getProcessServicesTicket(): string {
|
||||||
|
return this.processAuth.getToken()?.startsWith('Basic ') ? this.processAuth.getToken() : 'Basic ' + this.processAuth.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getContentServicesTicket(): string {
|
||||||
|
return 'Basic ' + btoa(this.contentAuth.getToken());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user