diff --git a/demo-shell/src/app/app.component.ts b/demo-shell/src/app/app.component.ts index 416e56d109..2273758e1d 100644 --- a/demo-shell/src/app/app.component.ts +++ b/demo-shell/src/app/app.component.ts @@ -18,11 +18,11 @@ import { Component, ViewEncapsulation, OnInit } from '@angular/core'; import { AuthenticationService, - AlfrescoApiService, PageTitleService } from '@alfresco/adf-core'; import { Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; +import { AdfHttpClient } from '@alfresco/adf-core/api'; @Component({ selector: 'app-root', @@ -33,7 +33,7 @@ import { MatDialog } from '@angular/material/dialog'; export class AppComponent implements OnInit { constructor(private pageTitleService: PageTitleService, - private alfrescoApiService: AlfrescoApiService, + private adfHttpClient: AdfHttpClient, private authenticationService: AuthenticationService, private router: Router, private dialogRef: MatDialog) { @@ -43,7 +43,7 @@ export class AppComponent implements OnInit { ngOnInit() { this.pageTitleService.setTitle('title'); - this.alfrescoApiService.getInstance().on('error', (error) => { + this.adfHttpClient.on('error', (error) => { if (error.status === 401) { if (!this.authenticationService.isLoggedIn()) { this.dialogRef.closeAll(); diff --git a/demo-shell/src/app/components/app-layout/user-info/user-info.component.ts b/demo-shell/src/app/components/app-layout/user-info/user-info.component.ts index 4d29c42294..894e4ba329 100644 --- a/demo-shell/src/app/components/app-layout/user-info/user-info.component.ts +++ b/demo-shell/src/app/components/app-layout/user-info/user-info.component.ts @@ -17,7 +17,13 @@ import { EcmUserModel, PeopleContentService } from '@alfresco/adf-content-services'; import { BpmUserModel, PeopleProcessService } from '@alfresco/adf-process-services'; -import { AuthenticationService, IdentityUserModel, IdentityUserService, UserInfoMode } from '@alfresco/adf-core'; +import { + AuthenticationService, + BasicAlfrescoAuthService, + IdentityUserModel, + IdentityUserService, + UserInfoMode +} from '@alfresco/adf-core'; import { Component, OnInit, Input } from '@angular/core'; import { MenuPositionX, MenuPositionY } from '@angular/material/menu'; import { Observable, of } from 'rxjs'; @@ -46,6 +52,7 @@ export class UserInfoComponent implements OnInit { constructor(private peopleContentService: PeopleContentService, private peopleProcessService: PeopleProcessService, private identityUserService: IdentityUserService, + private basicAlfrescoAuthService: BasicAlfrescoAuthService, private authService: AuthenticationService) { } @@ -77,7 +84,7 @@ export class UserInfoComponent implements OnInit { } get isLoggedIn(): boolean { - if (this.authService.isKerberosEnabled()) { + if (this.basicAlfrescoAuthService.isKerberosEnabled()) { return true; } return this.authService.isLoggedIn(); @@ -96,15 +103,15 @@ export class UserInfoComponent implements OnInit { } private isAllLoggedIn() { - return (this.authService.isEcmLoggedIn() && this.authService.isBpmLoggedIn()) || (this.authService.isALLProvider() && this.authService.isKerberosEnabled()); + return (this.authService.isEcmLoggedIn() && this.authService.isBpmLoggedIn()) || (this.authService.isALLProvider() && this.basicAlfrescoAuthService.isKerberosEnabled()); } private isBpmLoggedIn() { - return this.authService.isBpmLoggedIn() || (this.authService.isECMProvider() && this.authService.isKerberosEnabled()); + return this.authService.isBpmLoggedIn() || (this.authService.isECMProvider() && this.basicAlfrescoAuthService.isKerberosEnabled()); } private isEcmLoggedIn() { - return this.authService.isEcmLoggedIn() || (this.authService.isECMProvider() && this.authService.isKerberosEnabled()); + return this.authService.isEcmLoggedIn() || (this.authService.isECMProvider() && this.basicAlfrescoAuthService.isKerberosEnabled()); } } diff --git a/demo-shell/src/app/components/settings/host-settings.component.ts b/demo-shell/src/app/components/settings/host-settings.component.ts index 083a285a36..293c4d759c 100644 --- a/demo-shell/src/app/components/settings/host-settings.component.ts +++ b/demo-shell/src/app/components/settings/host-settings.component.ts @@ -65,7 +65,7 @@ export class HostSettingsComponent implements OnInit { private storageService: StorageService, private alfrescoApiService: AlfrescoApiService, private appConfig: AppConfigService, - private auth: AuthenticationService + private authenticationService: AuthenticationService ) {} ngOnInit() { @@ -191,7 +191,7 @@ export class HostSettingsComponent implements OnInit { this.storageService.setItem(AppConfigValues.AUTHTYPE, values.authType); this.alfrescoApiService.reset(); - this.auth.reset(); + this.authenticationService.reset(); this.alfrescoApiService.getInstance().invalidateSession(); this.success.emit(true); } @@ -235,10 +235,6 @@ export class HostSettingsComponent implements OnInit { return this.form.get('authType').value === 'OAUTH'; } - get supportsCodeFlow(): boolean { - return this.auth.supportCodeFlow; - } - get providersControl(): UntypedFormControl { return this.form.get('providersControl') as UntypedFormControl; } diff --git a/lib/core/api/src/lib/adf-http-client.service.ts b/lib/core/api/src/lib/adf-http-client.service.ts index 6f35384ff3..edd3a40672 100644 --- a/lib/core/api/src/lib/adf-http-client.service.ts +++ b/lib/core/api/src/lib/adf-http-client.service.ts @@ -42,7 +42,9 @@ import { AlfrescoApiParamEncoder } from './alfresco-api/alfresco-api.param-encod import { AlfrescoApiResponseError } from './alfresco-api/alfresco-api.response-error'; import { Constructor } from './types'; import { RequestOptions, SecurityOptions } from './interfaces'; -import ee, { Emitter } from 'event-emitter'; +import { AppConfigService, AppConfigValues } from '../../../src/lib/app-config/app-config.service'; +import ee from 'event-emitter'; +import { Emitter } from 'event-emitter'; export interface Emitters { readonly eventEmitter: Emitter; @@ -59,15 +61,6 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient { once: ee.EmitterMethod; emit: (type: string, ...args: any[]) => void; - private _disableCsrf = false; - - private defaultSecurityOptions = { - withCredentials: true, - isBpmRequest: false, - authentications: {}, - defaultHeaders: {} - }; - get disableCsrf(): boolean { return this._disableCsrf; } @@ -75,9 +68,15 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient { set disableCsrf(disableCsrf: boolean) { this._disableCsrf = disableCsrf; } + + private defaultSecurityOptions = { + withCredentials: true, + isBpmRequest: false, + authentications: {}, + defaultHeaders: {} + }; - constructor(private httpClient: HttpClient - ) { + constructor(private httpClient: HttpClient, private appConfig: AppConfigService) { ee(this); } @@ -237,7 +236,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient { takeUntil(abort$) ).toPromise(); - (promise as any).abort = function() { + (promise as any).abort = function () { eventEmitter.emit('abort'); abort$.next(); abort$.complete(); @@ -271,7 +270,9 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient { ...((options.contentType) && {'Content-Type': options.contentType}) }; - if (!this.disableCsrf) { + const disableCsrf = this.appConfig.get(AppConfigValues.DISABLECSRF); + + if (!disableCsrf) { this.setCsrfToken(optionsHeaders); } @@ -291,8 +292,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient { } private createCSRFToken(a?: any): string { - const randomValue = window.crypto.getRandomValues(new Uint32Array(1))[0]; - return a ? (a ^ ((randomValue * 16) >> (a / 4))).toString(16) : ([1e16] + (1e16).toString()).replace(/[01]/g, this.createCSRFToken); + return a ? (a ^ ((Math.random() * 16) >> (a / 4))).toString(16) : ([1e16] + (1e16).toString()).replace(/[01]/g, this.createCSRFToken); } private static getResponseType(options: RequestOptions): 'blob' | 'json' | 'text' { diff --git a/lib/core/api/src/lib/interfaces.ts b/lib/core/api/src/lib/interfaces.ts index a4e3d043e3..94c5533ed3 100644 --- a/lib/core/api/src/lib/interfaces.ts +++ b/lib/core/api/src/lib/interfaces.ts @@ -15,6 +15,32 @@ * limitations under the License. */ +export interface SecurityOptions { + // readonly isBpmRequest: boolean; + // readonly enableCsrf?: boolean; + readonly withCredentials?: boolean; + readonly authentications?: Authentication; + readonly defaultHeaders?: Record; +} + +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; queryParams?: any; @@ -23,14 +49,6 @@ export interface RequestOptions { bodyParam?: any; returnType?: any; responseType?: 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; + accept?: string; + contentType?: string; } diff --git a/lib/core/src/lib/api-factories/alfresco-api-no-auth.service.ts b/lib/core/src/lib/api-factories/alfresco-api-no-auth.service.ts index 61aad41b2f..162b5a0505 100644 --- a/lib/core/src/lib/api-factories/alfresco-api-no-auth.service.ts +++ b/lib/core/src/lib/api-factories/alfresco-api-no-auth.service.ts @@ -34,8 +34,7 @@ export class AlfrescoApiNoAuthService extends AlfrescoApiService { override createInstance(config: AlfrescoApiConfig) { return new AlfrescoApi({ - ...config, - oauthInit: false + ...config }, this.adfHttpClient); } } diff --git a/lib/core/src/lib/app-config/app-config.service.ts b/lib/core/src/lib/app-config/app-config.service.ts index b3e3112448..b059e39b0e 100644 --- a/lib/core/src/lib/app-config/app-config.service.ts +++ b/lib/core/src/lib/app-config/app-config.service.ts @@ -25,6 +25,7 @@ import { OpenidConfiguration } from '../auth/interfaces/openid-configuration.int import { OauthConfigModel } from '../auth/models/oauth-config.model'; /* spellchecker: disable */ + // eslint-disable-next-line no-shadow export enum AppConfigValues { APP_CONFIG_LANGUAGES_KEY = 'languages', @@ -74,6 +75,10 @@ export class AppConfigService { protected onLoadSubject: Subject; onLoad: Observable; + get isLoaded() { + return this.status = Status.LOADED; + } + constructor(protected http: HttpClient, protected extensionService: ExtensionService) { this.onLoadSubject = new Subject(); this.onLoad = this.onLoadSubject.asObservable(); @@ -217,7 +222,7 @@ export class AppConfigService { * * @returns Discovery configuration */ - loadWellKnown(hostIdp: string): Promise { + loadWellKnown(hostIdp: string): Promise { return new Promise(async (resolve, reject) => { this.http .get(`${hostIdp}/.well-known/openid-configuration`) @@ -259,4 +264,5 @@ export class AppConfigService { return result; } + } diff --git a/lib/core/src/lib/auth/authentication-interceptor/auth-bearer.interceptor.ts b/lib/core/src/lib/auth/authentication-interceptor/auth-bearer.interceptor.ts index e56e9dc297..c3678c1701 100644 --- a/lib/core/src/lib/auth/authentication-interceptor/auth-bearer.interceptor.ts +++ b/lib/core/src/lib/auth/authentication-interceptor/auth-bearer.interceptor.ts @@ -16,40 +16,36 @@ */ 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[]; + protected 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(); - this.excludedUrlsRegex = excludedUrls.map((urlPattern) => new RegExp(urlPattern, 'i')) || []; + this.excludedUrlsRegex = this.bearerExcludedUrls.map((urlPattern) => new RegExp(urlPattern, 'i')) || []; } intercept(req: HttpRequest, next: HttpHandler): Observable | HttpUserEvent> { - this.authService = this.injector.get(AuthenticationService); - - if (!this.authService || !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,7 +53,7 @@ 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); diff --git a/lib/core/src/lib/auth/basic-auth/basic-alfresco-auth.service.ts b/lib/core/src/lib/auth/basic-auth/basic-alfresco-auth.service.ts new file mode 100644 index 0000000000..8e164bdee1 --- /dev/null +++ b/lib/core/src/lib/auth/basic-auth/basic-alfresco-auth.service.ts @@ -0,0 +1,333 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service'; +import { Authentication } from '../interfaces/authentication.interface'; +import { CookieService } from '../../common/services/cookie.service'; +import { ContentAuth } from './contentAuth'; +import { ProcessAuth } from './processAuth'; +import { map } from 'rxjs/operators'; +import { from, Observable } from 'rxjs'; +import { RedirectionModel } from "../index"; +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.contentAuth.onLogout.pipe(map((event) => { + this.onLogout.next(event) + })); + this.contentAuth.onLogin.pipe(map((event) => { + this.onLogout.next(event) + })); + this.contentAuth.onError.pipe(map((event) => { + this.onLogout.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 + }; + }) + ); + } + + /** + * login Alfresco API + * @param username: // Username to login + * @param password: // Password to login + * + * @returns {Promise} A promise that returns {new authentication ticket} if resolved and {error} if rejected. + * */ + async executeLogin(username: string, password: string): Promise { + if (!this.isCredentialValid(username) || !this.isCredentialValid(password)) { + return Promise.reject('missing username or password'); + } + + if (username) { + username = username.trim(); + } + + if (this.isBPMProvider()) { + try { + return await this.processAuth.login(username, password); + } catch (e) { + console.log('login BPM error'); + } + + } else if (this.isECMProvider()) { + try { + return await this.contentAuth.login(username, password); + } catch (e) { + console.log('login BPM error'); + } + + } else if (this.isALLProvider()) { + return this.loginBPMECM(username, password); + } else { + return Promise.reject('Unknown configuration'); + } + + } + + private loginBPMECM(username: string, password: string): Promise { + 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 { + return ""; + } + + 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 + * */ + logout(): Promise { + if (this.isBPMProvider()) { + return this.processAuth.logout(); + } else if (this.isECMProvider()) { + const contentPromise = this.contentAuth.logout(); + contentPromise.then( + () => this.contentAuth.ticket = undefined, + () => { + } + ); + return contentPromise; + } else if (this.isALLProvider()) { + return this.logoutBPMECM(); + } + return Promise.resolve(); + } + + private logoutBPMECM(): Promise { + 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(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.contentAuth.authentications.basicAuth.username; + } + + getEcmUsername(): string { + return this.processAuth.authentications.basicAuth.username; + } + + /** + * Does kerberos enabled? + * + * @returns True if enabled, false otherwise + */ + isKerberosEnabled(): boolean { + return this.appConfig.get(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); + } + + + /** + * Gets the BPM ticket from the Storage in Base 64 format. + * + * @returns The ticket or `null` if none was found + */ + private getTicketEcmBase64(requestUrl: string): string | null { + let ticket = null; + + const contextRootBpm = this.appConfig.get(AppConfigValues.CONTEXTROOTBPM); + const contextRoot = this.appConfig.get(AppConfigValues.CONTEXTROOTECM); + + if (contextRoot && requestUrl.indexOf(contextRoot) !== -1) { + ticket = 'Basic ' + btoa(this.contentAuth.getTicket()); + } else if (contextRootBpm && requestUrl.indexOf(contextRootBpm) !== -1) { + ticket = 'Basic ' + this.processAuth.getTicket(); + } + + return ticket; + } +} diff --git a/lib/core/src/lib/auth/basic-auth/contentAuth.ts b/lib/core/src/lib/auth/basic-auth/contentAuth.ts new file mode 100644 index 0000000000..53b42c3bf0 --- /dev/null +++ b/lib/core/src/lib/auth/basic-auth/contentAuth.ts @@ -0,0 +1,221 @@ +/*! +* @license +* Copyright 2018 Alfresco Software, Ltd. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { Injectable } from '@angular/core'; +import { 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; + }; +} + +export const CONTENT_TICKET_STORAGE_LABEL = 'ticket-ECM'; + +@Injectable({ + providedIn: 'root' +}) +export class ContentAuth { + + onLogin = new ReplaySubject(1); + onLogout = new ReplaySubject(1); + onError = new Subject(); + + ticket: string; + config = { + ticketEcm: null + }; + + authentications: Authentication = { + basicAuth: { + ticket: '', + }, + type: 'basic', + }; + + get basePath(): string { + const contextRootEcm = this.appConfigService.get(AppConfigValues.CONTEXTROOTECM) || 'alfresco'; + return this.appConfigService.get(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(CONTENT_TICKET_STORAGE_LABEL)) { + this.setTicket(this.storageService.getItem(CONTENT_TICKET_STORAGE_LABEL)); + } + + } + + saveUsername(username: string) { + this.storageService.setItem('ACS_USERNAME', 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 { + this.authentications.basicAuth.username = username; + this.authentications.basicAuth.password = password; + + let 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'); + this.onError.next('unauthorized'); + } else if (error.status === 403) { + this.adfHttpClient.emit('forbidden'); + this.onError.next('forbidden'); + } else { + this.adfHttpClient.emit('error'); + this.onError.next('error'); + } + reject(error); + }); + }); + } + + /** + * logout Alfresco API + * + * @returns {Promise} A promise that returns { authentication ticket} if resolved and {error} if rejected. + * */ + logout(): Promise { + this.saveUsername(''); + return new Promise((resolve, reject) => { + this.deleteTicket().then( + () => { + this.adfHttpClient.emit('logout'); + this.onLogout.next('logout'); + this.invalidateSession(); + 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 + * */ + setTicket(ticket: string) { + this.authentications.basicAuth.username = 'ROLE_TICKET'; + this.authentications.basicAuth.password = ticket; + this.config.ticketEcm = ticket; + this.storageService.setItem(CONTENT_TICKET_STORAGE_LABEL, ticket); + this.ticket = ticket; + } + + /** + * Get the current Ticket + * */ + getTicket(): string { + if(!this.ticket){ + this.onError.next('error'); + } + + return this.ticket; + } + + invalidateSession() { + this.storageService.removeItem(CONTENT_TICKET_STORAGE_LABEL); + this.authentications.basicAuth.username = null; + this.authentications.basicAuth.password = null; + this.config.ticketEcm = null; + this.ticket = null; + } + + /** + * If the client is logged in return true + */ + isLoggedIn(): boolean { + return !!this.ticket; + } + + /** + * return the Authentication + * */ + getAuthentication() { + return this.authentications; + } + + createTicket(ticketBodyCreate: TicketBody): Promise { + if (ticketBodyCreate === null || ticketBodyCreate === undefined) { + this.onError.next((`Missing param ticketBodyCreate`)); + + throw new Error(`Missing param '${name}'`); + } + + return this.adfHttpClient.post(this.basePath + '/tickets', {bodyParam: ticketBodyCreate}); + } + + /** + * Delete ticket (logout) + * + * **Note:** this endpoint is available in Alfresco 5.2 and newer versions. + + Deletes logged in ticket (logout). + + * + * @return Promise<{}> + */ + deleteTicket(): Promise { + return this.adfHttpClient.delete(this.basePath + '/tickets/-me-'); + } + +} diff --git a/lib/core/src/lib/auth/basic-auth/processAuth.ts b/lib/core/src/lib/auth/basic-auth/processAuth.ts new file mode 100644 index 0000000000..783025ee8d --- /dev/null +++ b/lib/core/src/lib/auth/basic-auth/processAuth.ts @@ -0,0 +1,206 @@ +/*! +* @license +* Copyright 2018 Alfresco Software, Ltd. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { Injectable } from '@angular/core'; +import { 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'; + +export const PROCESS_TICKET_STORAGE_LABEL = 'ticket-BPM'; + +@Injectable({ + providedIn: 'root' +}) +export class ProcessAuth { + + onLogin = new ReplaySubject(1); + onLogout = new ReplaySubject(1); + onError = new Subject(); + + ticket: string; + config = { + ticketBpm: null + }; + + authentications: Authentication = { + basicAuth: {ticket: ''}, type: 'activiti' + }; + + get basePath(): string { + const contextRootBpm = this.appConfigService.get(AppConfigValues.CONTEXTROOTBPM) || 'activiti-app'; + return this.appConfigService.get(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(PROCESS_TICKET_STORAGE_LABEL)); + } + + saveUsername(username: string) { + this.storageService.setItem('APS_USERNAME', 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 { + 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', + }; + + let promise: any = new Promise((resolve, reject) => { + this.adfHttpClient.post(this.basePath + '/app/authentication', options).then( + () => { + this.saveUsername(username); + let 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'); + this.onError.next('unauthorized'); + } else if (error.status === 403) { + this.adfHttpClient.emit('forbidden'); + this.onError.next('forbidden'); + } else { + this.adfHttpClient.emit('error'); + this.onError.next('error'); + } + reject(error); + }); + }); + + return promise; + } + + /** + * logout Alfresco API + * + * @returns {Promise} A promise that returns {new authentication ticket} if resolved and {error} if rejected. + * */ + async logout(): Promise { + 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 base64; + } + + /** + * Set the current Ticket + * + * @param 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(PROCESS_TICKET_STORAGE_LABEL, ticket); + this.ticket = ticket; + } + } + + invalidateSession() { + this.storageService.removeItem(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; + } + + /** + * Get the current Ticket + * */ + getTicket(): string { + if(!this.ticket){ + this.onError.next('error'); + return null; + } + + return this.ticket; + } + + /** + * If the client is logged in return true + */ + isLoggedIn(): boolean { + return !!this.ticket; + } +} diff --git a/lib/core/src/lib/auth/guard/auth-guard-base.ts b/lib/core/src/lib/auth/guard/auth-guard-base.ts index 0a1c1a874b..1edab48061 100644 --- a/lib/core/src/lib/auth/guard/auth-guard-base.ts +++ b/lib/core/src/lib/auth/guard/auth-guard-base.ts @@ -1,6 +1,6 @@ /*! * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * 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. @@ -24,19 +24,18 @@ import { 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( @@ -45,6 +44,17 @@ export abstract class AuthGuardBase implements CanActivate, CanActivateChild { ); } + 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 @@ -89,15 +99,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); } @@ -112,7 +124,13 @@ export abstract class AuthGuardBase implements CanActivate, CanActivateChild { } protected getOauthConfig(): OauthConfigModel { - return this.appConfigService.oauth2; + return ( + this.appConfigService && + this.appConfigService.get( + AppConfigValues.OAUTHCONFIG, + null + ) + ); } protected getLoginRoute(): string { @@ -136,12 +154,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( + AppConfigValues.OAUTHCONFIG, + null + ); + return ( + this.authenticationService.isOauth() && !!oauth && !oauth.silentLogin + ); } protected isSilentLogin(): boolean { - const oauth = this.appConfigService.oauth2;; + const oauth = this.appConfigService.get( + AppConfigValues.OAUTHCONFIG, + null + ); + return this.authenticationService.isOauth() && oauth && oauth.silentLogin; } diff --git a/lib/core/src/lib/auth/guard/auth-guard-bpm.service.ts b/lib/core/src/lib/auth/guard/auth-guard-bpm.service.ts index 4d511832a9..fc5df2870b 100644 --- a/lib/core/src/lib/auth/guard/auth-guard-bpm.service.ts +++ b/lib/core/src/lib/auth/guard/auth-guard-bpm.service.ts @@ -1,6 +1,6 @@ /*! * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * 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. @@ -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 { if (this.authenticationService.isBpmLoggedIn() || this.withCredentials) { return true; diff --git a/lib/core/src/lib/auth/guard/auth-guard-ecm.service.ts b/lib/core/src/lib/auth/guard/auth-guard-ecm.service.ts index 0154b91ec7..05b0ac23c2 100644 --- a/lib/core/src/lib/auth/guard/auth-guard-ecm.service.ts +++ b/lib/core/src/lib/auth/guard/auth-guard-ecm.service.ts @@ -1,6 +1,6 @@ /*! * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * 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. @@ -16,13 +16,32 @@ */ 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 { if (this.authenticationService.isEcmLoggedIn() || this.withCredentials) { return true; diff --git a/lib/core/src/lib/auth/guard/auth-guard.service.ts b/lib/core/src/lib/auth/guard/auth-guard.service.ts index 9f6b1897d4..e8d3848bcb 100644 --- a/lib/core/src/lib/auth/guard/auth-guard.service.ts +++ b/lib/core/src/lib/auth/guard/auth-guard.service.ts @@ -1,6 +1,6 @@ /*! * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * 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. @@ -16,9 +16,15 @@ */ 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 +33,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); diff --git a/lib/core/src/lib/auth/interfaces/authentication-service.interface.ts b/lib/core/src/lib/auth/interfaces/authentication-service.interface.ts new file mode 100644 index 0000000000..7af8b43d3f --- /dev/null +++ b/lib/core/src/lib/auth/interfaces/authentication-service.interface.ts @@ -0,0 +1,57 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HttpHeaders } from '@angular/common/http'; +import ee from 'event-emitter'; + +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; + + reset(): void; +} diff --git a/lib/core/src/lib/auth/interfaces/authentication.interface.ts b/lib/core/src/lib/auth/interfaces/authentication.interface.ts new file mode 100644 index 0000000000..649b7f9895 --- /dev/null +++ b/lib/core/src/lib/auth/interfaces/authentication.interface.ts @@ -0,0 +1,28 @@ +/*! + * @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 interface Authentication { + basicAuth?: BasicAuth; + cookie?: string; + type?: string; +} + +export interface BasicAuth { + username?: string; + password?: string; + ticket?: string; +} diff --git a/lib/core/src/lib/auth/oidc/auth.module.ts b/lib/core/src/lib/auth/oidc/auth.module.ts index bf2a6fa550..fd57178da6 100644 --- a/lib/core/src/lib/auth/oidc/auth.module.ts +++ b/lib/core/src/lib/auth/oidc/auth.module.ts @@ -19,20 +19,16 @@ 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'; + export function loginFactory(oAuthService: OAuthService, storage: OAuthStorage, config: AuthConfig) { const service = new RedirectAuthService(oAuthService, storage, config); return () => service.init(); @@ -43,10 +39,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, diff --git a/lib/core/src/lib/auth/oidc/oidc-authentication.service.ts b/lib/core/src/lib/auth/oidc/oidc-authentication.service.ts deleted file mode 100644 index 5290e6a356..0000000000 --- a/lib/core/src/lib/auth/oidc/oidc-authentication.service.ts +++ /dev/null @@ -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 { - return this.oauthService.events.pipe(filter(_event => _event.type === event)); - } -} diff --git a/lib/core/src/lib/auth/public-api.ts b/lib/core/src/lib/auth/public-api.ts index 59454dbb3a..91d2212512 100644 --- a/lib/core/src/lib/auth/public-api.ts +++ b/lib/core/src/lib/auth/public-api.ts @@ -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/processAuth'; +export * from './basic-auth/contentAuth'; + export * from './interfaces/identity-user.service.interface'; export * from './interfaces/identity-group.interface'; export * from './interfaces/openid-configuration.interface'; diff --git a/lib/core/src/lib/auth/services/authentication.service.ts b/lib/core/src/lib/auth/services/authentication.service.ts index b7d00256d9..19389cacdb 100644 --- a/lib/core/src/lib/auth/services/authentication.service.ts +++ b/lib/core/src/lib/auth/services/authentication.service.ts @@ -1,6 +1,6 @@ /*! * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * 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. @@ -15,184 +15,104 @@ * 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 { Injectable, Injector } from '@angular/core'; +import { OidcAuthenticationService } from './oidc-authentication.service'; +import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service'; +import { Observable, of } from 'rxjs'; import { BaseAuthenticationService } from './base-authentication.service'; +import { AppConfigService } from '../../app-config'; +import { CookieService, LogService } from '../../common'; +import { HttpHeaders } from '@angular/common/http'; @Injectable({ providedIn: 'root' }) export class AuthenticationService extends BaseAuthenticationService { - private storageService = inject(StorageService); - readonly supportCodeFlow = false; - constructor() { - super(); - this.alfrescoApi.alfrescoApiInitialized.subscribe(() => { - this.alfrescoApi.getInstance().reply('logged-in', () => { - this.onLogin.next(); - }); - }); + constructor(appConfig: AppConfigService, + cookie: CookieService, + logService: LogService, + private injector: Injector) { + super(appConfig, cookie, logService); } - /** - * 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(); + private get oidcAuthenticationService(): OidcAuthenticationService { + return this.injector.get(OidcAuthenticationService); } - isLoggedInWith(provider: string): boolean { - if (provider === 'BPM') { - return this.isBpmLoggedIn(); - } else if (provider === 'ECM') { - return this.isEcmLoggedIn(); - } else { - return this.isLoggedIn(); - } + private get basicAlfrescoAuthService(): BasicAlfrescoAuthService { + return this.injector.get(BasicAlfrescoAuthService); } - /** - * Does the provider support OAuth? - * - * @returns True if supported, false otherwise - */ - isOauth(): boolean { - return this.alfrescoApi.getInstance().isOauthConfiguration(); - } - - /** - * 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)) - ); - } - - /** - * Logs the user in with SSO - */ - ssoImplicitLogin() { - this.alfrescoApi.getInstance().implicitLogin(); - } - - - /** - * 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)) - ); - } - - private callApiLogout(): Promise { - if (this.alfrescoApi.getInstance()) { - return this.alfrescoApi.getInstance().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(); - } - 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(); - } - return false; - } - - /** - * Gets the ECM username. - * - * @returns The ECM username - */ - getEcmUsername(): string { - return this.alfrescoApi.getInstance().getEcmUsername(); - } - - /** - * Gets the BPM username - * - * @returns The BPM username - */ - getBpmUsername(): string { - return this.alfrescoApi.getInstance().getBpmUsername(); - } - - isImplicitFlow(): boolean { - return !!this.appConfig.oauth2?.implicitFlow; - } - - isAuthCodeFlow(): boolean { - return false; - } - - /** - * Gets the auth token. - * - * @returns Auth token string - */ getToken(): string { - return this.storageService.getItem(JwtHelperService.USER_ACCESS_TOKEN); + if (this.isOauth()) { + return this.oidcAuthenticationService.getToken(); + } else { + return this.basicAlfrescoAuthService.getToken(); + } } - reset() { } + isLoggedIn(): boolean { + if (this.isOauth()) { + return this.oidcAuthenticationService.isLoggedIn(); + } else { + return this.basicAlfrescoAuthService.isLoggedIn(); + } + } - once(event: string): Observable { - const alfrescoApiEvent = event === 'token_received' ? 'token_issued' : event; - return new Observable((subscriber) => { - this.alfrescoApi.getInstance().oauth2Auth.once(alfrescoApiEvent, () => subscriber.next()); - }); + logout(): Observable { + if (this.isOauth()) { + return this.oidcAuthenticationService.logout(); + } else { + return of(this.basicAlfrescoAuthService.logout()); + } + } + + isEcmLoggedIn(): boolean { + if (this.isOauth()) { + return this.oidcAuthenticationService.isEcmLoggedIn(); + } else { + return this.basicAlfrescoAuthService.isEcmLoggedIn(); + } + } + + isBpmLoggedIn(): boolean { + 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(); + } + } + + getEcmUsername(): string { + if (this.isOauth()) { + return this.oidcAuthenticationService.getEcmUsername(); + } else { + return this.basicAlfrescoAuthService.getEcmUsername(); + } + } + + getBpmUsername(): string { + if (this.isOauth()) { + return this.oidcAuthenticationService.getBpmUsername(); + } else { + return this.basicAlfrescoAuthService.getBpmUsername(); + } + } + + getAuthHeaders(requestUrl: string, headers: HttpHeaders): HttpHeaders { + if (this.isOauth()) { + return this.oidcAuthenticationService.getAuthHeaders(requestUrl, headers); + } else { + return this.basicAlfrescoAuthService.getAuthHeaders(requestUrl, headers); + } } } diff --git a/lib/core/src/lib/auth/services/base-authentication.service.ts b/lib/core/src/lib/auth/services/base-authentication.service.ts index b79b60c36c..79fcc46f55 100644 --- a/lib/core/src/lib/auth/services/base-authentication.service.ts +++ b/lib/core/src/lib/auth/services/base-authentication.service.ts @@ -1,6 +1,6 @@ /*! * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * 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. @@ -15,61 +15,53 @@ * 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(1); onLogin = new ReplaySubject(1); onLogout = new ReplaySubject(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; - abstract isEcmLoggedIn(): boolean; - abstract isBpmLoggedIn(): boolean; - abstract getEcmUsername(): string; - abstract getBpmUsername(): string; - abstract reset(): void; - abstract once(event: string): Observable; - 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. @@ -77,14 +69,15 @@ export abstract class BaseAuthenticationService { * @param headersArg Header that will receive the token * @returns The new header with the token added */ - addTokenToHeader(headersArg?: HttpHeaders): Observable { + addTokenToHeader(requestUrl: string, headersArg?: HttpHeaders): Observable { return new Observable((observer: Observer) => { 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 +87,9 @@ export abstract class BaseAuthenticationService { }); } - private getAuthHeaders(header: HttpHeaders): HttpHeaders { - const authType = this.appConfig.get(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'); + return provider && provider.toUpperCase() === 'ECM'; } /** @@ -146,7 +98,8 @@ export abstract class BaseAuthenticationService { * @returns True if supported, false otherwise */ isBPMProvider(): boolean { - return this.alfrescoApi.getInstance().isBpmConfiguration(); + const provider = this.appConfig.get('providers'); + return provider && provider.toUpperCase() === 'BPM'; } /** @@ -155,38 +108,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'); + 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'); + return authType === 'OAUTH'; } /** @@ -196,63 +124,12 @@ export abstract class BaseAuthenticationService { * @returns Object representing the error message */ handleError(error: any): Observable { + 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(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(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'; } } diff --git a/lib/core/src/lib/auth/services/legacy-authentication.service.ts b/lib/core/src/lib/auth/services/legacy-authentication.service.ts new file mode 100644 index 0000000000..fb4f20bc7d --- /dev/null +++ b/lib/core/src/lib/auth/services/legacy-authentication.service.ts @@ -0,0 +1,176 @@ +// /*! +// * @license +// * Copyright 2019 Alfresco Software, Ltd. +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +// +// import { Injectable } from '@angular/core'; +// import { Observable, from } from 'rxjs'; +// import { catchError, tap } from 'rxjs/operators'; +// import { AlfrescoApiService } from '../../services/alfresco-api.service'; +// import { CookieService } from '../../common/services/cookie.service'; +// import { LogService } from '../../common/services/log.service'; +// import { AppConfigService } from '../../app-config/app-config.service'; +// import { StorageService } from '../../common/services/storage.service'; +// import { JwtHelperService } from './jwt-helper.service'; +// import { BaseAuthenticationService } from './base-authentication.service'; +// import { ContentAuth } from "../basic-auth/contentAuth"; +// +// @Injectable({ +// providedIn: 'root' +// }) +// export class AuthenticationService extends BaseAuthenticationService { +// readonly supportCodeFlow = false; +// +// constructor( +// appConfig: AppConfigService, +// cookie: CookieService, +// logService: LogService, +// contentAuth: ContentAuth, +// private alfrescoApi: AlfrescoApiService, +// private storageService: StorageService +// ) { +// super(appConfig, cookie, contentAuth, logService); +// this.alfrescoApi.alfrescoApiInitialized.subscribe(() => { +// this.alfrescoApi.getInstance().reply('logged-in', () => { +// this.onLogin.next(); +// }); +// }); +// } +// +// /** +// * 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(); +// } +// +// isLoggedInWith(provider: string): boolean { +// if (provider === 'BPM') { +// return this.isBpmLoggedIn(); +// } else if (provider === 'ECM') { +// return this.isEcmLoggedIn(); +// } else { +// return this.isLoggedIn(); +// } +// } +// +// /** +// * Does the provider support OAuth? +// * +// * @returns True if supported, false otherwise +// */ +// isOauth(): boolean { +// return this.alfrescoApi.getInstance().isOauthConfiguration(); +// } +// +// /** +// * Logs the user in with SSO +// */ +// ssoImplicitLogin() { +// this.alfrescoApi.getInstance().implicitLogin(); +// } +// +// /** +// * 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)) +// ); +// } +// +// private callApiLogout(): Promise { +// if (this.alfrescoApi.getInstance()) { +// return this.alfrescoApi.getInstance().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(); +// } +// 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(); +// } +// return false; +// } +// +// /** +// * Gets the ECM username. +// * +// * @returns The ECM username +// */ +// getEcmUsername(): string { +// return this.alfrescoApi.getInstance().getEcmUsername(); +// } +// +// /** +// * Gets the BPM username +// * +// * @returns The BPM username +// */ +// getBpmUsername(): string { +// return this.alfrescoApi.getInstance().getBpmUsername(); +// } +// +// /** +// * Gets the auth token. +// * +// * @returns Auth token string +// */ +// getToken(): string { +// return this.storageService.getItem(JwtHelperService.USER_ACCESS_TOKEN); +// } +// +// reset() { } +// +// once(event: string): Observable { +// return new Observable((subscriber) => { +// this.alfrescoApi.getInstance().once(event, () => subscriber.next()); +// }); +// } +// } diff --git a/lib/core/src/lib/auth/services/oidc-authentication.service.ts b/lib/core/src/lib/auth/services/oidc-authentication.service.ts new file mode 100644 index 0000000000..0b22830005 --- /dev/null +++ b/lib/core/src/lib/auth/services/oidc-authentication.service.ts @@ -0,0 +1,167 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { OAuthService, OAuthStorage } from 'angular-oauth2-oidc'; +import { EMPTY, Observable } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service'; +import { OauthConfigModel } from '../models/oauth-config.model'; +import { 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.hasValidAccessToken(); + } + + isImplicitFlow() { + const oauth2: OauthConfigModel = Object.assign({}, this.appConfig.get(AppConfigValues.OAUTHCONFIG, null)); + return !!oauth2?.implicitFlow; + } + + isAuthCodeFlow() { + const oauth2: OauthConfigModel = Object.assign({}, this.appConfig.get(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)) + ); + } + + getEcmUsername(): string { + return this.jwtHelperService.getValueFromLocalToken(JwtHelperService.USER_PREFERRED_USERNAME); + } + + getBpmUsername(): string { + return this.jwtHelperService.getValueFromLocalToken(JwtHelperService.USER_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: OauthConfigModel = Object.assign({}, this.appConfig.get(AppConfigValues.OAUTHCONFIG, null)); + + if (config.oidc && oauth2.silentLogin) { + this.auth.login(); + } + } + + isPublicUrl(): boolean { + const oauth2 = this.appConfig.get(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); + } + +} diff --git a/lib/core/src/lib/common/services/storage.service.ts b/lib/core/src/lib/common/services/storage.service.ts index 68c47a02a0..39945b3241 100644 --- a/lib/core/src/lib/common/services/storage.service.ts +++ b/lib/core/src/lib/common/services/storage.service.ts @@ -82,7 +82,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]; } diff --git a/lib/core/src/lib/common/utils/object-utils.ts b/lib/core/src/lib/common/utils/object-utils.ts index 01c73ab3d2..24b95d4588 100644 --- a/lib/core/src/lib/common/utils/object-utils.ts +++ b/lib/core/src/lib/common/utils/object-utils.ts @@ -25,7 +25,7 @@ export class ObjectUtils { */ static getValue(target: any, key: string): any { - if (!target) { + if (!target || !key) { return undefined; } diff --git a/lib/core/src/lib/directives/logout.directive.ts b/lib/core/src/lib/directives/logout.directive.ts index b1e54587f4..e4d446abaf 100644 --- a/lib/core/src/lib/directives/logout.directive.ts +++ b/lib/core/src/lib/directives/logout.directive.ts @@ -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]); } diff --git a/lib/core/src/lib/login/components/login.component.ts b/lib/core/src/lib/login/components/login.component.ts index 4dedaf4faa..86a7f15a40 100644 --- a/lib/core/src/lib/login/components/login.component.ts +++ b/lib/core/src/lib/login/components/login.component.ts @@ -35,6 +35,8 @@ import { 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 { @@ -130,6 +132,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, @@ -163,7 +167,7 @@ export class LoginComponent implements OnInit, OnDestroy { const url = params['redirectUrl']; const provider = this.appConfig.get(AppConfigValues.PROVIDERS); - this.authService.setRedirect({ provider, url }); + this.basicAlfrescoAuthService.setRedirect({ provider, url }); }); } @@ -186,14 +190,13 @@ export class LoginComponent implements OnInit, OnDestroy { } redirectToImplicitLogin() { - this.authService.ssoImplicitLogin(); + this.oidcAuthenticationService.ssoImplicitLogin(); } /** * Method called on submit form * * @param values - * @param event */ onSubmit(values: any): void { this.disableError(); @@ -213,7 +216,7 @@ export class LoginComponent implements OnInit, OnDestroy { if (this.authService.isLoggedIn()) { this.router.navigate([this.successRoute]); } - this.authService.ssoImplicitLogin(); + this.oidcAuthenticationService.ssoImplicitLogin(); } /** @@ -246,11 +249,10 @@ export class LoginComponent implements OnInit, OnDestroy { } performLogin(values: { username: string; password: string }) { - this.authService - .login(values.username, values.password, this.rememberMe) + this.basicAlfrescoAuthService.login(values.username, values.password, this.rememberMe) .subscribe( - (token: any) => { - const redirectUrl = this.authService.getRedirect(); + async (token: any) => { + const redirectUrl = this.basicAlfrescoAuthService.getRedirect(); this.actualLoginStep = LoginSteps.Welcome; this.userPreferences.setStoragePrefix(values.username); @@ -260,10 +262,10 @@ export class LoginComponent implements OnInit, OnDestroy { ); if (redirectUrl) { - this.authService.setRedirect(null); - this.router.navigateByUrl(redirectUrl); + this.basicAlfrescoAuthService.setRedirect(null); + await this.router.navigateByUrl(redirectUrl); } else if (this.successRoute) { - this.router.navigate([this.successRoute]); + await this.router.navigate([this.successRoute]); } }, (err: any) => {