npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@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

408

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...