Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add check for expired user-token to every initial page-load #5410

Merged
merged 1 commit into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion interfaces/Portal/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -32,14 +33,23 @@ export class AppComponent implements OnInit, OnDestroy {
}
}

public ngOnInit(): void {
public async ngOnInit() {
if (this.useSso) {
this.authService.logoutNonSsoUser();

this.msalSubscription = this.msalService
.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 {
Expand Down
15 changes: 9 additions & 6 deletions interfaces/Portal/src/app/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -191,19 +191,22 @@ export class AuthService {
});
}

// TODO: Think of a better name for this method
public async processAzureAuthSuccess(redirectToHome = false): Promise<void> {
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(() => {
Expand Down
1 change: 0 additions & 1 deletion services/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 6 additions & 3 deletions services/121-service/src/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,12 +280,15 @@ export class UserController {
description: 'No user detectable from cookie or no cookie present',
})
public async findMe(@Req() req): Promise<UserRO> {
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
Expand Down
3 changes: 2 additions & 1 deletion services/121-service/src/user/user.interface.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
29 changes: 23 additions & 6 deletions services/121-service/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -493,7 +493,10 @@ export class UserService {
return user;
}

public async getUserRoByUsernameOrThrow(username: string): Promise<UserRO> {
public async getUserRoByUsernameOrThrow(
username: string,
tokenExpiration?: number,
): Promise<UserRO> {
const user = await this.userRepository.findOne({
where: { username: username },
relations: [
Expand All @@ -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 {
Expand Down Expand Up @@ -546,18 +550,31 @@ export class UserService {
}
}

private async buildUserRO(user: UserEntity): Promise<UserRO> {
private async buildUserRO(
user: UserEntity,
tokenExpiration?: number,
): Promise<UserRO> {
const permissions = await this.buildPermissionsObject(user.id);

const userRO = {
const userData: UserData = {
id: user.id,
username: user.username ?? undefined,
permissions,
isAdmin: user.admin,
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<any> {
Expand Down
Loading