diff --git a/lib/core/src/lib/auth/guard/auth-guard-base.ts b/lib/core/src/lib/auth/guard/auth-guard-base.ts deleted file mode 100644 index 63f3456a26..0000000000 --- a/lib/core/src/lib/auth/guard/auth-guard-base.ts +++ /dev/null @@ -1,132 +0,0 @@ -/*! - * @license - * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, UrlTree } from '@angular/router'; -import { AuthenticationService } from '../services/authentication.service'; -import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service'; -import { OauthConfigModel } from '../models/oauth-config.model'; -import { MatDialog } from '@angular/material/dialog'; -import { StorageService } from '../../common/services/storage.service'; -import { Observable } from 'rxjs'; -import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service'; -import { OidcAuthenticationService } from '../oidc/oidc-authentication.service'; - -export abstract class AuthGuardBase implements CanActivate, CanActivateChild { - protected get withCredentials(): boolean { - return this.appConfigService.get('auth.withCredentials', false); - } - - constructor( - protected authenticationService: AuthenticationService, - protected basicAlfrescoAuthService: BasicAlfrescoAuthService, - protected oidcAuthenticationService: OidcAuthenticationService, - protected router: Router, - protected appConfigService: AppConfigService, - protected dialog: MatDialog, - private storageService: StorageService - ) {} - - abstract checkLogin( - activeRoute: ActivatedRouteSnapshot, - redirectUrl: string - ): Observable | Promise | boolean | UrlTree; - - canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot - ): Observable | Promise | boolean | UrlTree { - if (this.authenticationService.isLoggedIn() && this.authenticationService.isOauth() && this.isLoginFragmentPresent()) { - return this.redirectSSOSuccessURL(); - } - - return this.checkLogin(route, state.url); - } - - canActivateChild( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot - ): Observable | Promise | boolean | UrlTree { - return this.canActivate(route, state); - } - - protected async redirectSSOSuccessURL(): Promise { - const redirectFragment = this.storageService.getItem('loginFragment'); - - if (redirectFragment && this.getLoginRoute() !== redirectFragment) { - await this.navigate(redirectFragment); - this.storageService.removeItem('loginFragment'); - return false; - } - - return true; - } - - protected isLoginFragmentPresent(): boolean { - return !!this.storageService.getItem('loginFragment'); - } - - protected async redirectToUrl(url: string): Promise { - let urlToRedirect = `/${this.getLoginRoute()}`; - - if (!this.authenticationService.isOauth()) { - this.basicAlfrescoAuthService.setRedirect({ - provider: this.getProvider(), - url - }); - - urlToRedirect = `${urlToRedirect}?redirectUrl=${url}`; - return this.navigate(urlToRedirect); - } else if (this.getOauthConfig().silentLogin && !this.oidcAuthenticationService.isPublicUrl()) { - if (!this.oidcAuthenticationService.hasValidIdToken() || !this.oidcAuthenticationService.hasValidAccessToken()) { - this.oidcAuthenticationService.ssoLogin(url); - } - } else { - return this.navigate(urlToRedirect); - } - - return false; - } - - protected async navigate(url: string): Promise { - this.dialog.closeAll(); - await this.router.navigateByUrl(this.router.parseUrl(url)); - return false; - } - - protected getOauthConfig(): OauthConfigModel { - return this.appConfigService && this.appConfigService.get(AppConfigValues.OAUTHCONFIG, null); - } - - protected getLoginRoute(): string { - return this.appConfigService.get(AppConfigValues.LOGIN_ROUTE, 'login'); - } - - protected getProvider(): string { - return this.appConfigService.get(AppConfigValues.PROVIDERS, 'ALL'); - } - - protected isOAuthWithoutSilentLogin(): boolean { - const oauth = this.appConfigService.get(AppConfigValues.OAUTHCONFIG, null); - return this.authenticationService.isOauth() && !!oauth && !oauth.silentLogin; - } - - protected isSilentLogin(): boolean { - 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.spec.ts b/lib/core/src/lib/auth/guard/auth-guard-bpm.service.spec.ts index add0376cab..a6a463f975 100644 --- a/lib/core/src/lib/auth/guard/auth-guard-bpm.service.spec.ts +++ b/lib/core/src/lib/auth/guard/auth-guard-bpm.service.spec.ts @@ -19,19 +19,22 @@ import { TestBed } from '@angular/core/testing'; import { AppConfigService } from '../../app-config/app-config.service'; import { AuthGuardBpm } from './auth-guard-bpm.service'; import { AuthenticationService } from '../services/authentication.service'; -import { RouterStateSnapshot, Router } from '@angular/router'; +import { RouterStateSnapshot, Router, ActivatedRouteSnapshot } from '@angular/router'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service'; import { RedirectAuthService } from '../oidc/redirect-auth.service'; import { EMPTY, of } from 'rxjs'; import { OidcAuthenticationService } from '../oidc/oidc-authentication.service'; -import { NoopTranslateModule } from '@alfresco/adf-core'; +import { AuthGuardService } from './auth-guard.service'; +import { NoopTranslateModule } from '../../testing/noop-translate.module'; describe('AuthGuardService BPM', () => { - let authGuard: AuthGuardBpm; + let authGuard: Promise; let authService: AuthenticationService; let basicAlfrescoAuthService: BasicAlfrescoAuthService; let oidcAuthenticationService: OidcAuthenticationService; + let state: RouterStateSnapshot; + const route: ActivatedRouteSnapshot = new ActivatedRouteSnapshot(); let router: Router; let appConfigService: AppConfigService; @@ -40,6 +43,7 @@ describe('AuthGuardService BPM', () => { TestBed.configureTestingModule({ imports: [NoopTranslateModule, MatDialogModule], providers: [ + AuthGuardService, { provide: RedirectAuthService, useValue: { onLogin: EMPTY, onTokenReceived: of() } }, { provide: OidcAuthenticationService, @@ -56,17 +60,17 @@ describe('AuthGuardService BPM', () => { basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService); oidcAuthenticationService = TestBed.inject(OidcAuthenticationService); authService = TestBed.inject(AuthenticationService); - authGuard = TestBed.inject(AuthGuardBpm); router = TestBed.inject(Router); appConfigService = TestBed.inject(AppConfigService); appConfigService.config.providers = 'BPM'; appConfigService.config.auth = {}; appConfigService.config.oauth2 = {}; + spyOn(router, 'navigateByUrl'); + state = { url: 'some-url' } as RouterStateSnapshot; }); it('should redirect url if the alfresco js api is NOT logged in and isOAuth with silentLogin', async () => { - spyOn(router, 'navigateByUrl').and.stub(); spyOn(authService, 'isBpmLoggedIn').and.returnValue(false); spyOn(authService, 'isOauth').and.returnValue(true); spyOn(oidcAuthenticationService, 'isPublicUrl').and.returnValue(false); @@ -81,76 +85,74 @@ describe('AuthGuardService BPM', () => { scope: 'openid', provider: 'BPM' }; + state = { url: 'abc' } as RouterStateSnapshot; - const route = { url: 'abc' } as RouterStateSnapshot; + authGuard = TestBed.runInInjectionContext(() => AuthGuardBpm(route, state)) as Promise; - expect(await authGuard.canActivate(null, route)).toBeFalsy(); + expect(await authGuard).toBeFalsy(); expect(oidcAuthenticationService.ssoLogin).toHaveBeenCalledTimes(1); }); it('if the alfresco js api is logged in should canActivate be true', async () => { spyOn(authService, 'isBpmLoggedIn').and.returnValue(true); - const route = { url: 'some-url' } as RouterStateSnapshot; - expect(await authGuard.canActivate(null, route)).toBeTruthy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuardBpm(route, state)) as Promise; + expect(await authGuard).toBeTruthy(); }); it('if the alfresco js api is configured with withCredentials true should canActivate be true', async () => { spyOn(authService, 'isBpmLoggedIn').and.returnValue(true); appConfigService.config.auth.withCredentials = true; - const route = { url: 'some-url' } as RouterStateSnapshot; + authGuard = TestBed.runInInjectionContext(() => AuthGuardBpm(route, state)) as Promise; - expect(await authGuard.canActivate(null, route)).toBeTruthy(); + expect(await authGuard).toBeTruthy(); }); it('if the alfresco js api is NOT logged in should canActivate be false', async () => { spyOn(authService, 'isBpmLoggedIn').and.returnValue(false); - spyOn(router, 'navigateByUrl').and.stub(); - const route = { url: 'some-url' } as RouterStateSnapshot; - expect(await authGuard.canActivate(null, route)).toBeFalsy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuardBpm(route, state)) as Promise; + + expect(await authGuard).toBeFalsy(); }); it('if the alfresco js api is NOT logged in should trigger a redirect event', async () => { appConfigService.config.loginRoute = 'login'; - - spyOn(router, 'navigateByUrl'); spyOn(authService, 'isBpmLoggedIn').and.returnValue(false); - const route = { url: 'some-url' } as RouterStateSnapshot; - expect(await authGuard.canActivate(null, route)).toBeFalsy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuardBpm(route, state)) as Promise; + + expect(await authGuard).toBeFalsy(); expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl('/login?redirectUrl=some-url')); }); it('should redirect url if the alfresco js api is NOT logged in and isOAuthWithoutSilentLogin', async () => { - spyOn(router, 'navigateByUrl').and.stub(); spyOn(authService, 'isBpmLoggedIn').and.returnValue(false); spyOn(authService, 'isOauth').and.returnValue(true); appConfigService.config.oauth2.silentLogin = false; - const route = { url: 'some-url' } as RouterStateSnapshot; - expect(await authGuard.canActivate(null, route)).toBeFalsy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuardBpm(route, state)) as Promise; + + expect(await authGuard).toBeFalsy(); expect(router.navigateByUrl).toHaveBeenCalled(); }); it('should redirect url if NOT logged in and isOAuth but no silentLogin configured', async () => { - spyOn(router, 'navigateByUrl').and.stub(); spyOn(authService, 'isBpmLoggedIn').and.returnValue(false); spyOn(authService, 'isOauth').and.returnValue(true); appConfigService.config.oauth2.silentLogin = undefined; - const route = { url: 'some-url' } as RouterStateSnapshot; - expect(await authGuard.canActivate(null, route)).toBeFalsy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuardBpm(route, state)) as Promise; + + expect(await authGuard).toBeFalsy(); expect(router.navigateByUrl).toHaveBeenCalled(); }); - it('should set redirect url', () => { + it('should set redirect url', async () => { spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); - spyOn(router, 'navigateByUrl').and.stub(); - const route = { url: 'some-url' } as RouterStateSnapshot; - authGuard.canActivate(null, route); + await TestBed.runInInjectionContext(() => AuthGuardBpm(route, state)); expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'BPM', @@ -159,12 +161,11 @@ describe('AuthGuardService BPM', () => { expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url'); }); - it('should set redirect navigation commands with query params', () => { + it('should set redirect navigation commands with query params', async () => { spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); - spyOn(router, 'navigateByUrl').and.stub(); - const route = { url: 'some-url;q=123' } as RouterStateSnapshot; + state = { url: 'some-url;q=123' } as RouterStateSnapshot; - authGuard.canActivate(null, route); + await TestBed.runInInjectionContext(() => AuthGuardBpm(route, state)); expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'BPM', @@ -173,12 +174,11 @@ describe('AuthGuardService BPM', () => { expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url;q=123'); }); - it('should set redirect navigation commands with query params', () => { + it('should set redirect navigation commands with query params', async () => { spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); - spyOn(router, 'navigateByUrl').and.stub(); - const route = { url: '/' } as RouterStateSnapshot; + state = { url: '/' } as RouterStateSnapshot; - authGuard.canActivate(null, route); + await TestBed.runInInjectionContext(() => AuthGuardBpm(route, state)); expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'BPM', @@ -187,13 +187,11 @@ describe('AuthGuardService BPM', () => { expect(basicAlfrescoAuthService.getRedirect()).toEqual('/'); }); - it('should get redirect url from config if there is one configured', () => { + it('should get redirect url from config if there is one configured', async () => { appConfigService.config.loginRoute = 'fakeLoginRoute'; spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); - spyOn(router, 'navigateByUrl').and.stub(); - const route = { url: 'some-url' } as RouterStateSnapshot; - authGuard.canActivate(null, route); + await TestBed.runInInjectionContext(() => AuthGuardBpm(route, state)); expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'BPM', @@ -202,16 +200,12 @@ describe('AuthGuardService BPM', () => { expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl('/fakeLoginRoute?redirectUrl=some-url')); }); - it('should to close the material dialog if is redirect to the login', () => { + it('should to close the material dialog if is redirect to the login', async () => { const materialDialog = TestBed.inject(MatDialog); - spyOn(materialDialog, 'closeAll'); - spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); - spyOn(router, 'navigateByUrl').and.stub(); - const route = { url: 'some-url' } as RouterStateSnapshot; - authGuard.canActivate(null, route); + await TestBed.runInInjectionContext(() => AuthGuardBpm(route, state)); expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'BPM', 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 eb575e3cbd..7bbf844415 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 @@ -15,36 +15,22 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router'; -import { AppConfigService } from '../../app-config/app-config.service'; +import { inject } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router'; 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 '../oidc/oidc-authentication.service'; +import { AuthGuardService } from './auth-guard.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); +export const AuthGuardBpm: CanActivateFn = async (_: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise => { + const authGuardBaseService = inject(AuthGuardService); + const authenticationService = inject(AuthenticationService); + + if (authenticationService.isLoggedIn() && authenticationService.isOauth() && authGuardBaseService.isLoginFragmentPresent()) { + return authGuardBaseService.redirectSSOSuccessURL(); } - async checkLogin(_: ActivatedRouteSnapshot, redirectUrl: string): Promise { - if (this.authenticationService.isBpmLoggedIn() || this.withCredentials) { - return true; - } - return this.redirectToUrl(redirectUrl); + if (authenticationService.isBpmLoggedIn() || authGuardBaseService.withCredentials) { + return true; } -} + + return authGuardBaseService.redirectToUrl(state.url); +}; diff --git a/lib/core/src/lib/auth/guard/auth-guard-ecm.service.spec.ts b/lib/core/src/lib/auth/guard/auth-guard-ecm.service.spec.ts index 2131d94ad9..fccde1b257 100644 --- a/lib/core/src/lib/auth/guard/auth-guard-ecm.service.spec.ts +++ b/lib/core/src/lib/auth/guard/auth-guard-ecm.service.spec.ts @@ -19,7 +19,7 @@ import { TestBed } from '@angular/core/testing'; import { AppConfigService } from '../../app-config/app-config.service'; import { AuthGuardEcm } from './auth-guard-ecm.service'; import { AuthenticationService } from '../services/authentication.service'; -import { RouterStateSnapshot, Router } from '@angular/router'; +import { RouterStateSnapshot, Router, ActivatedRouteSnapshot } from '@angular/router'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service'; import { RouterTestingModule } from '@angular/router/testing'; @@ -29,12 +29,14 @@ import { OidcAuthenticationService } from '../oidc/oidc-authentication.service'; import { NoopTranslateModule } from '@alfresco/adf-core'; describe('AuthGuardService ECM', () => { - let authGuard: AuthGuardEcm; + let authGuard: Promise; let authService: AuthenticationService; let basicAlfrescoAuthService: BasicAlfrescoAuthService; let oidcAuthenticationService: OidcAuthenticationService; let router: Router; let appConfigService: AppConfigService; + let state: RouterStateSnapshot; + const route: ActivatedRouteSnapshot = new ActivatedRouteSnapshot(); beforeEach(() => { TestBed.configureTestingModule({ @@ -58,58 +60,59 @@ describe('AuthGuardService ECM', () => { oidcAuthenticationService = TestBed.inject(OidcAuthenticationService); basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService); authService = TestBed.inject(AuthenticationService); - authGuard = TestBed.inject(AuthGuardEcm); router = TestBed.inject(Router); appConfigService = TestBed.inject(AppConfigService); appConfigService.config.providers = 'ECM'; appConfigService.config.auth = {}; appConfigService.config.oauth2 = {}; + state = { url: 'some-url' } as RouterStateSnapshot; + spyOn(router, 'navigateByUrl'); }); it('if the alfresco js api is logged in should canActivate be true', async () => { spyOn(authService, 'isEcmLoggedIn').and.returnValue(true); - const route = { url: 'some-url' } as RouterStateSnapshot; - expect(await authGuard.canActivate(null, route)).toBeTruthy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuardEcm(route, state)) as Promise; + + expect(await authGuard).toBeTruthy(); }); it('if the alfresco js api is configured with withCredentials true should canActivate be true', async () => { spyOn(authService, 'isBpmLoggedIn').and.returnValue(true); appConfigService.config.auth.withCredentials = true; - const route = { url: 'some-url' } as RouterStateSnapshot; + authGuard = TestBed.runInInjectionContext(() => AuthGuardEcm(route, state)) as Promise; - expect(await authGuard.canActivate(null, route)).toBeTruthy(); + expect(await authGuard).toBeTruthy(); }); it('if the alfresco js api is NOT logged in should canActivate be false', async () => { spyOn(authService, 'isEcmLoggedIn').and.returnValue(false); - spyOn(router, 'navigateByUrl').and.stub(); - const route = { url: 'some-url' } as RouterStateSnapshot; - expect(await authGuard.canActivate(null, route)).toBeFalsy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuardEcm(route, state)) as Promise; + + expect(await authGuard).toBeFalsy(); }); it('if the alfresco js api is NOT logged in should trigger a redirect event', async () => { appConfigService.config.loginRoute = 'login'; - - spyOn(router, 'navigateByUrl'); spyOn(authService, 'isEcmLoggedIn').and.returnValue(false); - const route = { url: 'some-url' } as RouterStateSnapshot; - expect(await authGuard.canActivate(null, route)).toBeFalsy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuardEcm(route, state)) as Promise; + + expect(await authGuard).toBeFalsy(); expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl('/login?redirectUrl=some-url')); }); it('should redirect url if the alfresco js api is NOT logged in and isOAuthWithoutSilentLogin', async () => { - spyOn(router, 'navigateByUrl').and.stub(); spyOn(authService, 'isEcmLoggedIn').and.returnValue(false); spyOn(authService, 'isOauth').and.returnValue(true); appConfigService.config.oauth2.silentLogin = false; - const route = { url: 'some-url' } as RouterStateSnapshot; - expect(await authGuard.canActivate(null, route)).toBeFalsy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuardEcm(route, state)) as Promise; + + expect(await authGuard).toBeFalsy(); expect(router.navigateByUrl).toHaveBeenCalled(); }); @@ -127,30 +130,29 @@ describe('AuthGuardService ECM', () => { publicUrl: 'settings', scope: 'openid' }; + state = { url: 'abc' } as RouterStateSnapshot; - const route = { url: 'abc' } as RouterStateSnapshot; + authGuard = TestBed.runInInjectionContext(() => AuthGuardEcm(route, state)) as Promise; - expect(await authGuard.canActivate(null, route)).toBeFalsy(); + expect(await authGuard).toBeFalsy(); expect(oidcAuthenticationService.ssoLogin).toHaveBeenCalledTimes(1); }); it('should not redirect url if NOT logged in and isOAuth but no silentLogin configured', async () => { - spyOn(router, 'navigateByUrl').and.stub(); spyOn(authService, 'isEcmLoggedIn').and.returnValue(false); spyOn(authService, 'isOauth').and.returnValue(true); appConfigService.config.oauth2.silentLogin = undefined; - const route = { url: 'some-url' } as RouterStateSnapshot; - expect(await authGuard.canActivate(null, route)).toBeFalsy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuardEcm(route, state)) as Promise; + + expect(await authGuard).toBeFalsy(); expect(router.navigateByUrl).toHaveBeenCalled(); }); - it('should set redirect navigation commands', () => { + it('should set redirect navigation commands', async () => { spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); - spyOn(router, 'navigateByUrl').and.stub(); - const route = { url: 'some-url' } as RouterStateSnapshot; - authGuard.canActivate(null, route); + await TestBed.runInInjectionContext(() => AuthGuardEcm(route, state)); expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ECM', @@ -159,12 +161,11 @@ describe('AuthGuardService ECM', () => { expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url'); }); - it('should set redirect navigation commands with query params', () => { + it('should set redirect navigation commands with query params', async () => { spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); - spyOn(router, 'navigateByUrl').and.stub(); - const route = { url: 'some-url;q=123' } as RouterStateSnapshot; + state = { url: 'some-url;q=123' } as RouterStateSnapshot; - authGuard.canActivate(null, route); + await TestBed.runInInjectionContext(() => AuthGuardEcm(route, state)); expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ECM', @@ -173,12 +174,11 @@ describe('AuthGuardService ECM', () => { expect(basicAlfrescoAuthService.getRedirect()).toEqual('some-url;q=123'); }); - it('should set redirect navigation commands with query params', () => { + it('should set redirect navigation commands with query params', async () => { spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); - spyOn(router, 'navigateByUrl').and.stub(); - const route = { url: '/' } as RouterStateSnapshot; + state = { url: '/' } as RouterStateSnapshot; - authGuard.canActivate(null, route); + await TestBed.runInInjectionContext(() => AuthGuardEcm(route, state)); expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ECM', @@ -187,13 +187,11 @@ describe('AuthGuardService ECM', () => { expect(basicAlfrescoAuthService.getRedirect()).toEqual('/'); }); - it('should get redirect url from config if there is one configured', () => { + it('should get redirect url from config if there is one configured', async () => { appConfigService.config.loginRoute = 'fakeLoginRoute'; spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); - spyOn(router, 'navigateByUrl').and.stub(); - const route = { url: 'some-url' } as RouterStateSnapshot; - authGuard.canActivate(null, route); + await TestBed.runInInjectionContext(() => AuthGuardEcm(route, state)); expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ECM', @@ -202,16 +200,12 @@ describe('AuthGuardService ECM', () => { expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl('/fakeLoginRoute?redirectUrl=some-url')); }); - it('should to close the material dialog if is redirect to the login', () => { + it('should to close the material dialog if is redirect to the login', async () => { const materialDialog = TestBed.inject(MatDialog); - spyOn(materialDialog, 'closeAll'); - spyOn(basicAlfrescoAuthService, 'setRedirect').and.callThrough(); - spyOn(router, 'navigateByUrl').and.stub(); - const route = { url: 'some-url' } as RouterStateSnapshot; - authGuard.canActivate(null, route); + await TestBed.runInInjectionContext(() => AuthGuardEcm(route, state)); expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ECM', 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 d5bdf482e5..69cf958210 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 @@ -15,37 +15,22 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router'; +import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } 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 '../oidc/oidc-authentication.service'; +import { AuthGuardService } from './auth-guard.service'; +import { inject } from '@angular/core'; -@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); +export const AuthGuardEcm: CanActivateFn = async (_: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise => { + const authGuardBaseService = inject(AuthGuardService); + const authenticationService = inject(AuthenticationService); + + if (authenticationService.isLoggedIn() && authenticationService.isOauth() && authGuardBaseService.isLoginFragmentPresent()) { + return authGuardBaseService.redirectSSOSuccessURL(); } - async checkLogin(_: ActivatedRouteSnapshot, redirectUrl: string): Promise { - if (this.authenticationService.isEcmLoggedIn() || this.withCredentials) { - return true; - } - - return this.redirectToUrl(redirectUrl); + if (authenticationService.isEcmLoggedIn() || authGuardBaseService.withCredentials) { + return true; } -} + + return authGuardBaseService.redirectToUrl(state.url); +}; diff --git a/lib/core/src/lib/auth/guard/auth-guard.service.spec.ts b/lib/core/src/lib/auth/guard/auth-guard.service.spec.ts index f0785d605c..ef3314e45e 100644 --- a/lib/core/src/lib/auth/guard/auth-guard.service.spec.ts +++ b/lib/core/src/lib/auth/guard/auth-guard.service.spec.ts @@ -16,9 +16,9 @@ */ import { TestBed } from '@angular/core/testing'; -import { Router, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; import { AppConfigService } from '../../app-config/app-config.service'; -import { AuthGuard } from './auth-guard.service'; +import { AuthGuard } from './auth-guard'; import { AuthenticationService } from '../services/authentication.service'; import { StorageService } from '../../common/services/storage.service'; import { OidcAuthenticationService } from '../oidc/oidc-authentication.service'; @@ -27,17 +27,18 @@ import { RedirectAuthService } from '../oidc/redirect-auth.service'; import { EMPTY, of } from 'rxjs'; import { MatDialogModule } from '@angular/material/dialog'; import { RouterTestingModule } from '@angular/router/testing'; -import { NoopTranslateModule } from '@alfresco/adf-core'; +import { NoopTranslateModule } from '../../testing/noop-translate.module'; describe('AuthGuardService', () => { - let state; + let state: RouterStateSnapshot; let authService: AuthenticationService; let router: Router; - let authGuard: AuthGuard; + let authGuard: Promise; let storageService: StorageService; let appConfigService: AppConfigService; let basicAlfrescoAuthService: BasicAlfrescoAuthService; let oidcAuthenticationService: OidcAuthenticationService; + const route: ActivatedRouteSnapshot = new ActivatedRouteSnapshot(); beforeEach(() => { TestBed.configureTestingModule({ @@ -57,33 +58,34 @@ describe('AuthGuardService', () => { ] }); localStorage.clear(); - state = { url: '' }; + state = { url: 'some-url' } as RouterStateSnapshot; authService = TestBed.inject(AuthenticationService); basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService); oidcAuthenticationService = TestBed.inject(OidcAuthenticationService); router = TestBed.inject(Router); - authGuard = TestBed.inject(AuthGuard); appConfigService = TestBed.inject(AppConfigService); appConfigService.config.auth = {}; appConfigService.config.oauth2 = {}; storageService = TestBed.inject(StorageService); + spyOn(router, 'navigateByUrl'); }); it('if the alfresco js api is logged in should canActivate be true', async () => { - spyOn(router, 'navigateByUrl'); spyOn(authService, 'isLoggedIn').and.returnValue(true); - expect(await authGuard.canActivate(null, state)).toBeTruthy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuard(route, state)) as Promise; + + expect(await authGuard).toBeTruthy(); expect(router.navigateByUrl).not.toHaveBeenCalled(); }); it('if the alfresco js api is NOT logged in should canActivate be false', async () => { - state.url = 'some-url'; - spyOn(router, 'navigateByUrl'); spyOn(authService, 'isLoggedIn').and.returnValue(false); - expect(await authGuard.canActivate(null, state)).toBeFalsy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuard(route, state)) as Promise; + + expect(await authGuard).toBeFalsy(); expect(router.navigateByUrl).toHaveBeenCalled(); }); @@ -91,40 +93,42 @@ describe('AuthGuardService', () => { spyOn(authService, 'isBpmLoggedIn').and.returnValue(true); appConfigService.config.auth.withCredentials = true; - const route = { url: 'some-url' } as RouterStateSnapshot; + authGuard = TestBed.runInInjectionContext(() => AuthGuard(route, state)) as Promise; - expect(await authGuard.canActivate(null, route)).toBeTruthy(); + expect(await authGuard).toBeTruthy(); }); it('should not redirect to login', async () => { storageService.setItem('loginFragment', 'login'); - - spyOn(router, 'navigateByUrl').and.stub(); spyOn(authService, 'isLoggedIn').and.returnValue(true); spyOn(authService, 'isOauth').and.returnValue(true); appConfigService.config.oauth2.silentLogin = false; - expect(await authGuard.canActivate(null, state)).toBeTruthy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuard(route, state)) as Promise; + + expect(await authGuard).toBeTruthy(); expect(router.navigateByUrl).not.toHaveBeenCalled(); }); it('should redirect url if the User is NOT logged in and isOAuthWithoutSilentLogin', async () => { - spyOn(router, 'navigateByUrl').and.stub(); spyOn(authService, 'isLoggedIn').and.returnValue(false); spyOn(authService, 'isOauth').and.returnValue(true); appConfigService.config.oauth2.silentLogin = false; - expect(await authGuard.canActivate(null, state)).toBeFalsy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuard(route, state)) as Promise; + + expect(await authGuard).toBeFalsy(); expect(router.navigateByUrl).toHaveBeenCalled(); }); it('should redirect url if the User is NOT logged in and isOAuth but no silentLogin configured', async () => { - spyOn(router, 'navigateByUrl').and.stub(); spyOn(authService, 'isLoggedIn').and.returnValue(false); spyOn(authService, 'isOauth').and.returnValue(true); appConfigService.config.oauth2.silentLogin = undefined; - expect(await authGuard.canActivate(null, state)).toBeFalsy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuard(route, state)) as Promise; + + expect(await authGuard).toBeFalsy(); expect(router.navigateByUrl).toHaveBeenCalled(); }); @@ -134,18 +138,17 @@ describe('AuthGuardService', () => { spyOn(authService, 'isOauth').and.returnValue(true); appConfigService.config.oauth2.silentLogin = true; - expect(await authGuard.canActivate(null, state)).toBeFalsy(); + authGuard = TestBed.runInInjectionContext(() => AuthGuard(route, state)) as Promise; + + expect(await authGuard).toBeFalsy(); expect(oidcAuthenticationService.ssoLogin).toHaveBeenCalledTimes(1); }); it('should set redirect url', async () => { - state.url = 'some-url'; appConfigService.config.loginRoute = 'login'; - - spyOn(router, 'navigateByUrl'); spyOn(basicAlfrescoAuthService, 'setRedirect'); - await authGuard.canActivate(null, state); + await TestBed.runInInjectionContext(() => AuthGuard(route, state)); expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ALL', @@ -158,11 +161,9 @@ describe('AuthGuardService', () => { state.url = 'some-url;q=query'; appConfigService.config.loginRoute = 'login'; appConfigService.config.provider = 'ALL'; - - spyOn(router, 'navigateByUrl'); spyOn(basicAlfrescoAuthService, 'setRedirect'); - await authGuard.canActivate(null, state); + await TestBed.runInInjectionContext(() => AuthGuard(route, state)); expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ALL', @@ -172,13 +173,10 @@ describe('AuthGuardService', () => { }); it('should get redirect url from config if there is one configured', async () => { - state.url = 'some-url'; appConfigService.config.loginRoute = 'fakeLoginRoute'; - - spyOn(router, 'navigateByUrl'); spyOn(basicAlfrescoAuthService, 'setRedirect'); - await authGuard.canActivate(null, state); + await TestBed.runInInjectionContext(() => AuthGuard(route, state)); expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ALL', @@ -189,11 +187,9 @@ describe('AuthGuardService', () => { it('should pass actual redirect when no state segments exists', async () => { state.url = '/'; - - spyOn(router, 'navigateByUrl'); spyOn(basicAlfrescoAuthService, 'setRedirect'); - await authGuard.canActivate(null, state); + await TestBed.runInInjectionContext(() => AuthGuard(route, state)); expect(basicAlfrescoAuthService.setRedirect).toHaveBeenCalledWith({ provider: 'ALL', 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 62d9bcef2b..9d62c71285 100644 --- a/lib/core/src/lib/auth/guard/auth-guard.service.ts +++ b/lib/core/src/lib/auth/guard/auth-guard.service.ts @@ -15,69 +15,98 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router'; +import { Router } 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 { 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 { BasicAlfrescoAuthService } from '../basic-auth/basic-alfresco-auth.service'; import { OidcAuthenticationService } from '../oidc/oidc-authentication.service'; +import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) -export class AuthGuard extends AuthGuardBase { - ticketChangeBind: any; - +export class AuthGuardService { 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); + private authenticationService: AuthenticationService, + private basicAlfrescoAuthService: BasicAlfrescoAuthService, + private oidcAuthenticationService: OidcAuthenticationService, + private router: Router, + private appConfigService: AppConfigService, + private dialog: MatDialog, + private storageService: StorageService + ) {} - window.addEventListener('storage', this.ticketChangeBind); + get withCredentials(): boolean { + return this.appConfigService.get('auth.withCredentials', false); } - ticketChange(event: StorageEvent) { - if (event.key.includes('ticket-ECM') && event.newValue !== event.oldValue) { - this.ticketChangeRedirect(event); + async redirectSSOSuccessURL(): Promise { + const redirectFragment = this.storageService.getItem('loginFragment'); + + if (redirectFragment && this.getLoginRoute() !== redirectFragment) { + await this.navigate(redirectFragment); + this.storageService.removeItem('loginFragment'); + return false; } - if (event.key.includes('ticket-BPM') && event.newValue !== event.oldValue) { - this.ticketChangeRedirect(event); - } - - if ( - event.key.endsWith(JwtHelperService.USER_ACCESS_TOKEN) && - this.jwtHelperService.getValueFromToken(event.newValue, JwtHelperService.USER_PREFERRED_USERNAME) !== - this.jwtHelperService.getValueFromToken(event.oldValue, JwtHelperService.USER_PREFERRED_USERNAME) - ) { - this.ticketChangeRedirect(event); - } + return true; } - private ticketChangeRedirect(event: StorageEvent) { - if (event.newValue) { - this.navigate(this.router.url); + isLoginFragmentPresent(): boolean { + return !!this.storageService.getItem('loginFragment'); + } + + async redirectToUrl(url: string): Promise { + let urlToRedirect = `/${this.getLoginRoute()}`; + + if (!this.authenticationService.isOauth()) { + this.basicAlfrescoAuthService.setRedirect({ + provider: this.getProvider(), + url + }); + + urlToRedirect = `${urlToRedirect}?redirectUrl=${url}`; + return this.navigate(urlToRedirect); + } else if (this.getOauthConfig().silentLogin && !this.oidcAuthenticationService.isPublicUrl()) { + if (!this.oidcAuthenticationService.hasValidIdToken() || !this.oidcAuthenticationService.hasValidAccessToken()) { + this.oidcAuthenticationService.ssoLogin(url); + } } else { - window.location.reload(); + return this.navigate(urlToRedirect); } + + return false; } - async checkLogin(_: ActivatedRouteSnapshot, redirectUrl: string): Promise { - if (this.authenticationService.isLoggedIn() || this.withCredentials) { - return true; - } - return this.redirectToUrl(redirectUrl); + async navigate(url: string): Promise { + this.dialog.closeAll(); + await this.router.navigateByUrl(this.router.parseUrl(url)); + return false; + } + + private getOauthConfig(): OauthConfigModel { + return this.appConfigService?.get(AppConfigValues.OAUTHCONFIG, null); + } + + private getLoginRoute(): string { + return this.appConfigService.get(AppConfigValues.LOGIN_ROUTE, 'login'); + } + + private getProvider(): string { + return this.appConfigService.get(AppConfigValues.PROVIDERS, 'ALL'); + } + + isOAuthWithoutSilentLogin(): boolean { + const oauth = this.appConfigService.get(AppConfigValues.OAUTHCONFIG, null); + return this.authenticationService.isOauth() && !!oauth && !oauth.silentLogin; + } + + isSilentLogin(): boolean { + 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.ts b/lib/core/src/lib/auth/guard/auth-guard.ts new file mode 100644 index 0000000000..60b2a91a0a --- /dev/null +++ b/lib/core/src/lib/auth/guard/auth-guard.ts @@ -0,0 +1,61 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { inject } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; +import { AuthenticationService } from '../services/authentication.service'; +import { AuthGuardService } from './auth-guard.service'; +import { JwtHelperService } from '../services/jwt-helper.service'; + +const ticketChangeRedirect = (event: StorageEvent, authGuardBaseService: AuthGuardService, url: string): void => { + if (event.newValue) { + authGuardBaseService.navigate(url); + } else { + window.location.reload(); + } +}; + +const ticketChangeHandler = (event: StorageEvent, authGuardBaseService: AuthGuardService, jwtHelperService: JwtHelperService, url: string): void => { + if (event.newValue !== event.oldValue) { + if (event.key.includes('ticket-ECM') || event.key.includes('ticket-BPM')) { + ticketChangeRedirect(event, authGuardBaseService, url); + } + } + + if ( + event.key.endsWith(JwtHelperService.USER_ACCESS_TOKEN) && + jwtHelperService.getValueFromToken(event.newValue, JwtHelperService.USER_PREFERRED_USERNAME) !== + jwtHelperService.getValueFromToken(event.oldValue, JwtHelperService.USER_PREFERRED_USERNAME) + ) { + ticketChangeRedirect(event, authGuardBaseService, url); + } +}; + +export const AuthGuard: CanActivateFn = async (_: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise => { + const router = inject(Router); + const jwtHelperService = inject(JwtHelperService); + const authGuardBaseService = inject(AuthGuardService); + const authenticationService = inject(AuthenticationService); + + window.addEventListener('storage', (event: StorageEvent) => ticketChangeHandler(event, authGuardBaseService, jwtHelperService, router.url)); + + if (authenticationService.isLoggedIn() || authGuardBaseService.withCredentials) { + return true; + } + + return authGuardBaseService.redirectToUrl(state.url); +}; diff --git a/lib/core/src/lib/auth/public-api.ts b/lib/core/src/lib/auth/public-api.ts index ceeda1f08b..a457bd9530 100644 --- a/lib/core/src/lib/auth/public-api.ts +++ b/lib/core/src/lib/auth/public-api.ts @@ -17,8 +17,8 @@ export * from './authentication-interceptor/auth-bearer.interceptor'; -export * from './guard/auth-guard-base'; export * from './guard/auth-guard.service'; +export * from './guard/auth-guard'; export * from './guard/auth-guard-ecm.service'; export * from './guard/auth-guard-bpm.service'; export * from './guard/auth-guard-sso-role.service'; diff --git a/tools/doc/doctool.config.json b/tools/doc/doctool.config.json index 1320e30d63..73d5570542 100644 --- a/tools/doc/doctool.config.json +++ b/tools/doc/doctool.config.json @@ -113,7 +113,7 @@ "activiti-alfresco.service": "ActivitiContentService", "auth-guard-bpm.service": "AuthGuardBpm", "auth-guard-ecm.service": "AuthGuardEcm", - "auth-guard.service": "AuthGuard", + "auth-guard": "AuthGuard", "card-item-types.service": "CardItemTypeService", "card-view-item.interface": "CardViewItem", "confirm.dialog": "ConfirmDialogComponent",