@mercury-labs/nest-auth
TypeScript icon, indicating that this package has built-in type declarations

2.0.52 • Public • Published

Mercury Auth

A comprehensive NestJS authentication module with multiple strategies

Mercury Auth is a flexible authentication module for NestJS applications that provides multiple authentication strategies:

  1. JWT Authentication - Token-based authentication with access and refresh tokens
  2. API Key Authentication - For server-to-server communication and integrations
  3. Basic Authentication - For internal/admin routes
  4. Impersonation - For debugging and support scenarios

Key Features

  • Support for both FastifyAdapter and ExpressAdapter
  • Flexible token delivery via cookies, headers, or both
  • Refresh token rotation
  • API Key authentication
  • Role-based access control
  • GraphQL support
  • Event-driven architecture for auth events

Install

npm install --save @mercury-labs/nest-auth

Learn more about the relevant package @mercury-labs/nest-hashing

Authentication Strategies Overview

Mercury Auth provides multiple authentication strategies that can be used independently or together:

JWT Authentication (Default)

The primary authentication method using JSON Web Tokens:

  • Access Token: Short-lived token for API access
  • Refresh Token: Long-lived token to obtain new access tokens
  • Delivery Methods: Bearer token in headers, cookies, or both

API Key Authentication

Simple key-based authentication for server-to-server communication:

  • Single token passed via the Api-Key header
  • No expiration handling (managed by your application)
  • Ideal for service accounts, integrations, and automated processes

Basic Authentication

HTTP Basic Authentication for internal or admin routes:

  • Username and password encoded in the Authorization header
  • Configured via the basicAuth property in your auth definitions
  • Applied using the @InternalOnly() decorator

Impersonation

Allows administrators to log in as other users for support purposes:

  • Special login format: {cipher}username with impersonation password
  • Configurable via the impersonate property in your auth definitions
  • Can be enabled/disabled as needed

Authentication Flow

  1. Login: User provides credentials and receives access and refresh tokens
  2. API Access: User includes access token with requests
  3. Token Refresh: When access token expires, user uses refresh token to get a new one
  4. Logout: User invalidates their tokens

Sample Applications

Complete sample applications demonstrating different adapters and configurations:

Define a database repository to get user info

import {
  AUTH_PASSWORD_HASHER,
  AuthRepository,
  IAuthUserEntity,
  PasswordHasherService
} from '@mercury-labs/nest-auth'
import { Injectable } from '@nestjs/common'
import moment from 'moment'
import { Observable, of } from 'rxjs'

export class SampleAuthRepository implements AuthRepository<string, AuthDto> {
  /**
   * Your own throttle service
   * Sample demo
   */
  private _throttleAuthService: ThrottleAuthService

  public constructor(
    @InjectPasswordHasher()
    protected readonly hasher: PasswordHasherService,
    protected readonly moduleRef: ModuleRef
  ) {
  }

  public onModuleInit(): void {
    this._throttleAuthService = this.moduleRef.get(ThrottleAuthService)
  }

  public getAuthUserByUsername(
    username: string
  ): Observable<IAuthUserEntity | undefined> {
    return of({
      id: 'some-random-id',
      username: 'sample-user@gmail.com',
      email: 'sample-user@gmail.com',
      password: 'some-password-hash',
    })
  }

  public authenticate(
    username: string,
    request: AuthDto,
    impersonated: boolean
  ): Observable<IAuthUserEntity | undefined> {
    /**
     * Ignore the other checks if impersonated
     */
    if (impersonated) {
      return this.getAuthUserByUsername(username)
    }

    /**
     * Do some additional logics
     */
    return this._throttleAuthService.checkAlreadyBlockedAccount(username).pipe(
      mergeMap(() => this.getAuthUserByUsername(username))
    )
  }

  public getAuthUserByAccessToken(
    accessToken: string,
    jwtPayload: IJwtPayload
  ): Observable<IAuthUserEntity | undefined> {
    /**
     * You can check the token if it's stored in database.
     */

    return this.getAuthUserByUsername(jwtPayload.username)
  }

  public getAuthUserByRefreshToken(
    refreshToken: string,
    jwtPayload: IJwtPayload
  ): Observable<IAuthUserEntity | undefined> {
    /**
     * You can check the token if it's stored in database.
     */

    return this.getAuthUserByUsername(jwtPayload.username)
  }
}

The repository demo was built on top of RxJS, but you can also define it with Promise - async/await.

