22

We are maintaining a session based on user role. We want to implement timeout functionality when the session is idle for 5 min. We are using @ng-idle/core npm module to do that.

My Service file:

 import { ActivatedRouteSnapshot } from '@angular/router';
 import { RouterStateSnapshot } from '@angular/router';
 import {Idle, DEFAULT_INTERRUPTSOURCES, EventTargetInterruptSource} from 
 '@ng-idle/core';
 @Injectable()
export class LoginActService implements CanActivate {
constructor(private authService: APILogService, private router: 
 Router,private idle: Idle) {
  idle.setIdle(10);
  idle.setTimeout(10);
 }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
 ): Observable<boolean>|Promise<boolean>|boolean {
let role = localStorage.getItem('currentUser');

if (localStorage.getItem('currentUser')) {
  if(next.data[0] == role){
   },600000) 
    return true;
  } 
}
else{
  this.router.navigate(['/'], { queryParams: { returnUrl: state.url }});
  return false;
  }
 }
}

For sample, I have used setIdle timeout for 5 seconds, But it is not happening. Can somebody guide me how to do this?

0

7 Answers 7

31

You can use bn-ng-idle npm for user idle / session timeout detection in angular apps. This blog post explanation will help you Learn how to Handle user idleness and session timeout in Angular

npm install bn-ng-idle

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
 
import { AppComponent } from './app.component';
import { BnNgIdleService } from 'bn-ng-idle'; // import bn-ng-idle service
 
 
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [BnNgIdleService], // add it to the providers of your module
  bootstrap: [AppComponent]
})
export class AppModule { }

app.component.ts

import { Component } from '@angular/core';
import { BnNgIdleService } from 'bn-ng-idle'; // import it to your component
 
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
 
  constructor(private bnIdle: BnNgIdleService) { // initiate it in your component constructor
    this.bnIdle.startWatching(300).subscribe((res) => {
      if(res) {
          console.log("session expired");
      }
    })
  }
}

In the above example, I have invoked the startWatching(timeOutSeconds) method with 300 seconds (5 minutes) and subscribed to the observable, once the user is idle for five minute then the subscribe method will get invoked with the res parameter's value (which is a boolean) as true.

By checking whether the res is true or not, you can show your session timeout dialog or message. For brevity, I just logged the message to the console.

4
  • 2
    when the value of "res" would be false? When the call is going inside the subscribe function, res is always true. when to call resetTimer and stopTimer methods?I am trying to show first warning and then final session expire like above,could you please help since not getting enough implementation of same
    – F11
    Commented Jan 8, 2020 at 16:40
  • I am getting error "Uncaught TypeError: Object(...) is not a function" after installing "bn-ng-idle" and following above steps. I am using Angular 6. Any idea why I am facing this issue?
    – rk01
    Commented Dec 1, 2021 at 8:15
  • i got one error after installing the package , node_modules/bn-ng-idle/lib/bn-ng-idle.module.d.ts:4:21 - error TS2694: Namespace '"/node_modules/@angular/core/core"' has no exported member 'ɵɵNgModuleDeclaration'. static ɵfac: i0.ɵɵFactoryDeclaration<BnNgIdleService, never> any idea?
    – mevr
    Commented May 9, 2022 at 9:01
  • @F11 what are the advantages of using bn-ng-idle over ng-idle/core? I have to implement idle timeout on an application and want to figure out which one works better. I see that ng-idel/core has more downloads, at version 11 vs bn-ng-idle is at version 2, and more pull requests. That doesn't always means its better but when I search online to see the main differences I find little to no information.
    – eleon
    Commented Jan 19, 2023 at 22:00
11

Option: 1: angular-user-idle.

Logic

  • Library are waiting for a user's inactive for a 1 minutes (60 seconds).

  • If inactive are detected then onTimerStart() is fired and
    returning a countdown for a 2 minutes (120 seconds).

  • If user did notstop the timer by stopTimer() then time is up after 2 minutes (120 seconds) and onTimeout() is fire.

In AppModule:

@NgModule({
      imports: [
        BrowserModule,

        // Optionally you can set time for `idle`, `timeout` and `ping` in seconds.
        // Default values: `idle` is 600 (10 minutes), `timeout` is 300 (5 minutes) 
        // and `ping` is 120 (2 minutes).
        UserIdleModule.forRoot({idle: 600, timeout: 300, ping: 120})
      ],
      declarations: [AppComponent],
      bootstrap: [AppComponent]
    })

