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(core): Add verification token strategy #3294

Merged
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
Original file line number Diff line number Diff line change
@@ -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 changes: 34 additions & 0 deletions packages/core/src/config/auth/verification-token-strategy.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions packages/core/src/config/config.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -119,6 +120,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
sessionCacheStrategy,
passwordHashingStrategy,
passwordValidationStrategy,
verificationTokenStrategy,
assetNamingStrategy,
assetPreviewStrategy,
assetStorageStrategy,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/config/default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -111,6 +112,7 @@ export const defaultConfig: RuntimeVendureConfig = {
customPermissions: [],
passwordHashingStrategy: new BcryptPasswordHashingStrategy(),
passwordValidationStrategy: new DefaultPasswordValidationStrategy({ minLength: 4 }),
verificationTokenStrategy: new DefaultVerificationTokenStrategy(),
},
catalogOptions: {
collectionFilters: defaultCollectionFilters,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/config/vendure-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand All @@ -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);
}
}
29 changes: 22 additions & 7 deletions packages/core/src/service/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
Expand Down
Loading