UNPKG

@mercury-labs/nest-auth

Version:

Mercury framework auth library. It supports local auth, jwt with both bearer token and cookie, basic auth.

1,178 lines (948 loc) 31.9 kB
# 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 ```shell npm install --save @mercury-labs/nest-auth ``` Learn more about the relevant package [@mercury-labs/nest-hashing](https://www.npmjs.com/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: - [Fastify Sample](https://github.com/duysolo/mercury-auth-samples/tree/master/fastify) - [Express Sample](https://github.com/duysolo/mercury-auth-samples/tree/master/express) - [Apollo GraphQL Express Sample](https://github.com/duysolo/mercury-auth-samples/tree/master/apollo-graphql-express) ## Define a database repository to get user info ```typescript 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: ```typescript @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 ```typescript 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: ```typescript 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. ```typescript 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` ```typescript AuthModule.forRootAsync({ ..., passwordHasher: { useFactory: () => { return new BcryptPasswordHasherService() }, } }) ``` #### Sample updated `CmsAuthRepository` ```typescript 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: ```json { "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: ```typescript 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. ```typescript 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. ```typescript 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. ```typescript 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: ```typescript 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. ```typescript 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. ```typescript 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. ```typescript 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 ```typescript 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 ```typescript 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 ```typescript 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](https://www.npmjs.com/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`: ```typescript @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 }; } } ``` 2. Apply the `@AuthApiKey()` decorator to controllers or routes that should accept API key authentication: ```typescript @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: ```typescript @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() } ); } } ``` 2. Use this service in your `AuthRepository` implementation: ```typescript @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: ```bash 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: ```typescript // 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: ```typescript @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: ```typescript 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: ```typescript @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