From d2cdc257df448fb7c0f2e2a4e5f89f1077e06702 Mon Sep 17 00:00:00 2001 From: alexeh Date: Fri, 20 Sep 2024 07:26:40 +0200 Subject: [PATCH] handle multiple jwt strategies --- .../authentication/authentication.module.ts | 8 +++- api/src/modules/auth/services/auth.mailer.ts | 5 ++- .../modules/auth/strategies/jwt.strategy.ts | 3 +- .../strategies/reset-password.strategy.ts | 38 +++++++++++++++++ api/src/modules/config/app-config.service.ts | 14 ++++--- api/src/modules/config/auth-config.handler.ts | 42 +++++++++++++++++++ shared/contracts/auth/auth.contract.ts | 10 +++++ 7 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 api/src/modules/auth/strategies/reset-password.strategy.ts create mode 100644 api/src/modules/config/auth-config.handler.ts diff --git a/api/src/modules/auth/authentication/authentication.module.ts b/api/src/modules/auth/authentication/authentication.module.ts index db96df0a..9e408265 100644 --- a/api/src/modules/auth/authentication/authentication.module.ts +++ b/api/src/modules/auth/authentication/authentication.module.ts @@ -8,6 +8,7 @@ import { UsersService } from '@api/modules/users/users.service'; import { UsersModule } from '@api/modules/users/users.module'; import { LocalStrategy } from '@api/modules/auth/strategies/local.strategy'; import { JwtStrategy } from '@api/modules/auth/strategies/jwt.strategy'; +import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema'; @Module({ imports: [ @@ -16,8 +17,11 @@ import { JwtStrategy } from '@api/modules/auth/strategies/jwt.strategy'; imports: [ApiConfigModule], inject: [ApiConfigService], useFactory: (config: ApiConfigService) => ({ - secret: config.getJWTConfig().secret, - signOptions: { expiresIn: config.getJWTConfig().expiresIn }, + secret: config.getJWTConfigByType(TOKEN_TYPE_ENUM.ACCESS).secret, + signOptions: { + expiresIn: config.getJWTConfigByType(TOKEN_TYPE_ENUM.ACCESS) + .expiresIn, + }, }), }), UsersModule, diff --git a/api/src/modules/auth/services/auth.mailer.ts b/api/src/modules/auth/services/auth.mailer.ts index 74e9c280..bb206139 100644 --- a/api/src/modules/auth/services/auth.mailer.ts +++ b/api/src/modules/auth/services/auth.mailer.ts @@ -4,6 +4,7 @@ import { IEmailServiceToken, } from '@api/modules/notifications/email/email-service.interface'; import { ApiConfigService } from '@api/modules/config/app-config.service'; +import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema'; export type PasswordRecovery = { email: string; @@ -25,7 +26,9 @@ export class AuthMailer { // TODO: Investigate if it's worth using a template engine to generate the email content, the mail service provider allows it // TODO: Use a different expiration time, or different secret altogether for password recovery - const { expiresIn } = this.apiConfig.getJWTConfig(); + const { expiresIn } = this.apiConfig.getJWTConfigByType( + TOKEN_TYPE_ENUM.RESET_PASSWORD, + ); const resetPasswordUrl = `${passwordRecovery.origin}/auth/forgot-password/${passwordRecovery.token}`; diff --git a/api/src/modules/auth/strategies/jwt.strategy.ts b/api/src/modules/auth/strategies/jwt.strategy.ts index e35a8a69..16a1786c 100644 --- a/api/src/modules/auth/strategies/jwt.strategy.ts +++ b/api/src/modules/auth/strategies/jwt.strategy.ts @@ -3,6 +3,7 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { UsersService } from '@api/modules/users/users.service'; import { ApiConfigService } from '@api/modules/config/app-config.service'; +import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema'; export type JwtPayload = { id: string }; @@ -12,7 +13,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { private readonly userService: UsersService, private readonly config: ApiConfigService, ) { - const { secret } = config.getJWTConfig(); + const { secret } = config.getJWTConfigByType(TOKEN_TYPE_ENUM.ACCESS); super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: secret, diff --git a/api/src/modules/auth/strategies/reset-password.strategy.ts b/api/src/modules/auth/strategies/reset-password.strategy.ts new file mode 100644 index 00000000..9f8b3e77 --- /dev/null +++ b/api/src/modules/auth/strategies/reset-password.strategy.ts @@ -0,0 +1,38 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { UsersService } from '@api/modules/users/users.service'; +import { ApiConfigService } from '@api/modules/config/app-config.service'; +import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema'; + +export type JwtPayload = { id: string }; + +const ResetPasswordStrategyName = 'reset-password'; + +@Injectable() +export class ResetPasswordJwtStrategy extends PassportStrategy( + Strategy, + ResetPasswordStrategyName, +) { + constructor( + private readonly userService: UsersService, + private readonly config: ApiConfigService, + ) { + const { secret } = config.getJWTConfigByType( + TOKEN_TYPE_ENUM.RESET_PASSWORD, + ); + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: secret, + }); + } + + async validate(payload: JwtPayload) { + const { id } = payload; + const user = await this.userService.findOneBy(id); + if (!user) { + throw new UnauthorizedException(); + } + return user; + } +} diff --git a/api/src/modules/config/app-config.service.ts b/api/src/modules/config/app-config.service.ts index 7a5f1520..be4eb247 100644 --- a/api/src/modules/config/app-config.service.ts +++ b/api/src/modules/config/app-config.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { COMMON_DATABASE_ENTITIES } from '@shared/entities/database.entities'; import { ApiEventsEntity } from '@api/modules/events/api-events/api-events.entity'; +import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema'; +import { JwtConfigHandler } from '@api/modules/config/auth-config.handler'; export type JWTConfig = { secret: string; @@ -10,7 +12,10 @@ export type JWTConfig = { @Injectable() export class ApiConfigService { - constructor(private configService: ConfigService) {} + constructor( + private configService: ConfigService, + private readonly jwtConfigHandler: JwtConfigHandler, + ) {} /** * @note We could abstract this to a data layer access config specific class within database module, as well for other configs when the thing gets more complex. @@ -37,11 +42,8 @@ export class ApiConfigService { return this.configService.get('NODE_ENV') === 'production'; } - getJWTConfig(): JWTConfig { - return { - secret: this.configService.get('JWT_SECRET'), - expiresIn: this.configService.get('JWT_EXPIRES_IN'), - }; + getJWTConfigByType(type: TOKEN_TYPE_ENUM): JWTConfig { + return this.jwtConfigHandler.getJwtConfigByType(type); } get(envVarName: string): ConfigService { diff --git a/api/src/modules/config/auth-config.handler.ts b/api/src/modules/config/auth-config.handler.ts new file mode 100644 index 00000000..8f8b6865 --- /dev/null +++ b/api/src/modules/config/auth-config.handler.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema'; + +@Injectable() +export class JwtConfigHandler { + constructor(private readonly configService: ConfigService) {} + + getJwtConfigByType(tokenType: TOKEN_TYPE_ENUM): { + secret: string; + expiresIn: string; + } { + switch (tokenType) { + case TOKEN_TYPE_ENUM.ACCESS: + return { + secret: this.configService.get('ACCESS_TOKEN_SECRET'), + expiresIn: this.configService.get('ACCESS_TOKEN_EXPIRES_IN'), + }; + + case TOKEN_TYPE_ENUM.RESET_PASSWORD: + return { + secret: this.configService.get('RESET_PASSWORD_TOKEN_SECRET'), + expiresIn: this.configService.get( + 'RESET_PASSWORD_TOKEN_EXPIRES_IN', + ), + }; + + case TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION: + return { + secret: this.configService.get( + 'EMAIL_CONFIRMATION_TOKEN_SECRET', + ), + expiresIn: this.configService.get( + 'EMAIL_CONFIRMATION_TOKEN_EXPIRES_IN', + ), + }; + + default: + throw new Error('Invalid token type'); + } + } +} diff --git a/shared/contracts/auth/auth.contract.ts b/shared/contracts/auth/auth.contract.ts index bf1ecc66..1ac11ce9 100644 --- a/shared/contracts/auth/auth.contract.ts +++ b/shared/contracts/auth/auth.contract.ts @@ -2,6 +2,7 @@ import { initContract } from "@ts-rest/core"; import { LogInSchema } from "@shared/schemas/auth/login.schema"; import { UserWithAccessToken } from "@shared/dtos/user.dto"; import { JSONAPIError } from "@shared/dtos/json-api.error"; +import { TokenTypeSchema } from "@shared/schemas/auth/token-type.schema"; // TODO: This is a scaffold. We need to define types for responses, zod schemas for body and query param validation etc. @@ -16,4 +17,13 @@ export const authContract = contract.router({ }, body: LogInSchema, }, + validateToken: { + method: "GET", + path: "/authentication/validate-token", + responses: { + 200: null, + 401: null, + }, + query: TokenTypeSchema, + }, });