@mercury-labs/auth
v1.1.32
Published
Mercury framework auth library. It supports local auth, jwt with both bearer token and cookie, basic auth.
Downloads
9
Maintainers
Readme
Mercury Auth
A NestJS module package for authentication.
Support both FasitfyAdaptor
and ExpressAdaptor
.
Install
npm install --save @mercury-labs/auth
Learn more about the relevant package @mercury-labs/hashing
Define a database repository to get user info
import {
AUTH_PASSWORD_HASHER,
AuthRepository,
IAuthUserEntity,
PasswordHasherService
} from '@mercury-labs/auth'
import { Injectable } from '@nestjs/common'
import moment from 'moment'
import { map, Observable, scheduled } from 'rxjs'
@Injectable()
export class CmsAuthRepository implements AuthRepository {
public constructor(
@Inject(AUTH_PASSWORD_HASHER)
protected readonly hasher: PasswordHasherService
) {
}
public getAuthUserByUsername(
username: string
): Observable<IAuthUserEntity | undefined> {
// Create sample hashed password for demo only
return scheduled(this.hasher.hash('some-password-phrase'))
.pipe(
// Sample user for demo only
map((password: string) => ({
id: '123456',
firstName: 'John Doe',
lastName: '',
email: '[email protected]',
password,
createdAt: moment().toDate(),
updatedAt: moment().toDate(),
})),
map((user) => {
if (
user.email !== username
) {
return undefined
}
return {
...user,
username: user.email,
}
})
)
}
}
Register AuthModule
to your application module
import { AuthModule, AuthTransferTokenMethod } from '@mercury-labs/auth'
import { ConfigService } from '@nestjs/config'
import { Module } from '@nestjs/common'
@Module({
imports: [
AuthModule.forRootAsync({
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: config.get('AUTH_JWT_EXPIRES') || '1d',
},
transferTokenMethod: config.get<AuthTransferTokenMethod>(
'AUTH_TRANSFER_TOKEN_METHOD'
),
redactedFields: ['password'],
hashingSecretKey: config.get('HASHING_SECRET_KEY') || '',
usernameField: 'username',
passwordField: 'password',
httpAdaptorType: 'fastify'
}
},
inject: [ConfigService],
},
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 sentitive fields while getting user profile.
*/
redactedFields?: string[]
/**
* These routes will always be PUBLIC.
* No authentication required.
*/
ignoredRoutes?: string[]
/**
* 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 bcrypt
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
import crypto from 'crypto'
import { PasswordHasherService } from '@mercury-labs/auth'
import { Injectable } from '@nestjs/common'
export interface IPbkdf2Hash {
hash: string
salt: string
}
@Injectable()
export class Pbkdf2PasswordHasherService implements PasswordHasherService<IPbkdf2Hash> {
public async hash(password: string): Promise<IPbkdf2Hash> {
const salt = crypto.randomBytes(16).toString('hex')
const hash = crypto.pbkdf2Sync(
password,
salt,
10000,
512,
'sha512'
).toString('hex')
return { salt, hash }
}
public async compare(password: string, hashedPassword: IPbkdf2Hash): Promise<boolean> {
const hashPassword = crypto.pbkdf2Sync(
password,
hashedPassword.salt,
10000,
512,
'sha512'
).toString('hex')
return hashedPassword.hash === hashPassword
}
}
Register it to AuthModule
AuthModule.forRootAsync({
...,
passwordHasher: {
useFactory: () => {
return new Pbkdf2PasswordHasherService()
},
}
})
Sample updated CmsAuthRepository
@Injectable()
export class CmsAuthRepository implements AuthRepository {
public constructor(
@Inject(AUTH_PASSWORD_HASHER)
protected readonly hasher: PasswordHasherService<IPbkdf2Hash>
) {
}
public getAuthUserByUsername(
username: string
): Observable<IAuthUserEntity | undefined> {
// Create sample hashed password for demo only
return scheduled(this.hasher.hash('some-password-phrase'))
.pipe(
// Sample user for demo only
map((password: IPbkdf2Hash) => ({
id: '123456',
firstName: 'John Doe',
lastName: '',
email: '[email protected]',
password,
createdAt: moment().toDate(),
updatedAt: moment().toDate(),
})),
map((user) => {
if (
user.email !== username
) {
return undefined
}
return {
...user,
username: user.email,
}
})
)
}
}
Access the login route
curl
curl --request POST \
--url http://localhost:4005/auth/login \
--header 'Content-Type: application/json' \
--data '{
"username": "[email protected]",
"password": "some-password-phrase"
}'
Refresh your access token
curl
curl --request POST \
--url http://localhost:4005/auth/refresh-token \
--header 'Refresh-Token: eyJpdiI6IjFmNTY4ZWZmN2RmODRmZjkxNjQx...'
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.
Controller Decorators
@Public()
This decorator will help your controller available for all users. No authentication required.
import { Public } from '@mercury-labs/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/auth'
import { Controller, Get } from '@nestjs/common'
@Controller()
@InternalOnly()
export class AppController {
@Get()
public getHello(): string {
return 'Hello World!'
}
}
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 AccessToken
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 {your_access_token}
Refesh-Token: {your_refresh_token}
@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/auth'
@ApiTags('User details')
@Controller({ path: 'users/-' })
export class ProfileController {
@ApiOperation({
summary: 'Get profile',
})
@Get('profile')
public profile(
@CurrentUser() user: IAuthUserEntityForResponse
): IAuthUserEntityForResponse {
return user
}
}
Triggered Events
UserLoggedInEvent
Triggered when user logged in successfully.
Sample usages
import { UserLoggedInEvent } from '@mercury-labs/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 })
})
)
)
}
}
Notes:
- You must install package @nestjs/cqrs to work with auth events.
Next plan
I will implement some famous oauth methods
- Login using google/facebook/github...
- Allow user to revoke
accessToken
,refreshToken
of some user. - E2E tests, more tests...