Angular User Session Timeout

Home / Angular User Session Timeout

A long time ago, I blogged about a service that I used in AngularJS to let the user know that their session is about to expire and that they would be logged out if they didn’t take action. I needed to recreate this functionality in Angular for my latest swath of applications.


 

Dealing with timers in Angular can be significantly different when using Reactive components and subscriptions. The basic premises remains the same, though. We’ll have a service with a timer that will provide a Subject to which consumers of the service can subscribe. A Reactive Subject provides an easy mechanism to trigger a “next” subscription to alert consumers that the timer has expired. The new service, which I am simply calling IdleTimeoutService, will provide the same methods as the old AngularJS service: startTimer, resetTimer, etc.

import { Injectable } from '@angular/core';
import { Observable, Subject, Subscription, BehaviorSubject} from 'rxjs/Rx';
 
@Injectable()
export class IdleTimeoutService {
    private _count: number = 0;
    private _serviceId: string = 'idleTimeoutSvc-' + Math.floor(Math.random() * 10000);
    private _timeoutSeconds: number = 5;
    private timerSubscription: Subscription;
    private timer: Observable<number>;
    private resetOnTrigger: boolean = false;
    public timeoutExpired: Subject<number> = new Subject<number>();
 
    constructor() {
        console.log("Constructed idleTimeoutService " + this._serviceId);
 
        this.timeoutExpired.subscribe(n => {
            console.log("timeoutExpired subject next.. " + n.toString())
        });
 
        this.startTimer();
    }

The timer variable is our actual Observable that we use internally. You can see below that we use RxJS’s timer observable. We’ll have it trigger in five seconds and subscribe to it. Once it is triggered, we call our handler. The “startTimer” method sets up our subscriptions.

public startTimer() {
    if (this.timerSubscription) {
        this.timerSubscription.unsubscribe();
    }
 
    this.timer = Observable.timer(this._timeoutSeconds * 1000);
    this.timerSubscription = this.timer.subscribe(n => {
        this.timerComplete(n);
    });
}
 
public stopTimer() {
    this.timerSubscription.unsubscribe();
}
 
public resetTimer() {
    if (this.timerSubscription) {
        this.timerSubscription.unsubscribe();
    }
 
    this.timer = Observable.timer(this._timeoutSeconds * 1000);
    this.timerSubscription = this._timer.subscribe(n => {
        this.timerComplete(n);
    });

Our timer handler will call the “next” method on our Subject to which others may be subscribed. This provides a nice eventing mechanism. Optionally, we can reset the timer after trigger. However, for most of my use cases, I’ll be resetting this manually.

    private timerComplete(n: number) {
        this.timeoutExpired.next(++this._count);
 
        if (this.resetOnTrigger) {
            this.startTimer();
        }
    }

To consume the service, the IdleTimerService is injected as needed. Then, we can subscribe to its exposed/public Subject.

The basic subscription is show in the code snippet.

ngOnInit() {
    this.startCounter();
    this._idleTimerSubscription = this.idleTimeoutSvc.timeoutExpired.subscribe(res => {
      var modalPromise = this.dialogSvc.open("Session Expiring!", "Your session is about to expire. Do you need more time?",
          true, "Yes", "No");
      var newObservable = Observable.fromPromise(modalPromise);
      newObservable.subscribe(
          (res) => {
              if (res === true) {
                  console.log("Extending session...");
                  this._status = "Session was extended.";
                  this.idleTimeoutSvc.resetTimer();
                  this.startCounter();
                  this.changeRef.markForCheck();
 
              } else {
                  console.log("Not extending session...");
                  this._status = "Session was not extended.";
                  this.changeRef.markForCheck();
              }
          },
          (reason) => {
              console.log("Dismissed " + reason);
              this._status = "Session was not extended.";
              this.changeRef.markForCheck();
          }
      );
    });
  }

In the demo plunker below, I have a secondary counter within a component, also using an RxJS timer, to illustrate that the subject does emit to subscribers every five seconds. I then use a dialog service to display a modal. The modal itself returns a promise result. This simulates the concept of extending a user’s session if they click “Yes” or not extending it if they click “No.”

While I didn’t illustrate it, this becomes the starting point for hooking into Http interceptors to access a heartbeat API, or some other endpoint, to extend the user’s session at the server level (or cookie refresh). All in all, timers and observables are pretty fun to play around with in Angular.

2 thoughts on “Angular User Session Timeout”

  1. Hi!

    Thanks for the great article. I have a question about the following scenario: Say a user is using a laptop. He’s browsing the website and he session is alive on the FE. Then he closes his lid (counter also stops) and opens it up. On the BE his sessions is dead, but on the FE the counter starts from where it left off (so session is alive).

    How would you advise to solve this?

    1. That’s an interesting problem. I hadn’t really thought about it before, but I don’t think browsers give any sort of notification of when the system goes to sleep. However, it would be pretty easy to have an interval that is storing the current date and comparing it every five minutes or so. This could be done via setInterval or an RxJS timer in a similar fashion to what is already in the idleTimeoutService.

      Something like this:

      private startDateCompare() {
      	this.lastTime = (new Date()).getTime();
      	this.dateTimer = timer(this.dateTimerInterval); // compare every five minutes
      	this.dateTimerSubscription = this.dateTimer.subscribe(n => {
      		let currentTime: number = (new Date()).getTime();
      		if (currentTime > (this.lastTime + this.dateTimerInterval + this.dateTimerTolerance)) { // look for 10 sec diff
      			console.log("Looks like the machine just woke up.. ");
      		}
      		else {
      			console.log("Machine did not sleep.. ");
      		}
      		this.dateTimerSubscription.unsubscribe();
      		this.startDateCompare();
      	});
      }
      

      Of course, it is possible that if the tab is in the background, it could cause the JavaScript to not execute as often. Using a certain tolerance, I think, handles this well enough without needing to resort to a WebWorker.

      I updated the Plunk that includes this change as well as updated the Plunk to utilize the latest version of Angular/RxJS/etc.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.