In any of your core componets:

    ngOnInit() {
        //Start watching for user inactivity.
        this.userIdle.startWatching();

        // Start watching when user idle is starting.
        this.userIdle.onTimerStart().subscribe(count => console.log(count));

        // Start watch when time is up.
        this.userIdle.onTimeout().subscribe(() => console.log('Time is up!'));
      }

Bonus: You can use "ping" to make request to refresh token in a given interval of time (e.g. every 10 mins).

Option: 2: Using ngrx

Please refer to the article in the link: https://itnext.io/inactivity-auto-logout-w-angular-and-ngrx-3bcb2fd7983f

1
  • Does it work for only 1 minute idle.. I have set this setting UserIdleModule.forRoot({ idle: 1800, timeout: 600, ping: 1800 }), but still its logging out in few minutes only.. How to set timeout only when user is idle for 30 minutes Commented Jun 30, 2020 at 11:00
9

you can use the code below on the main or parent component. Let's say this is in the admin parent component and am assuming you have an authentication service so that you can know whether the user is logged

declare variables

   userActivity;
   userInactive: Subject<any> = new Subject();

in the constructor or on ngOnInit add

this.setTimeout();
 this.userInactive.subscribe(() => {
   this.logout();
 });  
 logout() {
 this.authService.logout();
 this.authService.redirectLogoutUser();
}

finally add the following methods

    setTimeout() {
    this.userActivity = setTimeout(() => {
      if (this.authService.isLoggedIn) {
        this.userInactive.next(undefined);
        console.log('logged out');
      }
    }, 420000);
  }

  @HostListener('window:mousemove') refreshUserState() {
    clearTimeout(this.userActivity);
    this.setTimeout();
  }
2
  • Working perfectly in angular 8 Commented Mar 4, 2020 at 8:17
  • 5
    This assumes people are using a mouse, which is not always the case (touch-screen devices, screen readers, keyboards, etc.) Commented Sep 3, 2020 at 23:18
1

I have added this.bnIdle.stopTimer() in Angular8 after the timeout, because when I visit the same component there is a glitch in timing.

--> I subscribed and unsubscribed in ngOnDestroy but then to the timer was not stopping.

--> found the stopTimer and implemented it and it is working perfectly fine for me. Hope it will help others.

this.bnIdle.startWatching(300).subscribe((res) => {
          if(res) {
              console.log("session expired");
        this.bnIdle.stopTimer();
          }
        });
0
  timer = 0;      

  setInterval(() => {
      if (window.localStorage['userStoredToken']) {
        let clearTimer = localStorage.getItem('time-limit'); 
        if (clearTimer == 'clear-now') {
          this.timer = 0;
          setTimeout(() => {
            localStorage.removeItem('time-limit');
          }, 5000);
        }
        this.timer++;
        if (this.timer > 2000) { // no of seconds after which user needs to logout
          this.logoutSessionTimeOut();
        }
      }
    }, 1000);


    @HostListener('mouseup', ['$event'])
    @HostListener('mousemove', ['$event'])
    onEvent(event: MouseEvent) {
        this.timer = 0;
        localStorage.setItem('time-limit', "clear-now");
    }
    @HostListener('document:keydown', ['$event']) onKeydownHandler(event: KeyboardEvent) {
        this.timer = 0;
        localStorage.setItem('time-limit', "clear-now");
    }
0

I wasn't thrilled with some approaches I found including some npm packages so I spun up something pretty simple for angular. You would just import the service in your app.component and call it's init() method. The only tricky part was handling the closing of the dialog on action across tabs. It's important to note that windoweventlisteners for storage only react when storage is altered outside of the current document (aka another window or tab).

import { Injectable, NgZone, OnDestroy } from '@angular/core';
import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { UserService } from '@common/services/user.service';
import { takeWhile } from 'rxjs/operators';
import { IdleTimeoutActions, IDLE_LOGOUT_TIME, IDLE_TIMEOUT_ACTION, IDLE_TIMEOUT_TIME } from './IdleTimeoutActions';
import { InactivityTimeoutModalComponent } from './inactivity-timeout-modal/inactivity-timeout-modal.component';


