Skip to content

Commit 35c8093

Browse files
[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
1 parent 3ec3e73 commit 35c8093

File tree

4 files changed

+191
-100
lines changed

4 files changed

+191
-100
lines changed

lib/core/api/src/lib/adf-http-client.service.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,4 +387,36 @@ describe('AdfHttpClient', () => {
387387

388388
req.flush(null, { status: 200, statusText: 'Ok' });
389389
});
390+
391+
it('should set X-CSRF-TOKEN header if CSRF is enabled', () => {
392+
const options: RequestOptions = {
393+
path: '',
394+
httpMethod: 'GET'
395+
};
396+
angularHttpClient.disableCsrf = false;
397+
398+
angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch((error) => fail(error));
399+
400+
const req = controller.expectOne('http://example.com');
401+
402+
expect(req.request.headers.get('X-CSRF-TOKEN')).toBeDefined();
403+
404+
req.flush(null, { status: 200, statusText: 'Ok' });
405+
});
406+
407+
it('should not set X-CSRF-TOKEN header if CSRF is disabled', () => {
408+
const options: RequestOptions = {
409+
path: '',
410+
httpMethod: 'GET'
411+
};
412+
angularHttpClient.disableCsrf = true;
413+
414+
angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch((error) => fail(error));
415+
416+
const req = controller.expectOne('http://example.com');
417+
418+
expect(req.request.headers.get('X-CSRF-TOKEN')).toBeNull();
419+
420+
req.flush(null, { status: 200, statusText: 'Ok' });
421+
});
390422
});

lib/core/api/src/lib/adf-http-client.service.ts

Lines changed: 80 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,7 @@
1717

1818
import { SHOULD_ADD_AUTH_TOKEN } from '@alfresco/adf-core/auth';
1919
import { Emitters as JsApiEmitters, HttpClient as JsApiHttpClient } from '@alfresco/js-api';
20-
import {
21-
HttpClient,
22-
HttpContext,
23-
HttpErrorResponse,
24-
HttpEvent,
25-
HttpHeaders,
26-
HttpParams,
27-
HttpResponse
28-
} from '@angular/common/http';
20+
import { HttpClient, HttpContext, HttpErrorResponse, HttpEvent, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http';
2921
import { Injectable } from '@angular/core';
3022
import { Observable, of, Subject, throwError } from 'rxjs';
3123
import { catchError, map, takeUntil } from 'rxjs/operators';
@@ -52,8 +44,7 @@ export interface Emitters {
5244
@Injectable({
5345
providedIn: 'root'
5446
})
55-
export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
56-
47+
export class AdfHttpClient implements ee.Emitter, JsApiHttpClient {
5748
on: ee.EmitterMethod;
5849
off: ee.EmitterMethod;
5950
once: ee.EmitterMethod;
@@ -107,47 +98,43 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
10798
const params = getQueryParamsWithCustomEncoder(options.queryParams, new AlfrescoApiParamEncoder());
10899
const responseType = AdfHttpClient.getResponseType(options);
109100
const context = new HttpContext().set(SHOULD_ADD_AUTH_TOKEN, true);
110-
const security: SecurityOptions = {...this.defaultSecurityOptions, ...sc};
101+
const security: SecurityOptions = { ...this.defaultSecurityOptions, ...sc };
111102
const headers = this.getHeaders(options);
112103
if (!emitters) {
113104
emitters = this.getEventEmitters();
114105
}
115106

116-
const request = this.httpClient.request(
117-
options.httpMethod,
118-
url,
119-
{
120-
context,
121-
...(body && {body}),
122-
...(responseType && {responseType}),
123-
...security,
124-
...(params && {params}),
125-
headers,
126-
observe: 'events',
127-
reportProgress: true
128-
}
129-
);
107+
const request = this.httpClient.request(options.httpMethod, url, {
108+
context,
109+
...(body && { body }),
110+
...(responseType && { responseType }),
111+
...security,
112+
...(params && { params }),
113+
headers,
114+
observe: 'events',
115+
reportProgress: true
116+
});
130117

131118
return this.requestWithLegacyEventEmitters<T>(request, emitters, options.returnType);
132119
}
133120

134121
post<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
135-
return this.request<T>(url, {...options, httpMethod: 'POST'}, sc, emitters);
122+
return this.request<T>(url, { ...options, httpMethod: 'POST' }, sc, emitters);
136123
}
137124

138125
put<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
139-
return this.request<T>(url, {...options, httpMethod: 'PUT'}, sc, emitters);
126+
return this.request<T>(url, { ...options, httpMethod: 'PUT' }, sc, emitters);
140127
}
141128

142129
get<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
143-
return this.request<T>(url, {...options, httpMethod: 'GET'}, sc, emitters);
130+
return this.request<T>(url, { ...options, httpMethod: 'GET' }, sc, emitters);
144131
}
145132

146133
delete<T = void>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
147-
return this.request<T>(url, {...options, httpMethod: 'DELETE'}, sc, emitters);
134+
return this.request<T>(url, { ...options, httpMethod: 'DELETE' }, sc, emitters);
148135
}
149136

