[AAE-12521] move navigation to the guard to fix infinite loop issue with code flow auth

This commit is contained in:
Amedeo Lepore
2024-01-18 13:17:45 +01:00
parent 1d097f695d
commit 6a80413880
3 changed files with 90 additions and 73 deletions

View File

@@ -16,57 +16,90 @@
*/
import { TestBed } from '@angular/core/testing';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { AuthService } from './auth.service';
import { Router } from '@angular/router';
import { OidcAuthGuard } from './oidc-auth.guard';
const state: RouterStateSnapshot = {
root: new ActivatedRouteSnapshot(),
url: 'http://example.com'
};
const routeSnapshot = new ActivatedRouteSnapshot();
import { AuthService } from './auth.service';
describe('OidcAuthGuard', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
providers: [OidcAuthGuard]
});
});
let oidcAuthGuard: OidcAuthGuard;
let authServiceSpy: jasmine.SpyObj<AuthService>;
let routerSpy: jasmine.SpyObj<Router>;
describe('#canActivate', () => {
it('should return false if the user is not authenticated, and call login method', () => {
const authService = { authenticated: false, login: jasmine.createSpy() } as unknown as AuthService;
const authGuard = new OidcAuthGuard(authService);
beforeEach(() => {
const routerSpyObj = jasmine.createSpyObj('Router', ['navigateByUrl']);
const authSpy = jasmine.createSpyObj('AuthService', ['loginCallback']);
expect(authGuard.canActivate(routeSnapshot, state)).toEqual(false);
expect(authService.login).toHaveBeenCalled();
TestBed.configureTestingModule({
providers: [
OidcAuthGuard,
{ provide: AuthService, useValue: authSpy },
{ provide: Router, useValue: routerSpyObj }
],
imports: [RouterTestingModule]
});
routerSpy = TestBed.inject(Router) as jasmine.SpyObj<Router>;
oidcAuthGuard = TestBed.inject(OidcAuthGuard);
authServiceSpy = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
});
it('should return true if the user is authenticated', () => {
const authService = { authenticated: true } as unknown as AuthService;
const authGuard = new OidcAuthGuard(authService);
describe('canActivate', () => {
it('should return true if is authenticated', () => {
authServiceSpy.authenticated = true;
expect(authGuard.canActivate(routeSnapshot, state)).toEqual(true);
});
});
const result = oidcAuthGuard.canActivate();
describe('#canActivateChild', () => {
it('should return false if the user is not authenticated, and call login method', () => {
const authService = { authenticated: false, login: jasmine.createSpy() } as unknown as AuthService;
const authGuard = new OidcAuthGuard(authService);
expect(result).toBe(true);
});
expect(authGuard.canActivateChild(routeSnapshot, state)).toEqual(false);
expect(authService.login).toHaveBeenCalled();
it('should call isAuthenticated and return the result', () => {
const isAuthenticatedSpy = spyOn<any>(oidcAuthGuard, '_isAuthenticated').and.returnValue(true);
const result = oidcAuthGuard.canActivate();
expect(isAuthenticatedSpy).toHaveBeenCalled();
expect(result).toBe(true);
});
});
it('should return true if the user is authenticated', () => {
const authService = { authenticated: true } as unknown as AuthService;
const authGuard = new OidcAuthGuard(authService);
describe('canActivateChild', () => {
it('should call isAuthenticated and return its result', () => {
const isAuthenticatedSpy = spyOn<any>(oidcAuthGuard, '_isAuthenticated').and.returnValue(true);
expect(authGuard.canActivateChild(routeSnapshot, state)).toEqual(true);
const result = oidcAuthGuard.canActivateChild();
expect(isAuthenticatedSpy).toHaveBeenCalled();
expect(result).toBe(true);
});
});
});
describe('isAuthenticated', () => {
it('should return true if is authenticated', () => {
authServiceSpy.authenticated = true;
const result = oidcAuthGuard['_isAuthenticated']();
expect(result).toBe(true);
});
it('should call loginCallback and navigateByUrl if not authenticated', async () => {
authServiceSpy.authenticated = false;
authServiceSpy.loginCallback.and.returnValue(Promise.resolve('/fake-route'));
await oidcAuthGuard.canActivate();
expect(authServiceSpy.loginCallback).toHaveBeenCalled();
expect(routerSpy.navigateByUrl).toHaveBeenCalledWith('/fake-route', { replaceUrl: true });
});
it('should navigate to default route if loginCallback fails', async () => {
authServiceSpy.authenticated = false;
authServiceSpy.loginCallback.and.returnValue(Promise.reject(new Error()));
await oidcAuthGuard.canActivate();
expect(routerSpy.navigateByUrl).toHaveBeenCalledWith('/', { replaceUrl: true });
});
});
});

View File

@@ -16,37 +16,36 @@
*/
import { Injectable } from '@angular/core';
import { CanActivate, UrlTree } from '@angular/router';
import { CanActivate, Router, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
const ROUTE_DEFAULT = '/';
@Injectable({
providedIn: 'root'
})
export class OidcAuthGuard implements CanActivate {
constructor(private auth: AuthService) {}
constructor(private auth: AuthService, private _router: Router) { }
canActivate(
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this._isAuthenticated();
}
canActivateChild() {
return this._isAuthenticated();
}
private _isAuthenticated() {
if (this.auth.authenticated) {
return true;
canActivate(
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this._isAuthenticated();
}
const loginResult = this.auth.loginCallback({customHashFragment: window.location.search});
if (loginResult instanceof Promise) {
return loginResult.then(() => true).catch(() => false);
canActivateChild() {
return this._isAuthenticated();
}
return false;
}
private _isAuthenticated() {
if (this.auth.authenticated) {
return true;
}
return this.auth.loginCallback({ customHashFragment: window.location.search })
.then(route => this._router.navigateByUrl(route, { replaceUrl: true }))
.catch(() => this._router.navigateByUrl(ROUTE_DEFAULT, { replaceUrl: true }));
}
}

View File

@@ -16,26 +16,11 @@
*/
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Router } from '@angular/router';
import { from, of } from 'rxjs';
import { catchError, first, map } from 'rxjs/operators';
import { AuthService } from '../../auth.service';
const ROUTE_DEFAULT = '/';
@Component({
template: '<div data-automation-id="auth-confirmation"></div>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AuthenticationConfirmationComponent {
constructor(private auth: AuthService, private _router: Router) {
const routeStored$ = from(this.auth.loginCallback()).pipe(
map((route) => route || ROUTE_DEFAULT),
catchError(() => of(ROUTE_DEFAULT))
);
routeStored$.pipe(first()).subscribe((route) => {
this._router.navigateByUrl(route, { replaceUrl: true });
});
}
constructor(){}
}