Skip to content

Commit

Permalink
handle multiple jwt strategies
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Sep 20, 2024
1 parent 7be72bd commit d2cdc25
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 10 deletions.
8 changes: 6 additions & 2 deletions api/src/modules/auth/authentication/authentication.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion api/src/modules/auth/services/auth.mailer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}`;

Expand Down
3 changes: 2 additions & 1 deletion api/src/modules/auth/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand All @@ -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,
Expand Down
38 changes: 38 additions & 0 deletions api/src/modules/auth/strategies/reset-password.strategy.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
14 changes: 8 additions & 6 deletions api/src/modules/config/app-config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -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 {
Expand Down
42 changes: 42 additions & 0 deletions api/src/modules/config/auth-config.handler.ts
Original file line number Diff line number Diff line change
@@ -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<string>('ACCESS_TOKEN_SECRET'),
expiresIn: this.configService.get<string>('ACCESS_TOKEN_EXPIRES_IN'),
};

case TOKEN_TYPE_ENUM.RESET_PASSWORD:
return {
secret: this.configService.get<string>('RESET_PASSWORD_TOKEN_SECRET'),
expiresIn: this.configService.get<string>(
'RESET_PASSWORD_TOKEN_EXPIRES_IN',
),
};

case TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION:
return {
secret: this.configService.get<string>(
'EMAIL_CONFIRMATION_TOKEN_SECRET',
),
expiresIn: this.configService.get<string>(
'EMAIL_CONFIRMATION_TOKEN_EXPIRES_IN',
),
};

default:
throw new Error('Invalid token type');
}
}
}
10 changes: 10 additions & 0 deletions shared/contracts/auth/auth.contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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,
},
});

0 comments on commit d2cdc25

Please sign in to comment.