Sfoglia il codice sorgente

feat(core): Add verification token strategy (#3294)

Twilight 1 anno fa
parent
commit
9375ba24b1

+ 55 - 0
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;
+    }
+}

+ 34 - 0
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> | string;
+
+    /**
+     * @description
+     * Checks the validity of a verification token.
+     *
+     * @since 3.2.0
+     */
+    verifyVerificationToken(ctx: RequestContext, token: string): Promise<boolean> | boolean;
+}

+ 2 - 0
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,

+ 2 - 0
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,

+ 2 - 0
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';

+ 9 - 0
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;
 }
 
 /**

+ 12 - 23
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<string> {
+        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<boolean> {
+        return this.configService.authOptions.verificationTokenStrategy.verifyVerificationToken(ctx, token);
     }
 }

+ 22 - 7
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<User> {
         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<User> {
         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();