@ognicki/nestjs-otp
v1.0.4
Published
OTP (One-Time Password), 2FA authentication module for NestJS
Downloads
597
Maintainers
Readme
NestJS OTP Authentication Module
@ognicki/nestjs-otp
is a NestJS module designed to provide OTP (One-Time Password)
authentication within your NestJS applications without relying on external APIs. The
module includes dynamic module registration, an OtpService
for managing OTP pairing and
verification, and an OtpGuard
to ensure the validity of provided OTP tokens.
You can use authenticator applications to generate OTP tokens, and they will be handled by
OtpModule
.
Who's Behind?
Hello, my name is Marek, and I am a fullstack developer. You can find out more about me at www.ognicki.pro or drop me a message at [email protected].
What is OTP Authentication?
One-Time Password (OTP) authentication is a method of verifying a user's identity by using a unique, one-time password. Unlike traditional passwords, which remain static until changed by the user, OTPs are temporary and expire after a short period of time or after a single use.
Authenticator Applications
Authenticator applications are a popular form of OTP authentication. These applications generate time-based OTPs (TOTPs) or HMAC-based OTPs (HOTPs) that can be used to authenticate users across multiple services and platforms.
The
otpauth
package under the hood of this module works with time-based OTPs.
Time-Based OTPs
Tokens in time-based one-time passwords (TOTPs) are generated using cryptographic algorithms. A secret key shared between the authentication device and the server is scrambled with the timestamp using one of the cryptographic algorithms and optionally processed to match the pre-defined length of a token.
This means one secret per user, not per application.
The optimal solution would be to have one secret per user per device, but this is not implemented in the module (yet?). Of course, in production environments, the secrets should not be stored on the server side without encryption, as any unwanted party could intercept them and thus have all they need to generate OTP tokens, thus violating the security of the application.
Module Dependencies
The @ognicki/nestjs-otp
module uses the following dependencies:
otpauth
Package
See https://www.npmjs.com/package/otpauth for more information.
The otpauth
package is used to generate and validate OTPs (One-Time Passwords) using the
TOTP (Time-Based One-Time Password) and HOTP (HMAC-Based One-Time Password) algorithms.
The package provides a complete OTP management solution to enable secure and reliable
authentication processes.
qrcode
Package
See https://www.npmjs.com/package/qrcode for more information.
The qrcode
package is used to generate QR codes to be scanned by authenticator applications
during the initial setup process. QR codes provide a convenient way for users to import shared
secret keys into their authenticator applications, simplifying the setup of OTP authentication.
By integrating qrcode
, the module provides a consistent and painless way for you to configure
OTP authentication. when configuring OTP authentication.
Installation
npm install --save @ognicki/nestjs-otp
or
yarn add @ognicki/nestjs-otp
Usage
Secret is a string assigned to each user individually. It should never be set globally per application.
In most scenarios, the OtpService
may not be used directly, since the primary logic
for OTP authentication logic is done efficiently by the OtpGuard
. The guard manages the
OTP authentication process inside your NestJS application without any hassle.
The lifecycle of the OtpGuard
involves the following steps:
- Obtaining Request Object: The
OtpGuard
first retrieves theRequest
object from theExecutionContext
. ThisRequest
object encapsulates the HTTP request made to the server. - Fetching OTP Secret: The OTP secret is a crucial component required for OTP
generation and validation. It is imperative that each user possesses a unique
OTP secret. By default, a stub string is returned as the OTP secret. However,
for actual authentication, it's essential to provide a
secretResolver
function. This function should retrieve the OTP secret from the user data store or any other appropriate source. - Extracting OTP Token: By default, the OTP token is extracted from the
X-One-Time-Password
header of the incoming request. However, you have the flexibility to customize both the header name and the logic for extracting the OTP token. For instance, you may opt to extract the OTP token from the request body instead of the headers, depending on your application's requirements.
Default OtpGuard
Behavior
By default, the OtpGuard
utilizes the HTTP execution context to retrieve the
request and extracts the OTP token from the X-One-Time-Password
header. This
behavior is suitable for standard HTTP-based authentication scenarios.
Initial Setup
Import the OtpModule
into your root AppModule
or any other module where you
want to use OTP authentication:
import { Module } from '@nestjs/common';
import { OtpModule } from '@ognicki/nestjs-otp';
@Module({
imports: [
OtpModule.register({
// Configuration options here
}),
],
})
export class AppModule {}
Securing Endpoints with OTP
import { Controller, UseGuards } from '@nestjs/common';
import { OtpGuard } from '@ognicki/nestjs-otp';
@Controller('secure')
export class SecureController {
@UseGuards(OtpGuard)
async protectedRoute() {
return 'This route is protected with OTP!';
}
}
Customizing the Resolvers Logic
The @ognicki/nestjs-otp
module offers the flexibility to customize resolver
functions, empowering developers to tailor OTP authentication according to their
specific requirements. These resolver functions are utilized by the OtpGuard
to handle various aspects of authentication, including obtaining the request
context and extracting the OTP token.
Customization
You can customize the following resolver functions to adapt OTP authentication to different use cases:
- Context Resolver: Retrieves the execution context, enabling access to the request object.
- Request Resolver: Obtains the request object from the execution context, facilitating extraction of relevant data.
- OTP Resolver: Retrieves the OTP token from the request, allowing flexibility in token extraction from different parts of the request payload.
Resolvers should be passed as module configuration options.
Implementation
import { ConfigModule, ConfigService } from '@nestjs/config';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Module } from '@nestjs/common';
import otpConfig from "./config/otp.config";
import { OtpModule } from '@ognicki/nestjs-otp';
@Module({
imports: [
OtpModule.registerAsync({
imports: [ConfigModule.forFeature(otpConfig), UserModule],
useFactory: (configService: ConfigService, userService: UserService) => ({
...configService.get('otp'),
requestResolver: (context: ExecutionContext) => {
try {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
} catch {
return context.switchToHttp().getRequest();
}
},
secretResolver: async (request: Request) => {
const user = getUserFromRequestOrSomewhereElse(request);
return userService.getOtpSecret(user);
},
header: undefined,
}),
inject: [ConfigService, UserService],
}),
],
})
export class AppModule {}
requestResolver
In the example above:
- This resolver function determines how the request object is obtained.
- It first attempts to retrieve the request from the GraphQL execution context.
- If unsuccessful (e.g., the context is not from a GraphQL resolver), it falls back to obtaining the request from the HTTP context.
secretResolver
In the example above:
- This resolver function determines how the secret key for OTP generation is obtained.
- It uses the
UserService
to fetch the OTP secret associated with the current user. - The
UserService
is expected to provide an asynchronous method (getOtpSecret
) that retrieves the OTP secret based on the user's context.
label
as Function
You can also configure label
as a function to resolve OTP label dynamically, eg from Request
.
This can be used to assign different labels to different users, so in the authenticator application,
your project will be presented as Your Project: [email protected], where Your Project is the value
of issuer
, and [email protected] - dynamically resolved label
from current user
Sample OTP Configuration File
An otp.config.ts
file might look like following:
import { registerAs } from '@nestjs/config';
import { IOtpModuleOptions } from "@ognicki/nestjs-otp";
export default registerAs('otp', (): IOtpModuleOptions => ({
label: 'Label',
issuer: 'Issuer',
}));
OtpService
In most use cases, the pairing link and QR codes functions will be used.
Example usage with NestJS controller should look like this:
import { Controller, Get } from '@nestjs/common';
import { OtpService } from '@ognicki/nestjs-otp';
@Controller('otp')
@UseGuards(AuthGuard)
export class OtpController {
constructor(private readonly otpService: OtpService) {}
@Get('pair')
async generatePairingLink(): Promise<string> {
// Generate a pairing link for OTP authentication
const pairingLink = await this.otpService.pair({
secret: 'user_secret_key', // Replace with actual user secret
});
return pairingLink;
}
@Get('qr-image')
async generateQrImage(@Res() res: Response): Promise<void> {
// Generate a QR code image for pairing link and display directly from controller
const pairingLink = await this.otpService.pair({
secret: 'user_secret_key', // Replace with actual user secret
});
const qrDataURL = await this.otpService.qrDataURL(pairingLink);
// Set response content type as image/png
res.setHeader('Content-Type', 'image/png');
// Send the QR code image as response
res.send(Buffer.from(qrDataURL.split(',')[1], 'base64'));
}
}
Important: controller should use the authentication guards, so OTP module can determine the currently authenticated user.