For example:

@Injectable()
export class SampleAuthRepository implements AuthRepository<string, AuthDto> {
  public constructor(
    @InjectPasswordHasher()
    protected readonly hasher: PasswordHasherService
  ) {}

  public async getAuthUserByUsername(
    username: string
  ): Promise<IAuthUserEntity | undefined> {
    return {
      id: _.random(1, 1999).toString(),
      username: 'sample-user@gmail.com',
      email: 'sample-user@gmail.com',
      password: await this.hasher.hash('testLogin@12345'),
    }
  }

  public async authenticate(
    username: string,
    request: AuthDto,
    impersonated: boolean
  ): Promise<IAuthUserEntity | undefined> {
    const user = await this.getAuthUserByUsername(username)

    if (impersonated) {
      return user
    }

    /**
     * Do some additional logics
     */

    return user
  }

  public async getAuthUserByAccessToken(
    accessToken: string,
    jwtPayload: IJwtPayload
  ): Promise<IAuthUserEntity | undefined> {
    /**
     * You can check the token if it's stored in database.
     */

    return this.getAuthUserByUsername(jwtPayload.username)
  }

  public async getAuthUserByRefreshToken(
    refreshToken: string,
    jwtPayload: IJwtPayload
  ): Promise<IAuthUserEntity | undefined> {
    /**
     * You can check the token if it's stored in database.
     */

    return this.getAuthUserByUsername(jwtPayload.username)
  }
}

Register AuthModule to your application module

import { AuthModule, AuthTransferTokenMethod } from '@mercury-labs/nest-auth'
import { ConfigService } from '@nestjs/config'
import { Module } from '@nestjs/common'

@Module({
  imports: [
    AuthModule.forRootAsync({
      /**
       * Use default local auth or not?
       * For example, you're using GraphQL and want to disable default HTTP auth
       */
      useLocalAuth: true,
      global: true,
      definitions: {
        useFactory: (config: ConfigService) => {
          return {
            basicAuth: {
              username: config.get('BASIC_AUTH_USER'),
              password: config.get('BASIC_AUTH_PASSWORD'),
            },
            impersonate: {
              isEnabled: config.get('AUTH_IMPERSONATE_ENABLED') === 'true',
              cipher: config.get('AUTH_IMPERSONATE_CIPHER'),
              password: config.get('AUTH_IMPERSONATE_PASSWORD'),
            },
            jwt: {
              secret: config.get('AUTH_JWT_SECRET'),
              expiresIn: envRequired('AUTH_JWT_EXPIRES') || '1h',
              refreshTokenExpiresIn: envRequired('AUTH_JWT_REFRESH_EXPIRES') || '7d',
            },
            transferTokenMethod: config.get<AuthTransferTokenMethod>(
              'AUTH_TRANSFER_TOKEN_METHOD'
            ),
            redactedFields: ['password'],
            hashingSecretKey: config.get('HASHING_SECRET_KEY') || '',
            usernameField: 'username',
            passwordField: 'password',
            httpAdaptorType: 'fastify'
          }
        },
        inject: [ConfigService],
      },
      /**
       * In case you want to register some providers inside AuthModule
       * These providers will be also exported from AuthModule
       */
      otherProviders: [],
      authRepository: {
        useFactory: (hasher: PasswordHasherService) => {
          return new CmsAuthRepository(hasher)
        },
        inject: [AUTH_PASSWORD_HASHER]
      }
    }),
  ]
})
export class AppModule {
}

Notes:

interface IAuthDefinitions {
  /**
   * Configuration for basic auth
   */
  basicAuth: {
    username: string
    password: string
    /**
     * The realm name for WWW-Authenticate header
     */
    realm?: string
  }

  /**
   * Configuration for JWT
   */
  jwt: {
    /**
     * Do not expose this key publicly.
     * We have done so here to make it clear what the code is doing,
     * but in a production system you must protect this key using appropriate measures,
     * such as a secrets vault, environment variable, or configuration service.
     */
    secret: string
    /**
     * Expressed in seconds or a string describing a time span zeit/ms.
     * @see https://github.com/vercel/ms
     * Eg: 60, “2 days”, “10h”, “7d”
     */
    expiresIn: string | number
    refreshTokenExpiresIn: string | number
  }

