From 18a5197b5aaa69e522325c83c9805fc89b4cd5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Serwicki?= <55290524+mserwicki@users.noreply.github.com> Date: Mon, 24 Oct 2022 10:28:06 +0200 Subject: [PATCH] [ACS-3551] feat: add context to outgoing requests for auth intgerceptors (#7913) --- angular.json | 4 +- demo-shell/src/app/app.module.ts | 9 ++- .../alfresco-api/alfresco-api.http-client.ts | 5 +- lib/core/auth/README.md | 3 + lib/core/auth/ng-package.json | 5 ++ .../src/authentication-interceptor/README.md | 25 ++++++ .../authentication.interceptor.spec.ts | 66 ++++++++++++++++ .../authentication.interceptor.ts | 77 +++++++++++++++++++ lib/core/auth/src/authentication.ts | 23 ++++++ lib/core/auth/src/index.ts | 20 +++++ lib/core/src/lib/core.module.ts | 6 +- .../lib/services/auth-bearer.interceptor.ts | 21 ++--- .../lib/services/authentication.service.ts | 4 +- tsconfig.json | 1 + 14 files changed, 246 insertions(+), 23 deletions(-) create mode 100644 lib/core/auth/README.md create mode 100644 lib/core/auth/ng-package.json create mode 100644 lib/core/auth/src/authentication-interceptor/README.md create mode 100644 lib/core/auth/src/authentication-interceptor/authentication.interceptor.spec.ts create mode 100644 lib/core/auth/src/authentication-interceptor/authentication.interceptor.ts create mode 100644 lib/core/auth/src/authentication.ts create mode 100644 lib/core/auth/src/index.ts diff --git a/angular.json b/angular.json index e018b6a609..96764e41c8 100644 --- a/angular.json +++ b/angular.json @@ -337,7 +337,9 @@ "lib/core/**/*.ts", "lib/core/**/*.html", "lib/core/api/**/*.ts", - "lib/core/api/**/*.html" + "lib/core/api/**/*.html", + "lib/core/auth/**/*.ts", + "lib/core/auth/**/*.html" ] } }, diff --git a/demo-shell/src/app/app.module.ts b/demo-shell/src/app/app.module.ts index 63f9ccc4e4..c736763b8c 100644 --- a/demo-shell/src/app/app.module.ts +++ b/demo-shell/src/app/app.module.ts @@ -20,7 +20,7 @@ import { APP_INITIALIZER, NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FlexLayoutModule } from '@angular/flex-layout'; import { ChartsModule } from 'ng2-charts'; -import { HttpClientModule } from '@angular/common/http'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule } from '@ngx-translate/core'; import { @@ -28,7 +28,8 @@ import { TRANSLATION_PROVIDER, DebugAppConfigService, CoreModule, - CoreAutomationService + CoreAutomationService, + AuthBearerInterceptor } from '@alfresco/adf-core'; import { ExtensionsModule } from '@alfresco/adf-extensions'; import { AppComponent } from './app.component'; @@ -208,6 +209,10 @@ registerLocaleData(localeSv); SearchFilterChipsComponent ], providers: [ + { + provide: HTTP_INTERCEPTORS, useClass: + AuthBearerInterceptor, multi: true + }, { provide: AppConfigService, useClass: DebugAppConfigService }, // not use this service in production { provide: TRANSLATION_PROVIDER, diff --git a/lib/core/api/alfresco-api/alfresco-api.http-client.ts b/lib/core/api/alfresco-api/alfresco-api.http-client.ts index 4abf3cbcbd..e9841f2c3c 100644 --- a/lib/core/api/alfresco-api/alfresco-api.http-client.ts +++ b/lib/core/api/alfresco-api/alfresco-api.http-client.ts @@ -15,8 +15,9 @@ * limitations under the License. */ +import { SHOULD_ADD_AUTH_TOKEN } from '@alfresco/adf-core/auth'; import { Emitters as JsApiEmitters, HttpClient as JsApiHttpClient, RequestOptions, SecurityOptions, isBrowser } from '@alfresco/js-api'; -import { HttpClient, HttpErrorResponse, HttpEvent, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http'; +import { HttpClient, HttpContext, HttpErrorResponse, HttpEvent, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of, Subject, throwError } from 'rxjs'; import { catchError, map, takeUntil } from 'rxjs/operators'; @@ -37,11 +38,13 @@ export class AlfrescoApiHttpClient implements JsApiHttpClient { const params = getQueryParamsWithCustomEncoder(options.queryParams, new AlfrescoApiParamEncoder()); const headers = AlfrescoApiHttpClient.getHeaders(options); const responseType = AlfrescoApiHttpClient.getResponseType(options); + const context = new HttpContext().set(SHOULD_ADD_AUTH_TOKEN, true); const request = this.httpClient.request( options.httpMethod, url, { + context, ...(body && { body }), ...(responseType && { responseType }), ...(sc.withCredentials && { withCredentials: true }), diff --git a/lib/core/auth/README.md b/lib/core/auth/README.md new file mode 100644 index 0000000000..a33492ea53 --- /dev/null +++ b/lib/core/auth/README.md @@ -0,0 +1,3 @@ +# @alfresco/adf-core/auth + +Secondary entry point of `@alfresco/adf-core`. It can be used by importing from `@alfresco/adf-core/auth`. diff --git a/lib/core/auth/ng-package.json b/lib/core/auth/ng-package.json new file mode 100644 index 0000000000..c781f0df46 --- /dev/null +++ b/lib/core/auth/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/lib/core/auth/src/authentication-interceptor/README.md b/lib/core/auth/src/authentication-interceptor/README.md new file mode 100644 index 0000000000..169e3a63a9 --- /dev/null +++ b/lib/core/auth/src/authentication-interceptor/README.md @@ -0,0 +1,25 @@ +# AuthenticationInterceptor + +This interceptor is responsible for providing authentication to angular HttpClient requests when a context `SHOULD_ADD_AUTH_TOKEN` is set to true. +By default, the interceptor won't do anything to the intercepted request. + +## Usage + +```typescript +import { SHOULD_ADD_AUTH_TOKEN } from '@alfresco/adf-core/auth'; +import { HttpClient, HttpContext } from '@angular/common/http'; + +getSth() { + return this.httpClient.get('http://example.com', { context: new HttpContext().set(SHOULD_ADD_AUTH_TOKEN, true)}); +} + +// or + +getSth() { + const someRequest = this.httpClient.get('GET', 'http://example.com'); + someRequest.context.set(SHOULD_ADD_AUTH_TOKEN, true); + + return someRequest; +} + +``` diff --git a/lib/core/auth/src/authentication-interceptor/authentication.interceptor.spec.ts b/lib/core/auth/src/authentication-interceptor/authentication.interceptor.spec.ts new file mode 100644 index 0000000000..c1cdb5f815 --- /dev/null +++ b/lib/core/auth/src/authentication-interceptor/authentication.interceptor.spec.ts @@ -0,0 +1,66 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HttpHandler, HttpHeaders, HttpRequest } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { Observable, of } from 'rxjs'; +import { Authentication } from '../authentication'; +import { AuthenticationInterceptor, SHOULD_ADD_AUTH_TOKEN } from './authentication.interceptor'; + +class MockAuthentication extends Authentication { + addTokenToHeader(httpHeaders: HttpHeaders): Observable { + return of(httpHeaders); + } +} + +const mockNext: HttpHandler = { + handle: () => new Observable(subscriber => { + subscriber.complete(); + }) +}; + +const request = new HttpRequest('GET', 'http://localhost:4200'); + +describe('AuthenticationInterceptor', () => { + let interceptor: AuthenticationInterceptor; + let addTokenToHeaderSpy: jasmine.Spy; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [AuthenticationInterceptor, {provide: Authentication, useClass: MockAuthentication}] + }); + interceptor = TestBed.inject(AuthenticationInterceptor); + addTokenToHeaderSpy = spyOn(interceptor['authService'], 'addTokenToHeader'); + }); + + it('should call add auth token method when SHOULD_ADD_AUTH_TOKEN context is set to true', () => { + request.context.set(SHOULD_ADD_AUTH_TOKEN, true); + interceptor.intercept(request, mockNext); + expect(addTokenToHeaderSpy).toHaveBeenCalled(); + }); + + it('should not call add auth token method when SHOULD_ADD_AUTH_TOKEN context is set to false', () => { + request.context.set(SHOULD_ADD_AUTH_TOKEN, false); + interceptor.intercept(request, mockNext); + expect(addTokenToHeaderSpy).not.toHaveBeenCalled(); + }); + + it('should not call add auth token method when SHOULD_ADD_AUTH_TOKEN context is not provided', () => { + interceptor.intercept(request, mockNext); + expect(addTokenToHeaderSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/lib/core/auth/src/authentication-interceptor/authentication.interceptor.ts b/lib/core/auth/src/authentication-interceptor/authentication.interceptor.ts new file mode 100644 index 0000000000..fbf7cb04de --- /dev/null +++ b/lib/core/auth/src/authentication-interceptor/authentication.interceptor.ts @@ -0,0 +1,77 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + HttpContextToken, + HttpHandler, + HttpHeaderResponse, + HttpHeaders, + HttpInterceptor, + HttpProgressEvent, + HttpRequest, + HttpResponse, + HttpSentEvent, + HttpUserEvent +} from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, throwError as observableThrowError } from 'rxjs'; +import { catchError, mergeMap } from 'rxjs/operators'; +import { Authentication } from '../authentication'; + +export const SHOULD_ADD_AUTH_TOKEN = new HttpContextToken(() => false); + +@Injectable() +export class AuthenticationInterceptor implements HttpInterceptor { + + constructor( private authService: Authentication) { } + + intercept(req: HttpRequest, next: HttpHandler): + Observable | HttpUserEvent> { + + if (req.context.get(SHOULD_ADD_AUTH_TOKEN)) { + return this.authService.addTokenToHeader(req.headers).pipe( + mergeMap((headersWithBearer) => { + const headerWithContentType = this.appendJsonContentType(headersWithBearer); + const kcReq = req.clone({ headers: headerWithContentType}); + return next.handle(kcReq) + .pipe( + catchError((error) => observableThrowError(error)) + ); + }) + ); + } + + return next.handle(req).pipe(catchError((error) => observableThrowError(error))); + } + + private appendJsonContentType(headers: HttpHeaders): 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 + + if (headers.get('Content-Type') === 'multipart/form-data') { + return headers.delete('Content-Type'); + } + + if (!headers.get('Content-Type')) { + return headers.set('Content-Type', 'application/json;charset=UTF-8'); + } + + return headers; + } + +} diff --git a/lib/core/auth/src/authentication.ts b/lib/core/auth/src/authentication.ts new file mode 100644 index 0000000000..583f24f58f --- /dev/null +++ b/lib/core/auth/src/authentication.ts @@ -0,0 +1,23 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HttpHeaders } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +export abstract class Authentication { + public abstract addTokenToHeader(headers: HttpHeaders): Observable; +} diff --git a/lib/core/auth/src/index.ts b/lib/core/auth/src/index.ts new file mode 100644 index 0000000000..2858bcd070 --- /dev/null +++ b/lib/core/auth/src/index.ts @@ -0,0 +1,20 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './authentication'; +export * from './authentication-interceptor/authentication.interceptor'; + diff --git a/lib/core/src/lib/core.module.ts b/lib/core/src/lib/core.module.ts index 0efcd91e01..02535a3751 100644 --- a/lib/core/src/lib/core.module.ts +++ b/lib/core/src/lib/core.module.ts @@ -61,10 +61,11 @@ import { SearchTextModule } from './search-text/search-text-input.module'; import { versionCompatibilityFactory } from './services/version-compatibility-factory'; import { VersionCompatibilityService } from './services/version-compatibility.service'; import { AlfrescoJsClientsModule } from '@alfresco/adf-core/api'; +import { AuthenticationInterceptor, Authentication } from '@alfresco/adf-core/auth'; import { LegacyApiClientModule } from './api-factories/legacy-api-client.module'; import { RichTextEditorModule } from './rich-text-editor/rich-text-editor.module'; import { HttpClientModule, HttpClientXsrfModule, HTTP_INTERCEPTORS } from '@angular/common/http'; -import { AuthBearerInterceptor } from './services/auth-bearer.interceptor'; +import { AuthenticationService } from './services/authentication.service'; @NgModule({ imports: [ @@ -175,7 +176,8 @@ export class CoreModule { deps: [VersionCompatibilityService], multi: true }, - { provide: HTTP_INTERCEPTORS, useClass: AuthBearerInterceptor, multi: true } + { provide: HTTP_INTERCEPTORS, useClass: AuthenticationInterceptor, multi: true }, + { provide: Authentication, useClass: AuthenticationService } ] }; } diff --git a/lib/core/src/lib/services/auth-bearer.interceptor.ts b/lib/core/src/lib/services/auth-bearer.interceptor.ts index 3a763cea9c..9c82c2eecc 100644 --- a/lib/core/src/lib/services/auth-bearer.interceptor.ts +++ b/lib/core/src/lib/services/auth-bearer.interceptor.ts @@ -27,13 +27,14 @@ import { catchError, mergeMap } from 'rxjs/operators'; @Injectable() export class AuthBearerInterceptor implements HttpInterceptor { private excludedUrlsRegex: RegExp[]; + private authService: AuthenticationService; - constructor(private injector: Injector, private authService: AuthenticationService) { } + constructor(private injector: Injector) { } private loadExcludedUrlsRegex() { const excludedUrls: string[] = this.authService.getBearerExcludedUrls(); + this.excludedUrlsRegex = excludedUrls.map((urlPattern) => new RegExp(urlPattern, 'gi')) || []; - this.excludedUrlsRegex = [...excludedUrls].map((urlPattern) => new RegExp(urlPattern, 'i')) || []; } intercept(req: HttpRequest, next: HttpHandler): @@ -50,7 +51,7 @@ export class AuthBearerInterceptor implements HttpInterceptor { } const urlRequest = req.url; - const shallPass: boolean = this.excludedUrlsRegex.some((regex) => regex.test(urlRequest)); + const shallPass: boolean = !!this.excludedUrlsRegex.find((regex) => regex.test(urlRequest)); if (shallPass) { return next.handle(req) .pipe( @@ -72,19 +73,7 @@ export class AuthBearerInterceptor implements HttpInterceptor { } private appendJsonContentType(headers: HttpHeaders): 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 - - if (headers.get('Content-Type') === 'multipart/form-data') { - return headers.delete('Content-Type'); - } - - if (!headers.get('Content-Type')) { - return headers.set('Content-Type', 'application/json;charset=UTF-8'); - } - - return headers; + return headers.set('Content-Type', 'application/json;charset=UTF-8'); } } diff --git a/lib/core/src/lib/services/authentication.service.ts b/lib/core/src/lib/services/authentication.service.ts index 2f8277286c..a33cdb24e9 100644 --- a/lib/core/src/lib/services/authentication.service.ts +++ b/lib/core/src/lib/services/authentication.service.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { Authentication } from '@alfresco/adf-core/auth'; import { Injectable } from '@angular/core'; import { Observable, from, throwError, Observer, ReplaySubject, forkJoin } from 'rxjs'; import { AlfrescoApiService } from './alfresco-api.service'; @@ -34,7 +35,7 @@ const REMEMBER_ME_UNTIL = 1000 * 60 * 60 * 24 * 30; @Injectable({ providedIn: 'root' }) -export class AuthenticationService { +export class AuthenticationService extends Authentication { private redirectUrl: RedirectionModel = null; private bearerExcludedUrls: string[] = ['auth/realms', 'resources/', 'assets/']; @@ -66,6 +67,7 @@ export class AuthenticationService { private alfrescoApi: AlfrescoApiService, private cookie: CookieService, private logService: LogService) { + super(); this.alfrescoApi.alfrescoApiInitialized.subscribe(() => { this.alfrescoApi.getInstance().reply('logged-in', () => { this.onLogin.next(); diff --git a/tsconfig.json b/tsconfig.json index 9126acce5f..ee21efd17d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,7 @@ "@alfresco/adf-content-services": ["lib/content-services"], "@alfresco/adf-core": ["lib/core"], "@alfresco/adf-core/*": ["lib/core/*/public-api.ts"], + "@alfresco/adf-core/auth": ["lib/core/auth/src/index.ts"], "@alfresco/adf-extensions": ["lib/extensions"], "@alfresco/adf-insights": ["lib/insights"], "@alfresco/adf-process-services": ["lib/process-services"],