150-
private addPromiseListeners<T = any>(promise: Promise<T>, eventEmitter: any) {
137+
private addPromiseListeners<T = any>(promise: Promise<T>, eventEmitter: any) {
151138
const eventPromise = Object.assign(promise, {
152139
on() {
153140
// eslint-disable-next-line prefer-spread, prefer-rest-params
@@ -189,58 +176,59 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
189176
}
190177

191178
private requestWithLegacyEventEmitters<T = any>(request$: Observable<HttpEvent<T>>, emitters: JsApiEmitters, returnType: any): Promise<T> {
192-
193179
const abort$ = new Subject<void>();
194-
const {eventEmitter, apiClientEmitter} = emitters;
195-
196-
const promise = request$.pipe(
197-
map((res) => {
198-
if (isHttpUploadProgressEvent(res)) {
199-
const percent = Math.round((res.loaded / res.total) * 100);
200-
eventEmitter.emit('progress', {loaded: res.loaded, total: res.total, percent});
201-
}
202-
203-
if (isHttpResponseEvent(res)) {
204-
eventEmitter.emit('success', res.body);
205-
return AdfHttpClient.deserialize(res, returnType);
206-
}
207-
}),
208-
catchError((err: HttpErrorResponse): Observable<AlfrescoApiResponseError> => {
209-
210-
// since we can't always determinate ahead of time if the response is going to be xml or plain text response
211-
// we need to handle false positive cases here.
212-
213-
if (err.status === 200) {
214-
eventEmitter.emit('success', err.error.text);
215-
return of(err.error.text);
216-
}
217-
218-
eventEmitter.emit('error', err);
219-
apiClientEmitter.emit('error', { ...err, response: { req: err } });
220-
221-
if (err.status === 401) {
222-
eventEmitter.emit('unauthorized');
223-
apiClientEmitter.emit('unauthorized');
224-
}
225-
226-
// for backwards compatibility we need to convert it to error class as the HttpErrorResponse only implements Error interface, not extending it,
227-
// and we need to be able to correctly pass instanceof Error conditions used inside repository
228-
// we also need to pass error as Stringify string as we are detecting statusCodes using JSON.parse(error.message) in some places
229-
const msg = typeof err.error === 'string' ? err.error : JSON.stringify(err.error);
230-
231-
// for backwards compatibility to handle cases in code where we try read response.error.response.body;
232-
233-
const error = {
234-
...err, body: err.error
235-
};
236-
237-
const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error);
238-
return throwError(alfrescoApiError);
239-
}),
240-
takeUntil(abort$)
241-
).toPromise();
242-
243-
(promise as any).abort = function() {
180+
const { eventEmitter, apiClientEmitter } = emitters;
181+
182+
const promise = request$
183+
.pipe(
184+
map((res) => {
185+
if (isHttpUploadProgressEvent(res)) {
186+
const percent = Math.round((res.loaded / res.total) * 100);
187+
eventEmitter.emit('progress', { loaded: res.loaded, total: res.total, percent });
188+
}
189+
190+
if (isHttpResponseEvent(res)) {
191+
eventEmitter.emit('success', res.body);
192+
return AdfHttpClient.deserialize(res, returnType);
193+
}
194+
}),
195+
catchError((err: HttpErrorResponse): Observable<AlfrescoApiResponseError> => {
196+
// since we can't always determinate ahead of time if the response is going to be xml or plain text response
197+
// we need to handle false positive cases here.
198+
199+
if (err.status === 200) {
200+
eventEmitter.emit('success', err.error.text);
201+
return of(err.error.text);
202+
}
203+
204+
eventEmitter.emit('error', err);
205+
apiClientEmitter.emit('error', { ...err, response: { req: err } });
206+
207+
if (err.status === 401) {
208+
eventEmitter.emit('unauthorized');
209+
apiClientEmitter.emit('unauthorized');
210+
}
211+
212+
// for backwards compatibility we need to convert it to error class as the HttpErrorResponse only implements Error interface, not extending it,
213+
// and we need to be able to correctly pass instanceof Error conditions used inside repository
214+
// we also need to pass error as Stringify string as we are detecting statusCodes using JSON.parse(error.message) in some places
215+
const msg = typeof err.error === 'string' ? err.error : JSON.stringify(err.error);
216+
217+
// for backwards compatibility to handle cases in code where we try read response.error.response.body;
218+
219+
const error = {
220+
...err,
221+
body: err.error
222+
};
223+
224+
const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error);
225+
return throwError(alfrescoApiError);
226+
}),
227+
takeUntil(abort$)
228+
)
229+
.toPromise();
230+
231+
(promise as any).abort = function () {
244232
eventEmitter.emit('abort');
245233
abort$.next();
246234
abort$.complete();
@@ -261,7 +249,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
261249
}
262250

263251
if (isFormUrlEncoded) {
264-
return new HttpParams({fromObject: removeNilValues(options.formParams)});
252+
return new HttpParams({ fromObject: removeNilValues(options.formParams) });
265253
}
266254

267255
return body;
@@ -273,8 +261,8 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
273261

274262
const optionsHeaders = {
275263
...options.headerParams,
276-
...(accept && {Accept: accept}),
277-
...((contentType) && {'Content-Type': contentType})
264+
...(accept && { Accept: accept }),
265+
...(contentType && { 'Content-Type': contentType })
278266
};
279267

280268
if (!this.disableCsrf) {
@@ -319,7 +307,6 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
319307
return Boolean(contentType?.match(/^application\/json(;.*)?$/i));
320308
}
321309

322-
323310
private setCsrfToken(optionsHeaders: any) {
324311
const token = this.createCSRFToken();
325312
optionsHeaders['X-CSRF-TOKEN'] = token;
@@ -332,12 +319,16 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
332319
}
333320

334321
private createCSRFToken(a?: any): string {
335-
const randomValue = window.crypto.getRandomValues(new Uint32Array(1))[0];
322+
const randomValue = AdfHttpClient.getSecureRandomValue();
336323
return a ? (a ^ ((randomValue * 16) >> (a / 4))).toString(16) : ([1e16] + (1e16).toString()).replace(/[01]/g, this.createCSRFToken);
337324
}
338325

339-
private static getResponseType(options: RequestOptions): 'blob' | 'json' | 'text' {
326+
private static getSecureRandomValue(): number {
327+
const max = Math.pow(2, 32);
328+
return window.crypto.getRandomValues(new Uint32Array(1))[0] / max;
329+
}
340330

331+
private static getResponseType(options: RequestOptions): 'blob' | 'json' | 'text' {
341332
const isBlobType = options.returnType?.toString().toLowerCase() === 'blob' || options.responseType?.toString().toLowerCase() === 'blob';
342333

343334
if (isBlobType) {
@@ -359,7 +350,6 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
359350
* @returns deserialized object
360351
*/
361352
private static deserialize<T>(response: HttpResponse<T>, returnType?: Constructor<unknown> | 'blob'): any {
362-
363353
if (response === null) {
364354
return null;
365355
}
@@ -390,9 +380,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
390380
return new returnType(body);
391381
}
392382

393-
394383
private static deserializeBlobResponse(response: HttpResponse<Blob>) {
395-
return new Blob([response.body], {type: response.headers.get('Content-Type')});
384+
return new Blob([response.body], { type: response.headers.get('Content-Type') });
396385
}
397386
}
398-
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*!
2+
* @license
3+
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { TestBed } from '@angular/core/testing';
19+
import { BasicAlfrescoAuthService } from './basic-alfresco-auth.service';
20+
import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service';
21+
import { ProcessAuth } from './process-auth';
22+
import { ContentAuth } from './content-auth';
23+
import { HttpClientTestingModule } from '@angular/common/http/testing';
24+
25+
describe('BasicAlfrescoAuthService', () => {
26+
let basicAlfrescoAuthService: BasicAlfrescoAuthService;
27+
28+
beforeEach(() => {
29+
TestBed.configureTestingModule({
30+
imports: [HttpClientTestingModule],
31+
providers: [BasicAlfrescoAuthService]
32+
});
33+
basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService);
34+
spyOn(TestBed.inject(ProcessAuth), 'getToken').and.returnValue('Mock Process Auth ticket');
35+
spyOn(TestBed.inject(ContentAuth), 'getToken').and.returnValue('Mock Content Auth ticket');
36+
const appConfigSpy = spyOn(TestBed.inject(AppConfigService), 'get');
37+
appConfigSpy.withArgs(AppConfigValues.CONTEXTROOTBPM).and.returnValue('activiti-app');
38+
appConfigSpy.withArgs(AppConfigValues.CONTEXTROOTECM).and.returnValue('alfresco');
39+
});
40+
41+
it('should return content services ticket when requestUrl contains ECM context root', () => {
42+
const ticket = basicAlfrescoAuthService.getTicketEcmBase64('http://www.exmple.com/alfresco/mock-api-url');
43+
const base64Segment = ticket.split('Basic ')[1];
44+
expect(atob(base64Segment)).toEqual('Mock Content Auth ticket');
45+
});
46+
47+
it('should return process services ticket when requestUrl contains ECM context root', () => {
48+
const ticket = basicAlfrescoAuthService.getTicketEcmBase64('http://www.example.com/activiti-app/mock-api-url');
49+
expect(ticket).toEqual('Basic Mock Process Auth ticket');
50+
});
51+
52+
it('should return content services ticket when requestUrl contains both ECM and BPM context root, but ECM context root comes before', () => {
53+
const ticket = basicAlfrescoAuthService.getTicketEcmBase64('http://www.exmple.com/alfresco/activiti-app/mock-api-url');
54+
const base64Segment = ticket.split('Basic ')[1];
55+
expect(atob(base64Segment)).toEqual('Mock Content Auth ticket');
56+
});
57+
58+
it('should return process services ticket when requestUrl contains both ECM and BPM context root, but BPM context root comes before', () => {
59+
const ticket = basicAlfrescoAuthService.getTicketEcmBase64('http://www.example.com/activiti-app/alfresco/mock-api-url');
60+
expect(ticket).toEqual('Basic Mock Process Auth ticket');
61+
});
62+
});

0 commit comments

Comments
 (0)