Angular Inactivity

Below are the instructions to quickly set up the automatic logout after 1 hour of inactivity. Here we use the ng-idle package to do most of the heavy lifting where the code below will handle the rest.

What is ng-idle?

ng-idle is a library that monitors user inactivity in Angular applications. It helps you:

  • Detect periods of user inactivity.
  • Notify users of impending timeouts.
    • One common example is a popup modal with a text counting down.
  • Automatically log out inactive users.

Example use case

gantt title Timeline dateFormat HH:mm:ss axisFormat %H:%M:%S section Inactivity User active :done, 1, 00:00:00, 00:30:00 Idle detected :active, 2, 00:30:00, 01:29:00 section Warning Warning displayed :crit, 3, 01:29:00, 01:30:00 User logged out :done, 4, 01:30:00, 01:30:00


Installation

To get started with ng-idle, you need to install it into your Angular project:

npm install @ng-idle/core @ng-idle/keepalive
  • @ng-idle/core: The core idle detection library. ( https://www.npmjs.com/package/ng-idle?activeTab=readme )
  • @ng-idle/keepalive: Adds keepalive pings to your server during idle periods.

Set up & Configuration

1. Update the config file

If using environment.ts

export const environment = {
  //...
  inactivityTimeout: 3540,
  inactivityWarning: 60
}

If using assets\config.json

{
    "inactivityTimeout": 3540,
    "inactivityWarning": 60
}

In this config example, the system will check that the user is inactive for a total of 60 minutes (3600 seconds). In the last 60 seconds, a warning logic will be triggered to display a popup and count down. Once that warning timer has complete, the user will be logged out.

Common test configs is to reduce the values down to 5 minutes total for testing purposes. In this scenario inactivityTimeout property should be updated to 240 (4 mins) or lower.

2. Update the app.module.ts file

Add NgIdleModule into imports and Keepalive into providers

...
import { NgIdleModule } from '@ng-idle/core';
import { Keepalive } from '@ng-idle/keepalive';
...

@NgModule({
  declarations: [...],
  imports: [
    ...,
    NgIdleModule.forRoot()
  ],
  providers: [
    ...,
    Keepalive
  ],
  bootstrap: [...]
})
export class AppModule {}

3. Add InactivityService

Add a service which handles the inactivity logic

import { Injectable } from '@angular/core';
import { Idle, DEFAULT_INTERRUPTSOURCES } from '@ng-idle/core';
import { AuthService } from './api-services/auth.service';
import { MatSnackBar, MatSnackBarRef } from '@angular/material/snack-bar';
import { SnackbarMessageComponent } from '../core/interaction/snackbar-message/snackbar-message.component';
import { SnackType } from '../core/interaction/snackbar-message/snackbar-message.helper';
import { MatDialog } from '@angular/material/dialog';

@Injectable({
  providedIn: 'root',
})
export class InactivityService {
  private snackBarRef: MatSnackBarRef<SnackbarMessageComponent> | null = null;

  constructor(
    private readonly idle: Idle, 
    private readonly authService: AuthService,
    private readonly snackBar: MatSnackBar,
    private readonly dialog: MatDialog
  ) {}

  /**
   * Initializes inactivity tracking logic.
   * @param idleTimeSeconds Time before idle state starts (in seconds). Default is 59 mins idle time
   * @param timeoutSeconds Time before user is logged out after idle (in seconds). Default is 1 min warning time before logout
   */
  initialize(idleTimeSeconds: number = 3540, timeoutSeconds: number = 60): void {
    this.idle.setIdle(idleTimeSeconds);
    this.idle.setTimeout(timeoutSeconds); 
    this.idle.setInterrupts(DEFAULT_INTERRUPTSOURCES);

    // execute code when warning is active every second
    this.idle.onTimeoutWarning.subscribe((countdown: number) => {
      const countdownMessage = `Your session is about to expire in ${countdown} seconds due to inactivity.`;

      // Using snackBar in this example but can be switched out to a dialog or other appropriate component
      if (!this.snackBarRef) {
        this.snackBarRef = this.snackBar.openFromComponent(SnackbarMessageComponent, {
          data: {
            text: countdownMessage,
            snackType: SnackType.Warning,
          },
          duration: countdown * 1000,
        });
      } else {
        this.snackBarRef.instance.data.text = countdownMessage;
      }
    });

    // execute code when user is no longer inactive
    this.idle.onIdleEnd.subscribe(() => {
      // auto dismiss warning popup if present
      this.dismissSnackBar();
    });

    // execute code when user has reached total inactivity timer
    this.idle.onTimeout.subscribe(() => {
      this.dismissSnackBar();
      this.logout();
    });

    this.startWatching();
  }

  /**
   * Starts watching for inactivity.
   */
  startWatching(): void {
    this.idle.watch();
  }

  /**
   * Stops watching for inactivity.
   */
  stopWatching(): void {
    this.idle.stop();
  }

  private dismissSnackBar(): void {
    if (this.snackBarRef) {
      this.snackBarRef.dismiss();
      this.snackBarRef = null;
    }
  }

  private logout(): void {
    this.dialog.closeAll(); //close any existing dialog popups (e.g. user edit form)
    this.authService.logout(); //trigger user logout
  }
}

4. Update app.component.ts

Add logic to the app.component.ts to start the inactivity timer

import { Component, OnInit } from '@angular/core';
import { ConfigService } from './services/config.service';
import { InactivityService } from './services/inactivity.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  constructor(
    ...,
    private readonly configService: ConfigService,
    private readonly inactivityService: InactivityService
  ) { }

  ngOnInit(): void {
    ...
    // initialize the service passing in the config values
    this.inactivityService.initialize(
      this.configService.config.inactivityTimeout, 
      this.configService.config.inactivityWarning
    );
    ...
  }