From dcc58eccbd6c73cd7c6ac3b9050467e9e7fec325 Mon Sep 17 00:00:00 2001 From: Youssef MAHTAT Date: Sun, 12 Jan 2025 13:25:55 +0100 Subject: [PATCH] MOSIP-37079 - Enhance Session Timeout Management and Extend Auto-Logout Feature Signed-off-by: Youssef MAHTAT --- pre-registration-ui/src/app/app.component.ts | 157 ++++--- .../src/app/core/core.module.ts | 39 +- .../app/core/services/auto-logout.service.ts | 432 +++++++++++------- .../dashboard/dashboard.component.ts | 1 + .../shared/cache-interceptor.service.spec.ts | 12 + .../app/shared/cache-interceptor.service.ts | 27 ++ 6 files changed, 443 insertions(+), 225 deletions(-) create mode 100644 pre-registration-ui/src/app/shared/cache-interceptor.service.spec.ts create mode 100644 pre-registration-ui/src/app/shared/cache-interceptor.service.ts diff --git a/pre-registration-ui/src/app/app.component.ts b/pre-registration-ui/src/app/app.component.ts index cb27c853..5abd1bb8 100644 --- a/pre-registration-ui/src/app/app.component.ts +++ b/pre-registration-ui/src/app/app.component.ts @@ -1,61 +1,108 @@ -import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; -import { Event as NavigationEvent, Router, NavigationStart } from '@angular/router'; -import { filter } from 'rxjs/operators'; +import {Component, HostListener, OnDestroy, OnInit} from '@angular/core'; +import {Event as NavigationEvent, NavigationStart, Router} from '@angular/router'; +import {filter} from 'rxjs/operators'; -import { AutoLogoutService } from 'src/app/core/services/auto-logout.service'; -import { ConfigService } from './core/services/config.service'; -import { Subscription } from 'rxjs'; +import {AutoLogoutService} from 'src/app/core/services/auto-logout.service'; +import {ConfigService} from './core/services/config.service'; +import {Subscription} from 'rxjs'; @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit, OnDestroy { - title = 'pre-registration'; - message: object; - subscriptions: Subscription[] = []; - - constructor(private autoLogout: AutoLogoutService, private router: Router, private configService: ConfigService) {} - - ngOnInit() { - this.subscriptions.push(this.autoLogout.currentMessageAutoLogout.subscribe(() => { - // This is intentional - })); - this.autoLogout.changeMessage({ timerFired: false }); - this.routerType(); - } - - routerType() { - this.subscriptions.push( - this.router.events - .pipe(filter((event: NavigationEvent) => event instanceof NavigationStart)) - .subscribe((event: NavigationStart) => { - if (event.restoredState) { - // This is intentional - } - }) - ); - } - - // preventBack() { - // window.history.forward(); - // window.onunload = function() { - // null; - // console.log("Hello") - // }; - // } - - @HostListener('mouseover') - @HostListener('document:mousemove', ['$event']) - @HostListener('keypress') - @HostListener('click') - @HostListener('document:keypress', ['$event']) - onMouseClick() { - this.autoLogout.setisActive(true); - } - - ngOnDestroy(): void { - this.subscriptions.forEach(subscription => subscription.unsubscribe()); - } + title = 'pre-registration'; + message: object; + subscriptions: Subscription[] = []; + + constructor(private autoLogout: AutoLogoutService, private router: Router, private configService: ConfigService) { + } + + ngOnInit() { + this.subscriptions.push(this.autoLogout.currentMessageAutoLogout.subscribe(() => { + // This is intentional + })); + this.autoLogout.changeMessage({timerFired: false}); + this.routerType(); + } + + routerType() { + this.subscriptions.push( + this.router.events + .pipe(filter((event: NavigationEvent) => event instanceof NavigationStart)) + .subscribe((event: NavigationStart) => { + if (event.restoredState) { + // This is intentional + } + }) + ); + } + + // preventBack() { + // window.history.forward(); + // window.onunload = function() { + // null; + // console.log("Hello") + // }; + // } + + @HostListener('mouseover') + @HostListener('document:mousemove', ['$event']) + @HostListener('keypress') + @HostListener('click') + @HostListener('document:keypress', ['$event']) + onMouseClick() { + this.autoLogout.setisActive(true); + this.updateLastActionTimestamp(); // Added as part of MOSIP-37079 + } + + + @HostListener('window:load', ['$event']) + @HostListener('document:visibilitychange', ['$event']) + @HostListener('window:beforeunload', ['$event']) + onVisibilityChange(event?: Event) { + console.warn(`Event detected: '${event.type}'.`); + this.onVisibilityChangeCheckSessionHandler(event); // Added as part of MOSIP-37079 + } + + /** + * @description Detects when the page becomes hidden or visible and validates the session. + * Implemented as part of MOSIP-37079 to handle session timeout when the user switches tabs. + * + * @param event + * @private + */ + private onVisibilityChangeCheckSessionHandler(event?: Event) { + const isHiddenState: boolean = (('beforeunload' === event.type) || ('hidden' === document.visibilityState)); + if (isHiddenState) { + this.updateHiddenTimestamp(); + } else { + this.autoLogout.validateHiddenState(); + } + } + + /** + * @description Updates the timestamp of the last user action in sessionStorage. + * Implemented as part of MOSIP-37079 to track user activity for session timeout management. + */ + private updateLastActionTimestamp() { + sessionStorage.setItem('lastActionTimestamp', Date.now().toString()); + } + + /** + * @description Updates the timestamp of hidden page action in sessionStorage. + * Implemented as part of MOSIP-37079 to track user activity for session timeout management. + */ + private updateHiddenTimestamp() { + let lastActionTimestampValue: string = (sessionStorage.getItem('lastActionTimestamp')); + if (!lastActionTimestampValue) { + lastActionTimestampValue = Date.now().toString(); + } + sessionStorage.setItem('hiddenTimestamp', lastActionTimestampValue); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(subscription => subscription.unsubscribe()); + } } diff --git a/pre-registration-ui/src/app/core/core.module.ts b/pre-registration-ui/src/app/core/core.module.ts index e9207906..2b2dbb59 100644 --- a/pre-registration-ui/src/app/core/core.module.ts +++ b/pre-registration-ui/src/app/core/core.module.ts @@ -1,20 +1,27 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; -import { AboutUsComponent } from './about-us/about-us.component'; -import { FaqComponent } from './faq/faq.component'; -import { HeaderComponent } from './header/header.component'; -import { FooterComponent } from './footer/footer.component'; -import { ContactComponent } from './contact/contact.component'; -import { AppRoutingModule } from '../app-routing.module'; -import { SharedModule } from '../shared/shared.module'; -import { AuthInterceptorService } from '../shared/auth-interceptor.service'; +import {AboutUsComponent} from './about-us/about-us.component'; +import {FaqComponent} from './faq/faq.component'; +import {HeaderComponent} from './header/header.component'; +import {FooterComponent} from './footer/footer.component'; +import {ContactComponent} from './contact/contact.component'; +import {AppRoutingModule} from '../app-routing.module'; +import {SharedModule} from '../shared/shared.module'; +import {AuthInterceptorService} from '../shared/auth-interceptor.service'; +import {CacheInterceptorService} from "../shared/cache-interceptor.service"; @NgModule({ - imports: [CommonModule, AppRoutingModule, SharedModule], - declarations: [HeaderComponent, FooterComponent, AboutUsComponent, FaqComponent, ContactComponent], - exports: [HeaderComponent, FooterComponent, SharedModule], - providers: [{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptorService, multi: true }] + imports: [CommonModule, AppRoutingModule, SharedModule], + declarations: [HeaderComponent, FooterComponent, AboutUsComponent, FaqComponent, ContactComponent], + exports: [HeaderComponent, FooterComponent, SharedModule], + providers: [ + {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptorService, multi: true}, + // CacheInterceptorService Added as part of MOSIP-37079 to ensure session data is not restored from cache. + {provide: HTTP_INTERCEPTORS, useClass: CacheInterceptorService, multi: true}, + ], }) -export class CoreModule {} +export class CoreModule { +} + diff --git a/pre-registration-ui/src/app/core/services/auto-logout.service.ts b/pre-registration-ui/src/app/core/services/auto-logout.service.ts index ffbda638..17f7aa2e 100644 --- a/pre-registration-ui/src/app/core/services/auto-logout.service.ts +++ b/pre-registration-ui/src/app/core/services/auto-logout.service.ts @@ -1,12 +1,12 @@ -import { Injectable } from '@angular/core'; -import { UserIdleService, UserIdleConfig } from 'angular-user-idle'; -import { AuthService } from 'src/app/auth/auth.service'; -import { MatDialog } from '@angular/material'; -import { DialougComponent } from 'src/app/shared/dialoug/dialoug.component'; -import { BehaviorSubject } from 'rxjs'; -import { ConfigService } from 'src/app/core/services/config.service'; +import {Injectable} from '@angular/core'; +import {UserIdleConfig, UserIdleService} from 'angular-user-idle'; +import {AuthService} from 'src/app/auth/auth.service'; +import {MatDialog} from '@angular/material'; +import {DialougComponent} from 'src/app/shared/dialoug/dialoug.component'; +import {BehaviorSubject} from 'rxjs'; +import {ConfigService} from 'src/app/core/services/config.service'; import * as appConstants from 'src/app/app.constants'; -import { DataStorageService } from './data-storage.service'; +import {DataStorageService} from './data-storage.service'; /** * @description This class is responsible for auto logging out user when he is inactive for a @@ -17,153 +17,277 @@ import { DataStorageService } from './data-storage.service'; */ @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class AutoLogoutService { - private messageAutoLogout = new BehaviorSubject({}); - currentMessageAutoLogout = this.messageAutoLogout.asObservable(); - isActive = false; - - timer = new UserIdleConfig(); - languagelabels: any; - langCode = localStorage.getItem('langCode'); - - idle: number; - timeout: number; - ping: number; - dialogref; - dialogreflogout; - - constructor( - private userIdle: UserIdleService, - private authService: AuthService, - private dialog: MatDialog, - private configservice: ConfigService, - private dataStorageService: DataStorageService - ) {} - - /** - * @description This method gets value of idle,timeout and ping parameter from config file. - * - * @returns void - * @memberof AutoLogoutService - */ - getValues(langCode) { - this.idle = Number(this.configservice.getConfigByKey(appConstants.CONFIG_KEYS.mosip_preregistration_auto_logout_idle)) - this.timeout = Number(this.configservice.getConfigByKey(appConstants.CONFIG_KEYS.mosip_preregistration_auto_logout_timeout)) - this.ping = Number(this.configservice.getConfigByKey(appConstants.CONFIG_KEYS.mosip_preregistration_auto_logout_ping)) - this.dataStorageService - .getI18NLanguageFiles(langCode) - .subscribe((response) => { - this.languagelabels = response['autologout']; - }); - - } - - setisActive(value: boolean) { - this.isActive = value; - } - getisActive() { - return this.isActive; - } - - changeMessage(message: object) { - this.messageAutoLogout.next(message); - } - /** - * @description This method sets value of idle,timeout and ping parameter from config file. - * - * @returns void - * @memberof AutoLogoutService - */ - setValues() { - this.timer.idle = this.idle; - this.timer.ping = this.ping; - this.timer.timeout = this.timeout; - this.userIdle.setConfigValues(this.timer); - } - - /** - * @description This method is fired when dashboard gets loaded and starts the timer to watch for - * user idle. onTimerStart() is fired when user idle has been detected for specified time. - * After that onTimeout() is fired. - * - * @returns void - * @memberof AutoLogoutService - */ - - public keepWatching() { - this.userIdle.startWatching(); - this.changeMessage({ timerFired: true }); - - this.userIdle.onTimerStart().subscribe( - res => { - if (res == 1) { - this.setisActive(false); - this.openPopUp(); - } else { - if (this.isActive) { - if (this.dialogref) this.dialogref.close(); - } + private messageAutoLogout = new BehaviorSubject({}); + currentMessageAutoLogout = this.messageAutoLogout.asObservable(); + isActive = false; + + timer = new UserIdleConfig(); + languagelabels: any; + langCode = localStorage.getItem('langCode'); + + idle: number; + timeout: number; + ping: number; + dialogref; + dialogreflogout; + + constructor( + private userIdle: UserIdleService, + private authService: AuthService, + private dialog: MatDialog, + private configservice: ConfigService, + private dataStorageService: DataStorageService + ) { + } + + /** + * @description This method gets value of idle,timeout and ping parameter from config file. + * + * @returns void + * @memberof AutoLogoutService + */ + getValues(langCode) { + this.idle = Number(this.configservice.getConfigByKey(appConstants.CONFIG_KEYS.mosip_preregistration_auto_logout_idle)) + this.timeout = Number(this.configservice.getConfigByKey(appConstants.CONFIG_KEYS.mosip_preregistration_auto_logout_timeout)) + this.ping = Number(this.configservice.getConfigByKey(appConstants.CONFIG_KEYS.mosip_preregistration_auto_logout_ping)) + this.dataStorageService + .getI18NLanguageFiles(langCode) + .subscribe((response) => { + this.languagelabels = response['autologout']; + }); + + } + + setisActive(value: boolean) { + this.isActive = value; + } + + getisActive() { + return this.isActive; + } + + changeMessage(message: object) { + this.messageAutoLogout.next(message); + } + + /** + * @description This method sets value of idle,timeout and ping parameter from config file. + * + * @returns void + * @memberof AutoLogoutService + */ + setValues() { + this.timer.idle = this.idle; + this.timer.ping = this.ping; + this.timer.timeout = this.timeout; + this.userIdle.setConfigValues(this.timer); + } + + /** + * @description This method is fired when dashboard gets loaded and starts the timer to watch for + * user idle. onTimerStart() is fired when user idle has been detected for specified time. + * After that onTimeout() is fired. + * + * @returns void + * @memberof AutoLogoutService + */ + public keepWatching() { + this.userIdle.startWatching(); + this.changeMessage({timerFired: true}); + + this.userIdle.onTimerStart().subscribe( + res => { + if (res == 1) { + this.setisActive(false); + this.openPopUp(); + } else { + if (this.isActive) { + if (this.dialogref) this.dialogref.close(); + } + } + }, + () => { + // This is intentional + }, + () => { + // This is intentional + } + ); + + this.userIdle.onTimeout().subscribe(() => { + if (!this.isActive) { + this.onLogOut(); + } else { + this.userIdle.resetTimer(); + } + }); + } + + public continueWatching() { + this.userIdle.startWatching(); + } + + + /** + * @description Starts periodic validation of user activity. + * Ensures that session remains valid during application usage. + */ + public startRegularValidation(): void { + if (this.timeout) { + const that = this; + // Validation interval set to half the timeout duration : + const intervalDelay = (this.timeout * 1000) / 2; + // Set Interval for regular checking : + setInterval(() => { + that.validateUserActivity(); // Validate user activity periodically + }, intervalDelay); // Validation interval set to half the timeout duration } - }, - () => { - // This is intentional - }, - () => { - // This is intentional - } - ); - - this.userIdle.onTimeout().subscribe(() => { - if (!this.isActive) { - this.onLogOut(); - } else { - this.userIdle.resetTimer(); - } - }); - } - - public continueWatching() { - this.userIdle.startWatching(); - } - /** - * @description This methoed is used to logged out the user. - * - * @returns void - * @memberof AutoLogoutService - */ - onLogOut() { - this.dialogref.close(); - this.dialog.closeAll(); - this.userIdle.stopWatching(); - this.popUpPostLogOut(); - this.authService.onLogout(); - } - - /** - * @description This methoed opens a pop up when user idle has been detected for given time.id - * @memberof AutoLogoutService - */ - - openPopUp() { - const data = { - case: 'POPUP', - content: this.languagelabels.preview - }; - this.dialogref = this.dialog.open(DialougComponent, { - width: '400px', - data: data - }); - } - popUpPostLogOut() { - const data = { - case: 'POSTLOGOUT', - contentLogout: this.languagelabels.post - }; - this.dialogreflogout = this.dialog.open(DialougComponent, { - width: '400px', - data: data - }); - } + } + + /** + * @description Validates user inactivity based on the last action timestamp. + * If the time since the last user action exceeds the timeout, the user is logged out. + * Implemented as part of MOSIP-37079 to handle session timeout due to user inactivity. + * @returns void + */ + public validateUserActivity(): void { + const lastActionTimestampValue = (sessionStorage.getItem('lastActionTimestamp') || '0'); + const lastActionTimestamp = parseInt(lastActionTimestampValue); + this.validateActionActivityBasedOnConfiguredIdleTimeout(lastActionTimestamp); + } + + /** + * @description Validates inactivity based on the hidden state timestamp or last action timestamp. + * If the time since the page was hidden exceeds the timeout, the user is logged out. + * Implemented as part of MOSIP-37079 to handle session timeout due to hidden page state. + * @returns void + */ + public validateHiddenState(): void { + let lastActivityTimestampValue = (sessionStorage.getItem('hiddenTimestamp')); + if (!lastActivityTimestampValue) { + lastActivityTimestampValue = (sessionStorage.getItem('lastActionTimestamp') || '0'); + } + const lastActivityTimestamp = parseInt(lastActivityTimestampValue); + this.validateActionActivityBasedOnConfiguredTimeout(lastActivityTimestamp); + } + + + /** + * @description Validates user inactivity based on configured idle timeout and action timestamp. + * If the time since the user action exceeds the timeout, the user is logged out. + * Implemented as part of MOSIP-37079 to handle session timeout due to user inactivity. + * + * @param actionTimestamp + * @private + */ + private validateActionActivityBasedOnConfiguredIdleTimeout(actionTimestamp: number): void { + if (this.authService.isAuthenticated() && actionTimestamp) { + this.validateActionActivityTimeout(this.idle, actionTimestamp); + } + } + + /** + * @description Validates user inactivity based on configuration timeout and action timestamp. + * If the time since the user action exceeds the timeout, the user is logged out. + * Implemented as part of MOSIP-37079 to handle session timeout due to user inactivity. + * + * @param actionTimestamp + * @private + */ + private validateActionActivityBasedOnConfiguredTimeout(actionTimestamp: number): void { + if (this.authService.isAuthenticated() && actionTimestamp) { + this.validateActionActivityTimeout(this.timeout, actionTimestamp); + } + } + + /** + * @description Validates user inactivity based on given timeout and action timestamp. + * If the time since the user action exceeds the timeout, the user is logged out. + * Implemented as part of MOSIP-37079 to handle session timeout due to user inactivity. + * + * @param timeout + * @param actionTimestamp + * @private + */ + private validateActionActivityTimeout(timeout: number, actionTimestamp: number): void { + if (this.authService.isAuthenticated() && actionTimestamp) { + const currentTime = Date.now(); + const timeoutDelay = (timeout * 1000); + const inactivityDelay = (currentTime - actionTimestamp); + const isSessionTimeout = (inactivityDelay > timeoutDelay); + if (isSessionTimeout) { + console.warn('Session timed out due to user inactivity.'); + this.logoutAndClearSessionData(); // Automatically logout the user + } + } + } + + + /** + * @description This methoed is used to logged out the user. + * + * @returns void + * @memberof AutoLogoutService + */ + onLogOut() { + this.dialogref.close(); + this.dialog.closeAll(); + this.userIdle.stopWatching(); + this.popUpPostLogOut(); + this.logoutAndClearSessionData(); // updated as part of MOSIP-37079 + } + + /** + * @description This methoed is used to logged out the user. + * Implemented as part of MOSIP-37079 to handle session timeout due to user inactivity. + * + * @returns void + * @memberof AutoLogoutService + */ + logoutAndClearSessionData(): void { + this.clearSessionData(); // Added as part of MOSIP-37079 + this.authService.onLogout(); + } + + + /** + * @description Clears all session-related data in sessionStorage. + * This method ensures a clean state by removing all user-related data during logout or timeout. + * Implemented as part of MOSIP-37079 for secure session management. + * @returns void + */ + private clearSessionData(): void { + sessionStorage.clear(); + console.info('All sessions, storages, and cookies have been cleared successfully.'); + } + + /** + * @description This methoed opens a pop up when user idle has been detected for given time.id + * @memberof AutoLogoutService + */ + + openPopUp() { + const data = { + case: 'POPUP', + content: this.languagelabels.preview + }; + this.dialogref = this.dialog.open(DialougComponent, { + width: '400px', + data: data + }); + } + + popUpPostLogOut() { + const data = { + case: 'POSTLOGOUT', + contentLogout: this.languagelabels.post + }; + this.dialogreflogout = this.dialog.open(DialougComponent, { + width: '400px', + data: data + }); + } } diff --git a/pre-registration-ui/src/app/feature/dashboard/dashboard/dashboard.component.ts b/pre-registration-ui/src/app/feature/dashboard/dashboard/dashboard.component.ts index 70e2b2db..6e2ea78e 100644 --- a/pre-registration-ui/src/app/feature/dashboard/dashboard/dashboard.component.ts +++ b/pre-registration-ui/src/app/feature/dashboard/dashboard/dashboard.component.ts @@ -135,6 +135,7 @@ export class DashBoardComponent implements OnInit, OnDestroy { this.autoLogout.getValues(this.userPreferredLangCode); this.autoLogout.continueWatching(); } + this.autoLogout.startRegularValidation(); // Start regular validation, Added as part of MOSIP-37079 this.regService.setSameAs(""); this.name = this.configService.getConfigByKey( appConstants.CONFIG_KEYS.preregistration_identity_name diff --git a/pre-registration-ui/src/app/shared/cache-interceptor.service.spec.ts b/pre-registration-ui/src/app/shared/cache-interceptor.service.spec.ts new file mode 100644 index 00000000..82181df0 --- /dev/null +++ b/pre-registration-ui/src/app/shared/cache-interceptor.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { CacheInterceptorService } from './cache-interceptor.service'; + +describe('CacheInterceptorService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: CacheInterceptorService = TestBed.get(CacheInterceptorService); + expect(service).toBeTruthy(); + }); +}); diff --git a/pre-registration-ui/src/app/shared/cache-interceptor.service.ts b/pre-registration-ui/src/app/shared/cache-interceptor.service.ts new file mode 100644 index 00000000..eede2b2c --- /dev/null +++ b/pre-registration-ui/src/app/shared/cache-interceptor.service.ts @@ -0,0 +1,27 @@ +import {Injectable} from '@angular/core'; +import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; +import {Observable} from 'rxjs'; + + +@Injectable() +export class CacheInterceptorService implements HttpInterceptor { + + /** + * @description Intercepts HTTP requests to disable caching by adding appropriate headers. + * Implemented as part of MOSIP-37079 to ensure session data is not restored from cache. + * @param req The outgoing HTTP request. + * @param next The HTTP request handler. + * @returns The modified HTTP request. + */ + intercept(req: HttpRequest, next: HttpHandler): Observable> { + const modifiedReq = req.clone({ + setHeaders: { + 'Cache-Control': 'no-store, no-cache, must-revalidate', + Pragma: 'no-cache', + Expires: '0', + }, + }); + return next.handle(modifiedReq); + } + +}