  /**
   * Configuration for impersonate login
   * You can login to a user account without their password.
   * Eg:
   *   - username: {your_impersonate_cipher_key}username
   *   - password: {your_impersonate_password}
   */
  impersonate?: {
    isEnabled: boolean
    cipher: string
    password: string
  }

  /**
   * Hide some sensitive fields while getting user profile.
   */
  redactedFields?: string[]

  /**
   * These routes will always be PUBLIC.
   * No authentication required.
   */
  ignoredRoutes?: string[]

  enableHashingToken?: boolean
  /**
   * Used to encode/decode the access/refresh token
   * 32 characters string
   */
  hashingSecretKey?: string

  /**
   * We accepted these 3 values: cookie|bearer|both
   * - cookie: after user login, their accessToken and refreshToken will be sent using cookie
   * - bearer: after user login, their accessToken and refreshToken will be sent to response body
   * - both: mixed those 2 above values.
   */
  transferTokenMethod: AuthTransferTokenMethod,

  cookieOptions?: {
    domain?: string
    path?: string // Default '/'
    sameSite?: boolean | 'lax' | 'strict' | 'none' // Default true
    signed?: boolean
    httpOnly?: boolean // Default true
    secure?: boolean
  },

  /**
   * Username field when login
   * Eg: email, username,...
   */
  usernameField?: string

  /**
   * Password field when login
   * Eg: password, pass...
   */
  passwordField?: string,

  httpAdaptorType: 'fastify' | 'express'
}

Customize your hasher method

By default, I use pbkdf2 to encode and compare password hash. In some case, you might need to change the way or algorithm to hash the password.

Create new hasher class

This example uses bcrypt to hash and compare password phrase.

import { Injectable } from '@nestjs/common'
import { compare, hash } from 'bcrypt'
import { PasswordHasherService } from './password-hasher.service'

@Injectable()
export class BcryptPasswordHasherService
  implements PasswordHasherService<string>
{
  public async hash(password: string): Promise<string> {
    return hash(password, 10)
  }

  public async compare(
    password: string,
    hashedPassword: string
  ): Promise<boolean> {
    return compare(password, hashedPassword)
  }
}

Register it to AuthModule

AuthModule.forRootAsync({
  ...,
  passwordHasher: {
    useFactory: () => {
      return new BcryptPasswordHasherService()
    },
  }
})

Sample updated CmsAuthRepository

import { Injectable } from '@nestjs/common'
import { ModuleRef } from '@nestjs/core'
import { mergeMap, Observable, of } from 'rxjs'
import {
  AuthDto,
  AuthRepository,
  IAuthUserEntity,
  InjectPasswordHasher,
  PasswordHasherService,
} from '@mercury-labs/nest-auth'

@Injectable()
export class SampleAuthRepository implements AuthRepository<IPbkdf2Hash, AuthDto> {
  /**
   * Your own throttle service
   * Sample demo
   */
  private _throttleAuthService: ThrottleAuthService

  public constructor(
    @InjectPasswordHasher()
    protected readonly hasher: PasswordHasherService,
    protected readonly moduleRef: ModuleRef
  ) {
  }

  public onModuleInit(): void {
    this._throttleAuthService = this.moduleRef.get(ThrottleAuthService)
  }

  public getAuthUserByUsername(
    username: string
  ): Observable<IAuthUserEntity<IPbkdf2Hash> | undefined> {
    return of({
      id: _.random(1, 1999).toString(),
      username: 'sample-user@gmail.com',
      email: 'sample-user@gmail.com',
      password: {
        hash: 'some-hash',
        salt: 'some-salt'
      },
    })
  }

  public authenticate(
    username: string,
    request: AuthDto,
    impersonated: boolean
  ): Observable<IAuthUserEntity<IPbkdf2Hash> | undefined> {
    /**
     * Ignore the other checks if impersonated
     */
    if (impersonated) {
      return this.getAuthUserByUsername(username)
    }

    /**
     * Do some additional logics
     */
    return this._throttleAuthService.checkAlreadyBlockedAccount(username).pipe(
      mergeMap(() => this.getAuthUserByUsername(username))
    )
  }

  public getAuthUserByAccessToken(
    accessToken: string,
    jwtPayload: IJwtPayload
  ): Observable<IAuthUserEntity<IPbkdf2Hash> | undefined> {
    /**
     * You can check the token if it's stored in database.
     */

    return this.getAuthUserByUsername(jwtPayload.username)
  }