@Injectable({
  providedIn: 'root'
})
export class IdleTimeoutService implements OnDestroy {

  alive = true;
  interval;
  timeoutSetExpiredTime;
  boundUpdateExpiredTime;
  boundOnIdleTimeoutAction;


  inactivityDialogRef: MatDialogRef<InactivityTimeoutModalComponent>;
  dialogConfig: MatDialogConfig = {
    panelClass: ['confirmation-dialog', 'l-w400'],
    disableClose: true
  };
  dialogOpen = false;

  currentStatus;
  constructor(private dialog: MatDialog,
              private userService: UserService,
              private zone: NgZone,
              private router: Router) {}

  init() {
    this.boundUpdateExpiredTime = this.updateExpiredTime.bind(this);
    this.boundOnIdleTimeoutAction = this.onIdleTimeoutAction.bind(this);
    this.userService.isLoggedIn().pipe(takeWhile(x => this.alive)).subscribe(userIsLoggedIn=> {
      if(userIsLoggedIn) {
        this.currentStatus = window.localStorage.getItem(IDLE_TIMEOUT_ACTION);
        if(this.currentStatus ===  IdleTimeoutActions.LOGOUT) { // if the user is logged in, reset the idletimeoutactions to null
          window.localStorage.setItem(IDLE_TIMEOUT_ACTION, null);
        }
        window.addEventListener('storage', this.boundOnIdleTimeoutAction); // handle dialog action events from other tabs
        this.startTrackingIdleTime();
      }
    });
  }

  /**
   * Starts the interval that checks localstorage to determine if the idle expired time is at it's limit
   */
  startTrackingIdleTime() {
    this.addListeners();
    if(window.localStorage.getItem(IDLE_TIMEOUT_ACTION) !== IdleTimeoutActions.IDLE_TRIGGERED) {
      this.updateExpiredTime(0);
    }
    if(this.interval) {
      clearInterval(this.interval);
    }
    this.interval = setInterval(() => {
      const expiredTime = parseInt(localStorage.getItem('_expiredTime'), 10);
      if(expiredTime + (IDLE_LOGOUT_TIME * 1000) < Date.now()) {
        this.triggerLogout();
      } else if (expiredTime < Date.now()) {
        if(!this.dialogOpen) {
          window.localStorage.setItem(IDLE_TIMEOUT_ACTION, IdleTimeoutActions.IDLE_TRIGGERED);
          this.openIdleDialog();
        }
      }
    }, 1000);
  }


  triggerLogout() {
    this.removeListeners();
    // triggers other tabs to logout
    window.localStorage.setItem(IDLE_TIMEOUT_ACTION, IdleTimeoutActions.LOGOUT);
    this.dialog.closeAll();
    this.userService.logout();
    localStorage.setItem(IDLE_TIMEOUT_ACTION, null);
  }

  /**
   * Update the _exporedTime localStorage variable with a new time (timeout used to throttle)
   */
  updateExpiredTime(timeout = 300) {
    if(window.localStorage.getItem(IDLE_TIMEOUT_ACTION) !== IdleTimeoutActions.IDLE_TRIGGERED) {
      if (this.timeoutSetExpiredTime) {
        clearTimeout(this.timeoutSetExpiredTime);
      }
      this.timeoutSetExpiredTime = setTimeout(() => {
        this.zone.run(() => {
          localStorage.setItem('_expiredTime', '' + (Date.now() + (IDLE_TIMEOUT_TIME * 1000)));
        });
      }, timeout);
    }
  }

  addListeners() {
    this.zone.runOutsideAngular(() => {
      window.addEventListener('mousemove', this.boundUpdateExpiredTime);
      window.addEventListener('scroll', this.boundUpdateExpiredTime);
      window.addEventListener('keydown', this.boundUpdateExpiredTime);
    });
  }

  removeListeners() {
    window.removeEventListener('mousemove', this.boundUpdateExpiredTime);
    window.removeEventListener('scroll', this.boundUpdateExpiredTime);
    window.removeEventListener('keydown', this.boundUpdateExpiredTime);
    window.removeEventListener('storage', this.boundOnIdleTimeoutAction);
    clearInterval(this.interval);
  }



