diff --git a/packages/core/src/config/auth/default-verification-token-strategy.ts b/packages/core/src/config/auth/default-verification-token-strategy.ts new file mode 100644 index 0000000000..058f990907 --- /dev/null +++ b/packages/core/src/config/auth/default-verification-token-strategy.ts @@ -0,0 +1,55 @@ +import ms from 'ms'; + +import { RequestContext } from '../../api/common/request-context'; +import { Injector } from '../../common'; +import { generatePublicId } from '../../common/generate-public-id'; +import { ConfigService } from '../config.service'; + +import { VerificationTokenStrategy } from './verification-token-strategy'; + +/** + * @description + * The default VerificationTokenStrategy which generates a token consisting of the + * base64-encoded current time concatenated with a random id. The token is considered + * valid if the current time is within the configured `verificationTokenDuration` of the + * time encoded in the token. + * + * @docsCategory auth + * @since 3.2.0 + */ +export class DefaultVerificationTokenStrategy implements VerificationTokenStrategy { + private configService: ConfigService; + + init(injector: Injector) { + this.configService = injector.get(ConfigService); + } + + /** + * Generates a verification token which encodes the time of generation and concatenates it with a + * random id. + */ + generateVerificationToken(_ctx: RequestContext): string { + const now = new Date(); + const base64Now = Buffer.from(now.toJSON()).toString('base64'); + const id = generatePublicId(); + return `${base64Now}_${id}`; + } + + /** + * Checks the age of the verification token to see if it falls within the token duration + * as specified in the VendureConfig. + */ + verifyVerificationToken(_ctx: RequestContext, token: string): boolean { + const { verificationTokenDuration } = this.configService.authOptions; + const verificationTokenDurationInMs = + typeof verificationTokenDuration === 'string' + ? ms(verificationTokenDuration) + : verificationTokenDuration; + + const [generatedOn] = token.split('_'); + const dateString = Buffer.from(generatedOn, 'base64').toString(); + const date = new Date(dateString); + const elapsed = +new Date() - +date; + return elapsed < verificationTokenDurationInMs; + } +} diff --git a/packages/core/src/config/auth/verification-token-strategy.ts b/packages/core/src/config/auth/verification-token-strategy.ts new file mode 100644 index 0000000000..4feb2e2703 --- /dev/null +++ b/packages/core/src/config/auth/verification-token-strategy.ts @@ -0,0 +1,34 @@ +import { RequestContext } from '../../api/common/request-context'; +import { InjectableStrategy } from '../../common/types/injectable-strategy'; + +/** + * @description + * Defines a custom strategy for creating and validating verification tokens. + * + * :::info + * + * This is configured via the `authOptions.verificationTokenStrategy` property of + * your VendureConfig. + * + * ::: + * + * @docsCategory auth + * @since 3.2.0 + */ +export interface VerificationTokenStrategy extends InjectableStrategy { + /** + * @description + * Generates a verification token. + * + * @since 3.2.0 + */ + generateVerificationToken(ctx: RequestContext): Promise | string; + + /** + * @description + * Checks the validity of a verification token. + * + * @since 3.2.0 + */ + verifyVerificationToken(ctx: RequestContext, token: string): Promise | boolean; +} diff --git a/packages/core/src/config/config.module.ts b/packages/core/src/config/config.module.ts index 241251b056..dcd0af3d74 100644 --- a/packages/core/src/config/config.module.ts +++ b/packages/core/src/config/config.module.ts @@ -83,6 +83,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo sessionCacheStrategy, passwordHashingStrategy, passwordValidationStrategy, + verificationTokenStrategy, } = this.configService.authOptions; const { taxZoneStrategy, taxLineCalculationStrategy } = this.configService.taxOptions; const { jobQueueStrategy, jobBufferStorageStrategy } = this.configService.jobQueueOptions; @@ -119,6 +120,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo sessionCacheStrategy, passwordHashingStrategy, passwordValidationStrategy, + verificationTokenStrategy, assetNamingStrategy, assetPreviewStrategy, assetStorageStrategy, diff --git a/packages/core/src/config/default-config.ts b/packages/core/src/config/default-config.ts index f2d31af6b3..dedd31ac69 100644 --- a/packages/core/src/config/default-config.ts +++ b/packages/core/src/config/default-config.ts @@ -17,6 +17,7 @@ import { NoAssetPreviewStrategy } from './asset-preview-strategy/no-asset-previe import { NoAssetStorageStrategy } from './asset-storage-strategy/no-asset-storage-strategy'; import { BcryptPasswordHashingStrategy } from './auth/bcrypt-password-hashing-strategy'; import { DefaultPasswordValidationStrategy } from './auth/default-password-validation-strategy'; +import { DefaultVerificationTokenStrategy } from './auth/default-verification-token-strategy'; import { NativeAuthenticationStrategy } from './auth/native-authentication-strategy'; import { defaultCollectionFilters } from './catalog/default-collection-filters'; import { DefaultProductVariantPriceCalculationStrategy } from './catalog/default-product-variant-price-calculation-strategy'; @@ -111,6 +112,7 @@ export const defaultConfig: RuntimeVendureConfig = { customPermissions: [], passwordHashingStrategy: new BcryptPasswordHashingStrategy(), passwordValidationStrategy: new DefaultPasswordValidationStrategy({ minLength: 4 }), + verificationTokenStrategy: new DefaultVerificationTokenStrategy(), }, catalogOptions: { collectionFilters: defaultCollectionFilters, diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts index ab480f30d7..97eeb72e36 100644 --- a/packages/core/src/config/index.ts +++ b/packages/core/src/config/index.ts @@ -6,9 +6,11 @@ export * from './asset-storage-strategy/asset-storage-strategy'; export * from './auth/authentication-strategy'; export * from './auth/bcrypt-password-hashing-strategy'; export * from './auth/default-password-validation-strategy'; +export * from './auth/default-verification-token-strategy'; export * from './auth/native-authentication-strategy'; export * from './auth/password-hashing-strategy'; export * from './auth/password-validation-strategy'; +export * from './auth/verification-token-strategy'; export * from './catalog/collection-filter'; export * from './catalog/default-collection-filters'; export * from './catalog/default-product-variant-price-selection-strategy'; diff --git a/packages/core/src/config/vendure-config.ts b/packages/core/src/config/vendure-config.ts index 1aaa3cc3c0..52d2801e83 100644 --- a/packages/core/src/config/vendure-config.ts +++ b/packages/core/src/config/vendure-config.ts @@ -17,6 +17,7 @@ import { AssetStorageStrategy } from './asset-storage-strategy/asset-storage-str import { AuthenticationStrategy } from './auth/authentication-strategy'; import { PasswordHashingStrategy } from './auth/password-hashing-strategy'; import { PasswordValidationStrategy } from './auth/password-validation-strategy'; +import { VerificationTokenStrategy } from './auth/verification-token-strategy'; import { CollectionFilter } from './catalog/collection-filter'; import { ProductVariantPriceCalculationStrategy } from './catalog/product-variant-price-calculation-strategy'; import { ProductVariantPriceSelectionStrategy } from './catalog/product-variant-price-selection-strategy'; @@ -476,6 +477,14 @@ export interface AuthOptions { * @default DefaultPasswordValidationStrategy */ passwordValidationStrategy?: PasswordValidationStrategy; + /** + * @description + * Allows you to customize the way verification tokens are generated. + * + * @default DefaultVerificationTokenStrategy + * @since 3.2.0 + */ + verificationTokenStrategy?: VerificationTokenStrategy; } /** diff --git a/packages/core/src/service/helpers/verification-token-generator/verification-token-generator.ts b/packages/core/src/service/helpers/verification-token-generator/verification-token-generator.ts index 6d27f60374..12edc6aee8 100644 --- a/packages/core/src/service/helpers/verification-token-generator/verification-token-generator.ts +++ b/packages/core/src/service/helpers/verification-token-generator/verification-token-generator.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { generatePublicId } from '../../../common/generate-public-id'; +import { RequestContext } from '../../../api'; import { ConfigService } from '../../../config/config.service'; /** @@ -13,31 +12,21 @@ export class VerificationTokenGenerator { constructor(private configService: ConfigService) {} /** - * Generates a verification token which encodes the time of generation and concatenates it with a - * random id. + * Generates a verification token using the configured {@link VerificationTokenStrategy}. + * @param ctx The RequestContext object. + * @returns The generated token. */ - generateVerificationToken() { - const now = new Date(); - const base64Now = Buffer.from(now.toJSON()).toString('base64'); - const id = generatePublicId(); - return `${base64Now}_${id}`; + async generateVerificationToken(ctx: RequestContext): Promise { + return this.configService.authOptions.verificationTokenStrategy.generateVerificationToken(ctx); } /** - * Checks the age of the verification token to see if it falls within the token duration - * as specified in the VendureConfig. + * Verifies a verification token using the configured {@link VerificationTokenStrategy}. + * @param ctx The RequestContext object. + * @param token The token to verify. + * @returns `true` if the token is valid, `false` otherwise. */ - verifyVerificationToken(token: string): boolean { - const { verificationTokenDuration } = this.configService.authOptions; - const verificationTokenDurationInMs = - typeof verificationTokenDuration === 'string' - ? ms(verificationTokenDuration) - : verificationTokenDuration; - - const [generatedOn] = token.split('_'); - const dateString = Buffer.from(generatedOn, 'base64').toString(); - const date = new Date(dateString); - const elapsed = +new Date() - +date; - return elapsed < verificationTokenDurationInMs; + async verifyVerificationToken(ctx: RequestContext, token: string): Promise { + return this.configService.authOptions.verificationTokenStrategy.verifyVerificationToken(ctx, token); } } diff --git a/packages/core/src/service/services/user.service.ts b/packages/core/src/service/services/user.service.ts index 8cfeda676d..87c5145294 100644 --- a/packages/core/src/service/services/user.service.ts +++ b/packages/core/src/service/services/user.service.ts @@ -135,7 +135,7 @@ export class UserService { const authenticationMethod = new NativeAuthenticationMethod(); if (this.configService.authOptions.requireVerification) { authenticationMethod.verificationToken = - this.verificationTokenGenerator.generateVerificationToken(); + await this.verificationTokenGenerator.generateVerificationToken(ctx); user.verified = false; } else { user.verified = true; @@ -193,7 +193,8 @@ export class UserService { */ async setVerificationToken(ctx: RequestContext, user: User): Promise { const nativeAuthMethod = user.getNativeAuthenticationMethod(); - nativeAuthMethod.verificationToken = this.verificationTokenGenerator.generateVerificationToken(); + nativeAuthMethod.verificationToken = + await this.verificationTokenGenerator.generateVerificationToken(ctx); user.verified = false; await this.connection.getRepository(ctx, NativeAuthenticationMethod).save(nativeAuthMethod); return this.connection.getRepository(ctx, User).save(user); @@ -220,7 +221,11 @@ export class UserService { .where('authenticationMethod.verificationToken = :verificationToken', { verificationToken }) .getOne(); if (user) { - if (this.verificationTokenGenerator.verifyVerificationToken(verificationToken)) { + const isTokenValid = await this.verificationTokenGenerator.verifyVerificationToken( + ctx, + verificationToken, + ); + if (isTokenValid) { const nativeAuthMethod = user.getNativeAuthenticationMethod(); if (!password) { if (!nativeAuthMethod.passwordHash) { @@ -262,7 +267,8 @@ export class UserService { if (!nativeAuthMethod) { return undefined; } - nativeAuthMethod.passwordResetToken = this.verificationTokenGenerator.generateVerificationToken(); + nativeAuthMethod.passwordResetToken = + await this.verificationTokenGenerator.generateVerificationToken(ctx); await this.connection.getRepository(ctx, NativeAuthenticationMethod).save(nativeAuthMethod); return user; } @@ -295,7 +301,13 @@ export class UserService { if (passwordValidationResult !== true) { return passwordValidationResult; } - if (this.verificationTokenGenerator.verifyVerificationToken(passwordResetToken)) { + + const isTokenValid = await this.verificationTokenGenerator.verifyVerificationToken( + ctx, + passwordResetToken, + ); + + if (isTokenValid) { const nativeAuthMethod = user.getNativeAuthenticationMethod(); nativeAuthMethod.passwordHash = await this.passwordCipher.hash(password); nativeAuthMethod.passwordResetToken = null; @@ -346,7 +358,8 @@ export class UserService { */ async setIdentifierChangeToken(ctx: RequestContext, user: User): Promise { const nativeAuthMethod = user.getNativeAuthenticationMethod(); - nativeAuthMethod.identifierChangeToken = this.verificationTokenGenerator.generateVerificationToken(); + nativeAuthMethod.identifierChangeToken = + await this.verificationTokenGenerator.generateVerificationToken(ctx); await this.connection.getRepository(ctx, NativeAuthenticationMethod).save(nativeAuthMethod); return user; } @@ -376,7 +389,9 @@ export class UserService { if (!user) { return new IdentifierChangeTokenInvalidError(); } - if (!this.verificationTokenGenerator.verifyVerificationToken(token)) { + const isTokenValid = await this.verificationTokenGenerator.verifyVerificationToken(ctx, token); + + if (!isTokenValid) { return new IdentifierChangeTokenExpiredError(); } const nativeAuthMethod = user.getNativeAuthenticationMethod();