@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
Markdown
# 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 -labs/nest-auth
```
Learn more about the relevant
package [-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 `()` 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 '-labs/nest-auth'
import { Injectable } from '/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(
()
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.com',
email: 'sample-user.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
()
export class SampleAuthRepository implements AuthRepository<string, AuthDto> {
public constructor(
()
protected readonly hasher: PasswordHasherService
) {}
public async getAuthUserByUsername(
username: string
): Promise<IAuthUserEntity | undefined> {
return {
id: _.random(1, 1999).toString(),
username: 'sample-user.com',
email: 'sample-user.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 '-labs/nest-auth'
import { ConfigService } from '/config'
import { Module } from '/common'
({
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 '/common'
import { compare, hash } from 'bcrypt'
import { PasswordHasherService } from './password-hasher.service'
()
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 '/common'
import { ModuleRef } from '/core'
import { mergeMap, Observable, of } from 'rxjs'
import {
AuthDto,
AuthRepository,
IAuthUserEntity,
InjectPasswordHasher,
PasswordHasherService,
} from '-labs/nest-auth'
()
export class SampleAuthRepository implements AuthRepository<IPbkdf2Hash, AuthDto> {
/**
* Your own throttle service
* Sample demo
*/
private _throttleAuthService: ThrottleAuthService
public constructor(
()
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.com',
email: 'sample-user.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
`()`: inject `IAuthDefinitions` to your injectable
classes.
`()`: inject `PasswordHasherService` to your injectable
classes. This is an alias for the statement below:
```typescript
import { Inject, Injectable } from '/common'
import { AUTH_PASSWORD_HASHER } from '-labs/nest-auth'
()
export class SampleService {
public constructor(
(AUTH_PASSWORD_HASHER)
protected readonly hasher: PasswordHasherService
) {}
}
```
### Controller Decorators
`()` This decorator will help your controller available for all users. No
authentication required.
```typescript
import { Public } from '-labs/nest-auth'
import { Controller, Get } from '/common'
()
()
export class AppController {
()
public getHello(): string {
return 'Hello World!'
}
}
```
`()` You need to use basic auth while accessing your controller.
```typescript
import { InternalOnly } from '-labs/nest-auth'
import { Controller, Get } from '/common'
()
()
export class AppController {
()
public getHello(): string {
return 'Hello World!'
}
}
```
`()` 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 '-labs/nest-auth'
import { Controller, Get } from '/common'
()
()
export class AppController {
()
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 '-labs/nest-auth'
import { Controller, Get } from '/common'
import { ApiOperation, ApiTags } from '/swagger'
('Authentication')
({ path: 'auth' })
export class ApiKeyController {
({
summary: 'Get profile by API Key',
})
()
('profile/api-key')
public profileByApiKey(
() user: IAuthUserEntityForResponse
): IAuthUserEntityForResponse {
return user
}
// This route uses JWT authentication (default)
('profile')
public profile(
() user: IAuthUserEntityForResponse
): IAuthUserEntityForResponse {
return user
}
}
```
`()` 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 '/common'
import { AuthRefreshToken, CurrentUserWithToken } from '-labs/nest-auth'
import { CookieAuthInterceptor } from '-labs/nest-auth'
({ path: 'auth' })
()
(CookieAuthInterceptor)
export class RefreshTokenController {
('refresh-token')
public index(
() 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}
```
`()` This decorator will return the current logged-in user.
```typescript
import { Controller, Get } from '/common'
import { ApiOperation, ApiTags } from '/swagger'
import {
IAuthUserEntityForResponse,
CurrentUser
} from '-labs/nest-auth'
('User details')
({path: 'users/-'})
export class ProfileController {
({
summary: 'Get profile',
})
('profile')
public profile(
() user: IAuthUserEntityForResponse
): IAuthUserEntityForResponse {
return user
}
}
```
`()` Same as `()`, but we can get user info
with `()` if you pass your access token into the request header.
```typescript
import { PublicWithOptionalUser } from '-labs/nest-auth'
import { Controller, Get } from '/common'
()
()
export class AppController {
()
public getHello(
() 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 '-labs/nest-auth'
import { EventsHandler, IEventHandler } from '/cqrs'
import { delay, lastValueFrom, of, tap } from 'rxjs'
(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 '-labs/nest-auth'
import { EventsHandler, IEventHandler } from '/cqrs'
import { delay, lastValueFrom, of, tap } from 'rxjs'
(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 '-labs/nest-auth'
import { EventsHandler, IEventHandler } from '/cqrs'
import { delay, lastValueFrom, of, tap } from 'rxjs'
(UserLoggedOutEvent)
export class UserLoggedOutEventHandler implements IEventHandler<UserLoggedOutEvent> {
public async handle(event: UserLoggedOutEvent): Promise<void> {
console.log('UserLoggedOutEvent', event)
}
}
```
#### Notes:
- You must install
package [/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
()
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 `()` decorator to controllers or routes that should accept API key authentication:
```typescript
('api')
()
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
()
export class ApiKeyService {
constructor(
(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
()
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
(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
()
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. `()` - No authentication required
2. `()` - Basic authentication
3. `()` - API Key authentication
4. `()` - 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