  public getAuthUserByRefreshToken(
    refreshToken: string,
    jwtPayload: IJwtPayload
  ): Observable<IAuthUserEntity<IPbkdf2Hash> | undefined> {
    /**
     * You can check the token if it's stored in database.
     */

    return this.getAuthUserByUsername(jwtPayload.username)
  }
}

Access the login route

curl

curl --request POST \
  --url http://localhost:4005/auth/login \
  --header 'Content-Type: application/json' \
  --data '{
	"username": "sample-email+dev@gmail.com",
	"password": "some-password-phrase"
}'

Refresh your access token

When your access token expires, you can use the refresh token to get a new access token without requiring the user to log in again. The refresh token has a longer expiration time than the access token (configured via refreshTokenExpiresIn in your JWT configuration).

Using the refresh token endpoint

The package provides a built-in endpoint for refreshing tokens at /auth/refresh-token. You need to include your refresh token in the request.

When using bearer token method:
curl --request POST \
  --url http://localhost:4005/auth/refresh-token \
  --header 'Refresh-Token: eyJpdiI6IjFmNTY4ZWZmN2RmODRmZjkxNjQx...'
When using cookie method:

The refresh token is automatically included in the cookie, so you just need to make the request:

curl --request POST \
  --url http://localhost:4005/auth/refresh-token \
  --cookie "refreshToken=eyJpdiI6IjFmNTY4ZWZmN2RmODRmZjkxNjQx..."
Response structure

The response will include a new access token but not a new refresh token:

{
  "userData": {
    "id": "user-id",
    "username": "sample-user@gmail.com",
    "email": "sample-user@gmail.com"
  },
  "token": {
    "accessToken": "new-access-token",
    "expiryDate": "2023-01-01T00:00:00.000Z"
  }
}

Get your logged in user profile

curl

curl --request GET \
  --url http://localhost:4005/auth/profile \
  --header 'Authorization: Bearer eyJpdiI6IjFmNTY4ZWZmN2RmODRmZjkxNjQx...'

Logout

curl

curl --request POST \
  --url http://localhost:4005/auth/logout
  --header 'Authorization: Bearer eyJpdiI6IjFmNTY4ZWZmN2RmODRmZjkxNjQx...'

Injection Decorators

@InjectAuthDefinitions(): inject IAuthDefinitions to your injectable classes.

@InjectPasswordHasher(): inject PasswordHasherService to your injectable classes. This is an alias for the statement below:

import { Inject, Injectable } from '@nestjs/common'
import { AUTH_PASSWORD_HASHER } from '@mercury-labs/nest-auth'

@Injectable()
export class SampleService {
  public constructor(
    @Inject(AUTH_PASSWORD_HASHER)
    protected readonly hasher: PasswordHasherService
  ) {}
}

Controller Decorators

@Public() This decorator will help your controller available for all users. No authentication required.

import { Public } from '@mercury-labs/nest-auth'
import { Controller, Get } from '@nestjs/common'

@Controller()
@Public()
export class AppController {
  @Get()
  public getHello(): string {
    return 'Hello World!'
  }
}

@InternalOnly() You need to use basic auth while accessing your controller.

import { InternalOnly } from '@mercury-labs/nest-auth'
import { Controller, Get } from '@nestjs/common'

@Controller()
@InternalOnly()
export class AppController {
  @Get()
  public getHello(): string {
    return 'Hello World!'
  }
}

@AuthApiKey() This decorator applies the API Key authentication guard to your controller or route. It requires an Api-Key header to be present in the request.

import { AuthApiKey } from '@mercury-labs/nest-auth'
import { Controller, Get } from '@nestjs/common'

@Controller()
@AuthApiKey()
export class AppController {
  @Get()
  public getHello(): string {
    return 'Hello World!'
  }
}

You can also apply the decorator to specific routes instead of the entire controller:

import { AuthApiKey, CurrentUser } from '@mercury-labs/nest-auth'
import { Controller, Get } from '@nestjs/common'
import { ApiOperation, ApiTags } from '@nestjs/swagger'

@ApiTags('Authentication')
@Controller({ path: 'auth' })
export class ApiKeyController {
  @ApiOperation({
    summary: 'Get profile by API Key',
  })
  @AuthApiKey()
  @Get('profile/api-key')
  public profileByApiKey(
    @CurrentUser() user: IAuthUserEntityForResponse
  ): IAuthUserEntityForResponse {
    return user
  }

