Skip to content

Commit

Permalink
Merge pull request #511 from openziti/510-session-handling
Browse files Browse the repository at this point in the history
Improve ziti session expiration handling
  • Loading branch information
rgallettonf authored Sep 27, 2024
2 parents 9ee4938 + 2ab2f3b commit b4f88ea
Show file tree
Hide file tree
Showing 14 changed files with 53 additions and 69 deletions.
10 changes: 3 additions & 7 deletions projects/app-ziti-console/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -49,6 +50,27 @@ export class ZitiApiInterceptor implements HttpInterceptor {

}

private handleErrorResponse(err: HttpErrorResponse): Observable<any> {
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<any>, next: HttpHandler):
Observable<HttpEvent<any>> {

Expand All @@ -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'}});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export class ControllerLoginService extends LoginServiceClass {
controllerDomain: this.domain,
authorization: 100,
expiresAt: body.data.expiresAt,
expirationSeconds: body.data.expirationSeconds,
username,
password
}
Expand All @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion projects/app-ziti-console/src/app/login/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions projects/app-ziti-console/src/app/login/node-login.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions projects/ziti-console-lib/src/lib/assets/scripts/growler.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

var growler = {
disabled: false,
isDebugging: true,
showId: -1,
data: [],
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
abstract logout();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@ export class NoopLoginService extends LoginServiceClass {
clearSession() {
}

hasSession(): boolean {
return false;
}

login(prefix: string, url: string, username: string, password: string) {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SettingsServiceClass>('SETTINGS_SERVICE');

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ZitiDataService>('ZITI_DATA_SERVICE');

Expand All @@ -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
) {}
Expand Down

0 comments on commit b4f88ea

Please sign in to comment.