From 2ab2f3bbedbf1ec995ab6d37471ce31a0dc3ff0e Mon Sep 17 00:00:00 2001 From: Ryan Galletto Date: Fri, 27 Sep 2024 10:59:32 -0400 Subject: [PATCH] Improve ziti session expiration handling --- .../app-ziti-console/src/app/app.component.ts | 10 +-- .../src/app/guards/authentication.guard.ts | 2 +- .../app/interceptors/ziti-api.interceptor.ts | 72 +++++++------------ .../src/app/login/controller-login.service.ts | 6 +- .../src/app/login/login.component.ts | 2 +- .../src/app/login/node-login.service.ts | 2 + .../src/lib/assets/scripts/growler.js | 4 ++ .../wrappers/zac-wrapper.component.ts | 2 +- .../src/lib/services/login-service.class.ts | 1 - .../src/lib/services/node-data.service.ts | 7 +- .../src/lib/services/noop-login.service.ts | 4 -- .../lib/services/settings-service.class.ts | 1 + .../src/lib/services/settings.service.ts | 6 ++ .../src/lib/services/ziti-data.service.ts | 3 +- 14 files changed, 53 insertions(+), 69 deletions(-) diff --git a/projects/app-ziti-console/src/app/app.component.ts b/projects/app-ziti-console/src/app/app.component.ts index f0707ae6..0176ef78 100644 --- a/projects/app-ziti-console/src/app/app.component.ts +++ b/projects/app-ziti-console/src/app/app.component.ts @@ -54,15 +54,11 @@ export class AppComponent implements OnInit { } async checkSession() { - if (this.loginService.hasSession()) { - this.isAuthorized = true; - return Promise.resolve(); - } else { - this.isAuthorized = false; + this.isAuthorized = this.settingsService.hasSession(); + if (!this.isAuthorized) { this.router.navigate(['/login']); - this.isAuthorized = false; - return Promise.resolve(); } + return Promise.resolve(); } handleUserSettings() { diff --git a/projects/app-ziti-console/src/app/guards/authentication.guard.ts b/projects/app-ziti-console/src/app/guards/authentication.guard.ts index 1c88bb26..0fbd1375 100644 --- a/projects/app-ziti-console/src/app/guards/authentication.guard.ts +++ b/projects/app-ziti-console/src/app/guards/authentication.guard.ts @@ -39,7 +39,7 @@ export class AuthenticationGuard { } canActivate(next, state) { - const isAuthorized = this.loginService.hasSession(); + const isAuthorized = this.settingsSvc.hasSession(); if (!isAuthorized) { // messaging.error('not authorized'); this.settingsSvc.set(this.settingsSvc.settings); diff --git a/projects/app-ziti-console/src/app/interceptors/ziti-api.interceptor.ts b/projects/app-ziti-console/src/app/interceptors/ziti-api.interceptor.ts index fb81bc2d..2e952641 100644 --- a/projects/app-ziti-console/src/app/interceptors/ziti-api.interceptor.ts +++ b/projects/app-ziti-console/src/app/interceptors/ziti-api.interceptor.ts @@ -18,7 +18,7 @@ import {Injectable, Inject} from '@angular/core'; import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; import {map} from 'rxjs/operators'; -import {BehaviorSubject, filter, finalize, Observable, of, switchMap, take, EMPTY} from 'rxjs'; +import {BehaviorSubject, filter, finalize, Observable, of, switchMap, take, EMPTY, catchError, throwError} from 'rxjs'; import { SettingsServiceClass, LoginServiceClass, @@ -32,13 +32,14 @@ import {MatDialog} from "@angular/material/dialog"; import {defer} from 'lodash'; +// @ts-ignore +const {growler} = window; + /** Pass untouched request through to the next request handler. */ @Injectable({ providedIn: 'root' }) export class ZitiApiInterceptor implements HttpInterceptor { - private refreshTokenInProgress = false; - private refreshTokenSubject = new BehaviorSubject(null); constructor(@Inject(SETTINGS_SERVICE) private settingsService: SettingsServiceClass, @Inject(ZAC_LOGIN_SERVICE) private loginService: LoginServiceClass, @@ -49,6 +50,27 @@ export class ZitiApiInterceptor implements HttpInterceptor { } + private handleErrorResponse(err: HttpErrorResponse): Observable { + if (err.status === 401) { + // User is unauthorized. Show growler/warning and redirect user back to login page + const gorwlerData: GrowlerModel = new GrowlerModel('warning', 'Invalid Session', 'Session Expired', 'Your session is no longer valid. Please login to continue.'); + growler.disabled = true; + defer(() => { + this.dialogRef.closeAll(); + this.growlerService.show(gorwlerData); + growler.disabled = false; + }); + if (this.settingsService?.settings?.session) { + this.settingsService.settings.session.id = undefined; + this.settingsService.settings.session.expiresAt = undefined; + this.settingsService.set(this.settingsService.settings); + } + this.router.navigate(['/login']); + return of(err.message); // or EMPTY may be appropriate here + } + return throwError(() => err); + } + intercept(req: HttpRequest, next: HttpHandler): Observable> { @@ -58,52 +80,10 @@ export class ZitiApiInterceptor implements HttpInterceptor { ) { return next.handle(req); } else { - const session = this.settingsService.settings.session; - const tokenExpirationDate = moment(session.expiresAt); - const expTime = tokenExpirationDate.diff(moment(), 'seconds'); - if (session?.id && expTime > 10) { - // I have everything. add token and continue - return next.handle(this.addAuthToken(req)); - } else if (this.refreshTokenInProgress) { - // I have already requested a new token, - // when its finished, continue this request - return this.refreshTokenSubject.pipe( - filter((result) => result), - take(1), - switchMap(() => next.handle(this.addAuthToken(req))) - ); - } else { - this.handleUnauthorized(); - return EMPTY; - } + return next.handle(this.addAuthToken(req)).pipe(catchError(err=> this.handleErrorResponse(err))); } } - private refreshAuthToken() { - this.refreshTokenInProgress = true; - this.refreshTokenSubject.next(null); - const apiVersions = this.settingsService.apiVersions; - const prefix = apiVersions["edge-management"].v1.path; - const url = this.settingsService.settings.selectedEdgeController; - const controllerId = this.settingsService.settings.session.username.trim(); - const controllerPassword = this.settingsService.settings.session.password; - if (prefix && url && controllerId && controllerPassword) { - const serviceUrl = url + prefix; - return this.loginService.observeLogin(serviceUrl, controllerId, controllerPassword); - } - this.router.navigate(['/login']); - return of(null); - } - - private handleUnauthorized() { - const gorwlerData: GrowlerModel = new GrowlerModel('warning', 'Invalid Session', 'Session Expired', 'Your session is no longer valid. Please login to continue.'); - this.growlerService.show(gorwlerData); - defer(() => { - this.dialogRef.closeAll(); - }); - this.router.navigate(['/login']); - } - private addAuthToken(request: any) { const session = this.settingsService.settings.session; return request.clone({setHeaders: {"zt-session": session.id, 'content-type': 'application/json', accept: 'application/json'}}); diff --git a/projects/app-ziti-console/src/app/login/controller-login.service.ts b/projects/app-ziti-console/src/app/login/controller-login.service.ts index 53a42162..145e01f8 100644 --- a/projects/app-ziti-console/src/app/login/controller-login.service.ts +++ b/projects/app-ziti-console/src/app/login/controller-login.service.ts @@ -122,6 +122,7 @@ export class ControllerLoginService extends LoginServiceClass { controllerDomain: this.domain, authorization: 100, expiresAt: body.data.expiresAt, + expirationSeconds: body.data.expirationSeconds, username, password } @@ -130,11 +131,6 @@ export class ControllerLoginService extends LoginServiceClass { return of([body.data?.token]); } - hasSession() { - const hasSessionId = !isEmpty(this.settingsService?.settings?.session?.id); - const sessionExpired = moment(this.settingsService?.settings?.session?.expiresAt).isBefore(new Date()); - return hasSessionId && !sessionExpired; - } logout() { localStorage.removeItem('ziti.settings'); diff --git a/projects/app-ziti-console/src/app/login/login.component.ts b/projects/app-ziti-console/src/app/login/login.component.ts index 7ed0ad7d..993c5ab7 100644 --- a/projects/app-ziti-console/src/app/login/login.component.ts +++ b/projects/app-ziti-console/src/app/login/login.component.ts @@ -55,7 +55,7 @@ export class LoginComponent implements OnInit, OnDestroy { if (this.svc.originIsController !== false && this.svc.originIsController !== true) { this.checkOriginForController(); } - if (this.svc.hasSession()) { + if (this.settingsService.hasSession()) { this.router.navigate(['/dashboard']); } this.subscription.add( diff --git a/projects/app-ziti-console/src/app/login/node-login.service.ts b/projects/app-ziti-console/src/app/login/node-login.service.ts index ebb834ab..f34ddb2b 100644 --- a/projects/app-ziti-console/src/app/login/node-login.service.ts +++ b/projects/app-ziti-console/src/app/login/node-login.service.ts @@ -123,6 +123,8 @@ export class NodeLoginService extends LoginServiceClass { return this.httpClient.post(apiUrl, body, options).toPromise().then((resp: any) => { //just checking for a non-error response to see if there is a valid session with the node server this.hasNodeSession = isEmpty(resp?.error); + this.settingsService.hasNodeSession = this.hasNodeSession; + this.settingsService.set(this.settingsService.settings); return this.hasNodeSession; }).catch((resp) => { this.hasNodeSession = false; diff --git a/projects/ziti-console-lib/src/lib/assets/scripts/growler.js b/projects/ziti-console-lib/src/lib/assets/scripts/growler.js index 745ac4f7..1ba33a4b 100644 --- a/projects/ziti-console-lib/src/lib/assets/scripts/growler.js +++ b/projects/ziti-console-lib/src/lib/assets/scripts/growler.js @@ -15,6 +15,7 @@ */ var growler = { + disabled: false, isDebugging: true, showId: -1, data: [], @@ -77,6 +78,9 @@ var growler = { } }, show: function(type, title, subtitle, message) { + if (growler.disabled) { + return; + } if (growler.showId!=-1) clearTimeout(growler.showId); $("#Growler").removeClass("open"); $("#Growler").removeClass("success"); diff --git a/projects/ziti-console-lib/src/lib/features/wrappers/zac-wrapper.component.ts b/projects/ziti-console-lib/src/lib/features/wrappers/zac-wrapper.component.ts index c33f7d58..358c5f8d 100644 --- a/projects/ziti-console-lib/src/lib/features/wrappers/zac-wrapper.component.ts +++ b/projects/ziti-console-lib/src/lib/features/wrappers/zac-wrapper.component.ts @@ -71,7 +71,7 @@ export class ZacWrapperComponent implements OnInit, OnDestroy { ); this.subscription.add( this.settingsService.settingsChange.subscribe(async (results:any) => { - const hasSession = !isEmpty(results?.session?.id); + const hasSession = this.settingsService.hasSession(); const controllerChanged = this.settings?.session?.controllerDomain !== results?.session?.controllerDomain; if (hasSession && (!this.pageLoading || controllerChanged)) { this.pageLoading = true; diff --git a/projects/ziti-console-lib/src/lib/services/login-service.class.ts b/projects/ziti-console-lib/src/lib/services/login-service.class.ts index 2d14bdcd..fb920d23 100644 --- a/projects/ziti-console-lib/src/lib/services/login-service.class.ts +++ b/projects/ziti-console-lib/src/lib/services/login-service.class.ts @@ -16,7 +16,6 @@ export abstract class LoginServiceClass { abstract init(); abstract login(prefix: string, url: string, username: string, password: string); abstract observeLogin(serviceUrl: string, username: string, password: string); - abstract hasSession(): boolean; abstract clearSession(); abstract checkOriginForController(): Promise; abstract logout(); diff --git a/projects/ziti-console-lib/src/lib/services/node-data.service.ts b/projects/ziti-console-lib/src/lib/services/node-data.service.ts index b0c03f0b..91113fd8 100644 --- a/projects/ziti-console-lib/src/lib/services/node-data.service.ts +++ b/projects/ziti-console-lib/src/lib/services/node-data.service.ts @@ -23,7 +23,7 @@ import {firstValueFrom, map} from "rxjs"; import {catchError} from "rxjs/operators"; import {HttpClient} from "@angular/common/http"; import {FilterObj} from "../features/data-table/data-table-filter.service"; -import {isEmpty, get, isArray, isNumber} from "lodash"; +import {isEmpty, get, isArray, isNumber, set} from "lodash"; import {ZitiDataService} from "./ziti-data.service"; import {Resolver} from "@stoplight/json-ref-resolver"; import moment from "moment"; @@ -346,7 +346,10 @@ export class NodeDataService extends ZitiDataService { handleError(results) { if (results?.errorObj?.code === 'UNAUTHORIZED') { localStorage.removeItem('ziti.settings'); - window.location.href = '/login'; + //window.location.href = '/login'; + this.router.navigate(['/login']); + set(this.settingsService, 'hasNodeSession', false); + this.settingsService.set(this.settingsService.settings); } else { this.logger.error(results?.error) } diff --git a/projects/ziti-console-lib/src/lib/services/noop-login.service.ts b/projects/ziti-console-lib/src/lib/services/noop-login.service.ts index 58cb2487..839e0a3c 100644 --- a/projects/ziti-console-lib/src/lib/services/noop-login.service.ts +++ b/projects/ziti-console-lib/src/lib/services/noop-login.service.ts @@ -25,10 +25,6 @@ export class NoopLoginService extends LoginServiceClass { clearSession() { } - hasSession(): boolean { - return false; - } - login(prefix: string, url: string, username: string, password: string) { } diff --git a/projects/ziti-console-lib/src/lib/services/settings-service.class.ts b/projects/ziti-console-lib/src/lib/services/settings-service.class.ts index f0a29ab3..261f0dec 100644 --- a/projects/ziti-console-lib/src/lib/services/settings-service.class.ts +++ b/projects/ziti-console-lib/src/lib/services/settings-service.class.ts @@ -80,6 +80,7 @@ export abstract class SettingsServiceClass { public abstract controllerSave(name: string, url: string); public abstract initApiVersions(url); public abstract loadSettings(); + public abstract hasSession(); public get() { const tmp = localStorage.getItem('ziti.settings'); diff --git a/projects/ziti-console-lib/src/lib/services/settings.service.ts b/projects/ziti-console-lib/src/lib/services/settings.service.ts index 9cca331b..77aa758f 100644 --- a/projects/ziti-console-lib/src/lib/services/settings.service.ts +++ b/projects/ziti-console-lib/src/lib/services/settings.service.ts @@ -22,6 +22,7 @@ import {HttpBackend, HttpClient} from "@angular/common/http"; import {SettingsServiceClass} from "./settings-service.class"; import {GrowlerService} from "../features/messaging/growler.service"; import {GrowlerModel} from "../features/messaging/growler.model"; +import moment from "moment/moment"; export const SETTINGS_SERVICE = new InjectionToken('SETTINGS_SERVICE'); @@ -102,6 +103,11 @@ export class SettingsService extends SettingsServiceClass { //this is a no-op for the default settings service } + override hasSession() { + const hasSessionId = !isEmpty(this.settings?.session?.id); + return hasSessionId; + } + override controllerSave(name: string, url: string) { url = url.split('#').join('').split('?').join(''); if (url.endsWith('/')) url = url.substr(0, url.length - 1); diff --git a/projects/ziti-console-lib/src/lib/services/ziti-data.service.ts b/projects/ziti-console-lib/src/lib/services/ziti-data.service.ts index 6e006a45..b0e832b1 100644 --- a/projects/ziti-console-lib/src/lib/services/ziti-data.service.ts +++ b/projects/ziti-console-lib/src/lib/services/ziti-data.service.ts @@ -24,6 +24,7 @@ import {FilterObj} from "../features/data-table/data-table-filter.service"; import { LoginServiceClass } from './login-service.class'; import {cloneDeep, isEmpty, sortedUniq} from "lodash"; +import {SettingsServiceClass} from "./settings-service.class"; export const ZITI_DATA_SERVICE = new InjectionToken('ZITI_DATA_SERVICE'); @@ -46,7 +47,7 @@ export abstract class ZitiDataService { constructor(protected logger: LoggerService, protected growler: GrowlerService, - protected settingsService: SettingsService, + protected settingsService: SettingsServiceClass, protected httpClient: HttpClient, protected router: Router ) {}