  // This route uses JWT authentication (default)
  @Get('profile')
  public profile(
    @CurrentUser() user: IAuthUserEntityForResponse
  ): IAuthUserEntityForResponse {
    return user
  }
}

@AuthRefreshToken() This decorator applies the refresh token guard to your controller or route. It's used internally by the refresh token endpoint.

import { Controller, Post, UseInterceptors } from '@nestjs/common'
import { AuthRefreshToken, CurrentUserWithToken } from '@mercury-labs/nest-auth'
import { CookieAuthInterceptor } from '@mercury-labs/nest-auth'

@Controller({ path: 'auth' })
@AuthRefreshToken()
@UseInterceptors(CookieAuthInterceptor)
export class RefreshTokenController {
  @Post('refresh-token')
  public index(
    @CurrentUserWithToken() user: IAuthWithTokenResponse
  ): Observable<IRefreshTokenAuthResponse> {
    // Implementation to return a new access token
    // The refresh token is extracted from the request by the guard
  }
}

JWT By default, all another routes will be checked using JWT strategy guard.

It means, you need to pass your access token into the request header.

If you set the transfer method to both or cookie, you don't need to do anything. The Authorization and RefreshToken already be sent via cookie.

If you set the transfer method to bearer, you need to pass your access token to the Authorization header.

Authorization: Bearer {yourAccessToken}
Refresh-Token: {yourRefreshToken}

@CurrentUser() This decorator will return the current logged-in user.

import { Controller, Get } from '@nestjs/common'
import { ApiOperation, ApiTags } from '@nestjs/swagger'
import {
  IAuthUserEntityForResponse,
  CurrentUser
} from '@mercury-labs/nest-auth'

@ApiTags('User details')
@Controller({path: 'users/-'})
export class ProfileController {
  @ApiOperation({
    summary: 'Get profile',
  })
  @Get('profile')
  public profile(
    @CurrentUser() user: IAuthUserEntityForResponse
  ): IAuthUserEntityForResponse {
    return user
  }
}

@PublicWithOptionalUser() Same as @Public(), but we can get user info with @CurrentUser() if you pass your access token into the request header.

import { PublicWithOptionalUser } from '@mercury-labs/nest-auth'
import { Controller, Get } from '@nestjs/common'

@Controller()
@PublicWithOptionalUser()
export class AppController {
  @Get()
  public getHello(
    @CurrentUser() user?: IAuthUserEntityForResponse | undefined
  ): string {
    /**
     * user is nullable
     */
    console.log({
      userId: user?.id
    })
    return 'Hello World!'
  }
}

Triggered Events

UserLoggedInEvent

Triggered when user logged in successfully.

You can store the relevant access/refresh tokens in database using this event.

Sample usages

import { UserLoggedInEvent } from '@mercury-labs/nest-auth'
import { EventsHandler, IEventHandler } from '@nestjs/cqrs'
import { delay, lastValueFrom, of, tap } from 'rxjs'

@EventsHandler(UserLoggedInEvent)
export class UserLoggedInEventHandler implements IEventHandler<UserLoggedInEvent> {
  public async handle(event: UserLoggedInEvent): Promise<void> {
    await lastValueFrom(
      of(event).pipe(
        delay(1200),
        tap(({user, isImpersonated}) => {
          console.log('UserLoggedInEvent', {user, isImpersonated})
        })
      )
    )
  }
}

AccessTokenGeneratedFromRefreshTokenEvent

Triggered when a new access token is generated from refresh token.

You can store the relevant new access token in database using this event.

Sample usages

import { AccessTokenGeneratedFromRefreshTokenEvent } from '@mercury-labs/nest-auth'
import { EventsHandler, IEventHandler } from '@nestjs/cqrs'
import { delay, lastValueFrom, of, tap } from 'rxjs'

@EventsHandler(AccessTokenGeneratedFromRefreshTokenEvent)
export class AccessTokenGeneratedFromRefreshTokenEventHandler
  implements IEventHandler<AccessTokenGeneratedFromRefreshTokenEvent> {
  public async handle(event: AccessTokenGeneratedFromRefreshTokenEvent): Promise<void> {
    console.log('AccessTokenGeneratedFromRefreshTokenEvent', event)
  }
}

UserLoggedOutEvent

Triggered when user logged out.

Sample usages

