From 892f0f17beb288af03600efbfefb84622dec9266 Mon Sep 17 00:00:00 2001 From: Elwin Schmitz Date: Wed, 12 Jun 2024 12:23:18 +0200 Subject: [PATCH] feat: Add check for expired user-token to every initial page-load --- interfaces/Portal/src/app/app.component.ts | 12 +++++++- .../Portal/src/app/auth/auth.service.ts | 15 ++++++---- services/.env.example | 1 - .../121-service/src/user/user.controller.ts | 9 ++++-- .../121-service/src/user/user.interface.ts | 3 +- services/121-service/src/user/user.service.ts | 29 +++++++++++++++---- 6 files changed, 51 insertions(+), 18 deletions(-) diff --git a/interfaces/Portal/src/app/app.component.ts b/interfaces/Portal/src/app/app.component.ts index c2b23e82c9..de2d203b70 100644 --- a/interfaces/Portal/src/app/app.component.ts +++ b/interfaces/Portal/src/app/app.component.ts @@ -2,6 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { MsalService } from '@azure/msal-angular'; import { Subscription } from 'rxjs'; import { environment } from 'src/environments/environment'; +import { AppRoutes } from './app-routes.enum'; import { AuthService } from './auth/auth.service'; import { LanguageService } from './services/language.service'; import { LoggingService } from './services/logging.service'; @@ -32,7 +33,7 @@ export class AppComponent implements OnInit, OnDestroy { } } - public ngOnInit(): void { + public async ngOnInit() { if (this.useSso) { this.authService.logoutNonSsoUser(); @@ -40,6 +41,15 @@ export class AppComponent implements OnInit, OnDestroy { .handleRedirectObservable() .subscribe(); } + + if ( + // Do not check the current users' logged-in state on "login-related" pages: + ![AppRoutes.login, AppRoutes.auth].includes( + window.location.pathname.substring(1) as AppRoutes, + ) + ) { + this.authService.refreshCurrentUser(); + } } public ngOnDestroy(): void { diff --git a/interfaces/Portal/src/app/auth/auth.service.ts b/interfaces/Portal/src/app/auth/auth.service.ts index d1569d6064..75a826b694 100644 --- a/interfaces/Portal/src/app/auth/auth.service.ts +++ b/interfaces/Portal/src/app/auth/auth.service.ts @@ -152,7 +152,7 @@ export class AuthService { return { username: user.username, permissions: user.permissions, - expires: user.expires ? user.expires : '', + expires: user.expires ? user.expires : undefined, isAdmin: user.isAdmin, isEntraUser: user.isEntraUser, }; @@ -191,19 +191,22 @@ export class AuthService { }); } - // TODO: Think of a better name for this method - public async processAzureAuthSuccess(redirectToHome = false): Promise { + public async refreshCurrentUser() { const userDto = await this.programsService.getCurrentUser(); if (!userDto || !userDto.user) { - localStorage.removeItem(USER_KEY); - this.router.navigate(['/', AppRoutes.login]); + await this.logout(); return; } - await this.checkSsoTokenExpirationDate(); this.setUserInStorage(userDto.user); this.updateAuthenticationState(); + } + + // TODO: Think of a better name for this method + public async processAzureAuthSuccess(redirectToHome = false) { + await this.refreshCurrentUser(); + await this.checkSsoTokenExpirationDate(); if (redirectToHome) { setTimeout(() => { diff --git a/services/.env.example b/services/.env.example index 90fb3a68f9..357afb2012 100644 --- a/services/.env.example +++ b/services/.env.example @@ -85,7 +85,6 @@ SECRETS_121_SERVICE_SECRET=121_service_secret # ----------------- # To enable single-sign-on via Azure Entra ID, use: `TRUE` to enable, leave empty or out to disable. -# NOTE: Not used at the moment, is is set in front-end now. Should be moved to back-end though. USE_SSO_AZURE_ENTRA= # Azure Entra environment-specific IDs. diff --git a/services/121-service/src/user/user.controller.ts b/services/121-service/src/user/user.controller.ts index f31847f6ce..1fe97aec83 100644 --- a/services/121-service/src/user/user.controller.ts +++ b/services/121-service/src/user/user.controller.ts @@ -280,12 +280,15 @@ export class UserController { description: 'No user detectable from cookie or no cookie present', }) public async findMe(@Req() req): Promise { - const username = req.user.username; - if (!username) { + if (!req.user || !req.user.username) { const errors = `No user detectable from cookie or no cookie present'`; throw new HttpException({ errors }, HttpStatus.UNAUTHORIZED); } - return await this.userService.getUserRoByUsernameOrThrow(username); + + return await this.userService.getUserRoByUsernameOrThrow( + req.user.username, + req.user.exp, + ); } // This endpoint searches users accross all programs, which is needed to add a user to a program diff --git a/services/121-service/src/user/user.interface.ts b/services/121-service/src/user/user.interface.ts index b00fea617e..2e0e8004e3 100644 --- a/services/121-service/src/user/user.interface.ts +++ b/services/121-service/src/user/user.interface.ts @@ -1,12 +1,13 @@ import { PermissionEnum } from '@121-service/src/user/enum/permission.enum'; -interface UserData { +export interface UserData { id: number; username?: string; permissions: UserPermissions; isAdmin?: boolean; isEntraUser?: boolean; lastLogin?: Date; + expires?: Date; } export interface UserRO { diff --git a/services/121-service/src/user/user.service.ts b/services/121-service/src/user/user.service.ts index 7171cbf5e5..9caf6c76bd 100644 --- a/services/121-service/src/user/user.service.ts +++ b/services/121-service/src/user/user.service.ts @@ -32,7 +32,7 @@ import { PermissionEntity } from '@121-service/src/user/permissions.entity'; import { UserRoleEntity } from '@121-service/src/user/user-role.entity'; import { UserType } from '@121-service/src/user/user-type-enum'; import { UserEntity } from '@121-service/src/user/user.entity'; -import { UserRO } from '@121-service/src/user/user.interface'; +import { UserData, UserRO } from '@121-service/src/user/user.interface'; import { HttpStatus, Inject, Injectable, Scope } from '@nestjs/common'; import { HttpException } from '@nestjs/common/exceptions/http.exception'; import { REQUEST } from '@nestjs/core'; @@ -493,7 +493,10 @@ export class UserService { return user; } - public async getUserRoByUsernameOrThrow(username: string): Promise { + public async getUserRoByUsernameOrThrow( + username: string, + tokenExpiration?: number, + ): Promise { const user = await this.userRepository.findOne({ where: { username: username }, relations: [ @@ -506,7 +509,8 @@ export class UserService { const errors = `User not found'`; throw new HttpException({ errors }, HttpStatus.NOT_FOUND); } - return await this.buildUserRO(user); + + return await this.buildUserRO(user, tokenExpiration); } public generateJWT(user: UserEntity): string { @@ -546,10 +550,13 @@ export class UserService { } } - private async buildUserRO(user: UserEntity): Promise { + private async buildUserRO( + user: UserEntity, + tokenExpiration?: number, + ): Promise { const permissions = await this.buildPermissionsObject(user.id); - const userRO = { + const userData: UserData = { id: user.id, username: user.username ?? undefined, permissions, @@ -557,7 +564,17 @@ export class UserService { isEntraUser: user.isEntraUser, lastLogin: user.lastLogin ?? undefined, }; - return { user: userRO }; + + // For SSO-users, token expiration is handled by Azure + if ( + !process.env.USE_SSO_AZURE_ENTRA && + !user.isEntraUser && + tokenExpiration + ) { + userData.expires = new Date(tokenExpiration * 1_000); + } + + return { user: userData }; } private async buildPermissionsObject(userId: number): Promise {