  openIdleDialog() {
    this.dialogOpen = true;
    this.inactivityDialogRef = this.dialog.open(InactivityTimeoutModalComponent, this.dialogConfig);
    this.inactivityDialogRef.afterClosed().subscribe(action => {
      if(action === IdleTimeoutActions.CONTINUE) {
        this.updateExpiredTime(0);
        // trigger other tabs to close the modal
        localStorage.setItem(IDLE_TIMEOUT_ACTION, IdleTimeoutActions.CONTINUE);
        localStorage.setItem(IDLE_TIMEOUT_ACTION, null);
      } else if(action === IdleTimeoutActions.LOGOUT){
        this.triggerLogout();
      }
      this.dialogOpen = false;
    });
  }

  onIdleTimeoutAction = (event) => {
    if (event.storageArea === localStorage) {
      if(this.dialogOpen) {
        const action = localStorage.getItem(IDLE_TIMEOUT_ACTION);
        if(action === IdleTimeoutActions.LOGOUT) {
          this.removeListeners();
          this.dialog.closeAll();
          this.router.navigate(['login']);
        } else if (action === IdleTimeoutActions.CONTINUE) {
          this.updateExpiredTime(0);
          this.inactivityDialogRef?.close(IdleTimeoutActions.CONTINUE);
        }
      }
    }
  }

  ngOnDestroy() {
    this.removeListeners();
    this.alive = false;
  }
}
1
  • Can you update this answer and include the contents of IdleTimeoutActions? I think this solution looks clean. Commented Mar 29, 2023 at 11:33
0

Here is something short and generic.

Listens to the HTML events and reports on idle. The result comes as rxjs Subject or if needed one can use a custom HTML event.

import {Injectable, OnDestroy} from '@angular/core';
import {Subject} from "rxjs";

@Injectable({
    providedIn: 'root'
})
export class IdleDetectionService implements OnDestroy {
    public EVENT_WATCH_LIST: string[] = ['mousemove', 'mouseup', 'mousedown', 'scroll', 'keydown'];
    private readonly CHECK_FREQUENCY_MS = 1000 * 60;
    private idleCounterTarget: number | undefined = undefined;
    private eventActivityCounter: number = 0;
    private idleCounter: number = 0;
    private idleCheckTimerRef: any;
    private idleEvent: Subject<void> = new Subject();

    constructor() {
    }

    public start(timeToDetectIdleInMinutes: number): Subject<void> {
        this.detachEventListeners();
        this.attachEventListeners(timeToDetectIdleInMinutes);
        return this.idleEvent;
    }

    detachEventListeners() {
        if (this.idleEvent) {
            this.idleEvent.complete();
        }
        clearInterval(this.idleCheckTimerRef);
        this.EVENT_WATCH_LIST.forEach((eventName) => {
            window.removeEventListener(eventName, this.onActivity.bind(this));
        });
    }

    ngOnDestroy() {
        this.detachEventListeners();
    }

    private attachEventListeners(timeToDetectIdleInMinutes: number) {
        if (this.idleEvent) {
            this.idleEvent.complete();
            this.idleEvent = new Subject();
        }
        this.idleCounterTarget = timeToDetectIdleInMinutes;
        // very important - when you passing the methods of the class pass them with .bind(this) otherwise it wont work!
        this.EVENT_WATCH_LIST.forEach((eventName)=>{
            window.addEventListener(eventName, this.onActivity.bind(this));
        });
        this.idleCheckTimerRef = setInterval(this.onIdleCheck.bind(this), this.CHECK_FREQUENCY_MS);
    }

    private onIdleCheck() {
        if (this.eventActivityCounter === 0) {
            this.idleCounter++;
            if (this.idleCounterTarget !== undefined && this.idleCounter >= this.idleCounterTarget) {
                setTimeout(() => {
                    // console.log('DETECTED IDLE, REPORT IT')
                    this.idleEvent.next();
                    // Note that this will continue to fire every minute
                    // the consumer might want to consume it with take(1)
                });
            }
            // wait for more idle
            return;
        }
        this.eventActivityCounter = 0;
        this.idleCounter = 0;
    }

    private onActivity() {
        // events could be very frequent, therefore we need something fast here
        this.eventActivityCounter++;
    }

}
 

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.