import { UserLoggedOutEvent } from '@mercury-labs/nest-auth'
import { EventsHandler, IEventHandler } from '@nestjs/cqrs'
import { delay, lastValueFrom, of, tap } from 'rxjs'

@EventsHandler(UserLoggedOutEvent)
export class UserLoggedOutEventHandler implements IEventHandler<UserLoggedOutEvent> {
  public async handle(event: UserLoggedOutEvent): Promise<void> {
    console.log('UserLoggedOutEvent', event)
  }
}

Notes:

  • You must install package @nestjs/cqrs to work with auth events.

API Key Authentication

Mercury Auth provides a robust API Key authentication strategy that allows clients to authenticate using an API key instead of JWT tokens. This is particularly useful for server-to-server communication, background jobs, or third-party integrations where the traditional username/password flow isn't appropriate.

How API Key Authentication Works

  1. The client includes an API key in the request header (Api-Key)
  2. The ApiKeyStrategy extracts the API key from the request
  3. The strategy calls the getAuthUserByApiKey method on your AuthRepository implementation
  4. If a valid user is returned, the request is authenticated

Implementing API Key Support

To implement API Key authentication, you need to:

  1. Implement the getAuthUserByApiKey method in your AuthRepository:
@Injectable()
export class YourAuthRepository implements AuthRepository<string, AuthDto> {
  // Other required methods...

  public async getAuthUserByApiKey(
    apiKey: string
  ): Promise<IAuthUserEntity | undefined> {
    // Validate the API key against your database or other storage
    const user = await this.userRepository.findByApiKey(apiKey);

    if (!user) {
      return undefined; // Invalid API key
    }

    return {
      id: user.id,
      username: user.email,
      email: user.email,
      // Other user properties
    };
  }
}
  1. Apply the @AuthApiKey() decorator to controllers or routes that should accept API key authentication:
@Controller('api')
@AuthApiKey()
export class ApiController {
  // All routes in this controller require API key authentication
}

Generating and Managing API Keys

Mercury Auth doesn't provide built-in API key generation or management, as these are typically application-specific. Here's a recommended approach:

  1. Create a service for generating and managing API keys:
@Injectable()
export class ApiKeyService {
  constructor(
    @InjectRepository(ApiKeyEntity)
    private apiKeyRepository: Repository<ApiKeyEntity>,
  ) {}

  public async generateApiKey(userId: string, name: string): Promise<string> {
    // Generate a secure random API key
    const apiKey = crypto.randomUUID();

    // Store it in your database
    await this.apiKeyRepository.save({
      userId,
      name,
      key: apiKey,
      createdAt: new Date(),
    });

    return apiKey;
  }

  public async validateApiKey(apiKey: string): Promise<User | null> {
    const apiKeyRecord = await this.apiKeyRepository.findOne({
      where: { key: apiKey, isActive: true },
      relations: ['user'],
    });

    return apiKeyRecord?.user || null;
  }

  public async revokeApiKey(apiKeyId: string): Promise<void> {
    await this.apiKeyRepository.update(
      { id: apiKeyId },
      { isActive: false, revokedAt: new Date() }
    );
  }
}
  1. Use this service in your AuthRepository implementation:
@Injectable()
export class YourAuthRepository implements AuthRepository<string, AuthDto> {
  constructor(private apiKeyService: ApiKeyService) {}

  public async getAuthUserByApiKey(
    apiKey: string
  ): Promise<IAuthUserEntity | undefined> {
    const user = await this.apiKeyService.validateApiKey(apiKey);

    if (!user) {
      return undefined;
    }

    return {
      id: user.id,
      username: user.email,
      email: user.email,
      // Other user properties
    };
  }
}

Testing API Key Authentication

You can test API key authentication using curl:

curl --request GET \
  --url http://localhost:4005/auth/profile/api-key \
  --header 'Api-Key: your-api-key-here'

Security Best Practices for API Keys

  1. Generate Strong Keys: Use a cryptographically secure random generator
  2. Store Securely: Never store API keys in plaintext; use hashing
  3. Set Expirations: Consider adding expiration dates to API keys
  4. Limit Scope: Assign specific permissions to each API key
  5. Monitor Usage: Log and monitor API key usage for suspicious activity
  6. Rotate Regularly: Encourage or enforce regular key rotation

Understanding Token Interfaces

Token Response Interfaces

The package provides several interfaces for handling authentication responses:

// Full authentication response with both access and refresh tokens
interface IAuthWithTokenResponse<UserDataType = IAuthUserEntityForResponse> {
  userData: UserDataType;
  token: IJwtTokenResponse;
}

// Response when refreshing tokens (no new refresh token)
interface IRefreshTokenAuthResponse<UserDataType = IAuthUserEntityForResponse> {
  userData: UserDataType;
  token: Omit<IJwtTokenResponse, 'refreshToken' | 'refreshTokenExpiryDate'>;
}

// JWT token response structure
interface IJwtTokenResponse {
  accessToken: string;
  refreshToken: string;
  expiryDate: Date;
  refreshTokenExpiryDate: Date;
}

Implementing Token Storage

For production applications, you should consider storing tokens in your database to enable features like:

  • Token revocation
  • Tracking active sessions
  • Limiting concurrent logins

You can use the UserLoggedInEvent to store tokens when a user logs in:

@EventsHandler(UserLoggedInEvent)
export class UserLoggedInEventHandler implements IEventHandler<UserLoggedInEvent> {
  constructor(private readonly tokenRepository: TokenRepository) {}

  public async handle(event: UserLoggedInEvent): Promise<void> {
    const { user, accessToken, refreshToken } = event;

    // Store tokens in your database
    await this.tokenRepository.saveTokens({
      userId: user.id,
      accessToken,
      refreshToken,
      // Add other metadata as needed
    });
  }
}

Then in your AuthRepository implementation, you can validate tokens against your database:

public async getAuthUserByRefreshToken(
  refreshToken: string,
  jwtPayload: IJwtPayload
): Promise<IAuthUserEntity | undefined> {
  // Check if the token exists and is valid in your database
  const tokenRecord = await this.tokenRepository.findByRefreshToken(refreshToken);

  if (!tokenRecord || tokenRecord.isRevoked) {
    return undefined;
  }

  // Get the user associated with this token
  return this.getAuthUserByUsername(jwtPayload.username);
}

Authentication Guards

Mercury Auth uses a global guard system that automatically applies the appropriate authentication strategy based on route decorators. Here's how the guards work together:

Global Guard

The AuthGlobalGuard is registered as a global guard and determines which specific guard to apply based on route decorators:

@Injectable()
export class AuthGlobalGuard implements CanActivate {
  // ...

  public canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    // Check if route is public
    const isPublic = this._reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ])

    if (isPublic) {
      return true; // Allow access without authentication
    }

    // Check if route is internal only (basic auth)
    const isInternalOnly = this._reflector.getAllAndOverride<boolean>(IS_INTERNAL_ONLY, [
      context.getHandler(),
      context.getClass()
    ])

    if (isInternalOnly) {
      return this._basicAuthGuard.canActivate(context);
    }

    // Check if route requires API key
    const isApiKey = this._reflector.getAllAndOverride<boolean>(IS_API_KEY, [
      context.getHandler(),
      context.getClass()
    ])

    if (isApiKey) {
      return this._authApiKeyGuard.canActivate(context);
    }

    // Check if route is for refresh token
    const isRefreshToken = this._reflector.getAllAndOverride<boolean>(IS_REFRESH_TOKEN_KEY, [
      context.getHandler(),
      context.getClass()
    ])

    if (isRefreshToken) {
      return this._refreshTokenGuard.canActivate(context);
    }

    // Default to JWT authentication
    return this._jwtGuard.canActivate(context);
  }
}

Guard Priority

When multiple decorators are applied, the guards are evaluated in this order:

  1. @Public() - No authentication required
  2. @InternalOnly() - Basic authentication
  3. @AuthApiKey() - API Key authentication
  4. @AuthRefreshToken() - Refresh token authentication
  5. Default - JWT authentication

GraphQL Support

Mercury Auth provides GraphQL-specific guards that work with the NestJS GraphQL module:

  • GraphqlAuthJwtGuard
  • GraphqlAuthRefreshTokenGuard
  • GraphqlAuthApiKeyGuard

These guards extract authentication data from the GraphQL context instead of HTTP requests.

Next plan

  • Allow user to revoke accessToken, refreshToken of some user.
  • Add support for token blacklisting
  • Implement token rotation for enhanced security
  • Add support for OAuth2 authentication providers

Package Sidebar

Install

npm i @mercury-labs/nest-auth

Weekly Downloads

114

Version

2.0.52

License

MIT

Unpacked Size

327 kB

Total Files

294

Last publish

Collaborators

  • duysolo