|
| 1 | +<p align="center"> |
| 2 | + <h1 align="center">@nestbolt/authentication</h1> |
| 3 | + <p align="center">Frontend-agnostic authentication backend for NestJS</p> |
| 4 | +</p> |
| 5 | + |
| 6 | +<p align="center"> |
| 7 | + <a href="https://www.npmjs.com/package/@nestbolt/authentication"><img src="https://img.shields.io/npm/v/@nestbolt/authentication.svg" alt="NPM Version" /></a> |
| 8 | + <a href="https://www.npmjs.com/package/@nestbolt/authentication"><img src="https://img.shields.io/npm/dm/@nestbolt/authentication.svg" alt="NPM Downloads" /></a> |
| 9 | + <a href="https://github.com/nestbolt/authentication/actions"><img src="https://github.com/nestbolt/authentication/workflows/Tests/badge.svg" alt="Tests" /></a> |
| 10 | + <a href="https://github.com/nestbolt/authentication/blob/main/LICENSE.md"><img src="https://img.shields.io/npm/l/@nestbolt/authentication.svg" alt="License" /></a> |
| 11 | +</p> |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +A complete, database-agnostic authentication backend for NestJS with support for registration, login, password reset, email verification, profile management, password confirmation, and two-factor authentication (TOTP). |
| 16 | + |
| 17 | +Inspired by [Laravel Fortify](https://github.com/laravel/fortify). |
| 18 | + |
| 19 | +## Table of Contents |
| 20 | + |
| 21 | +- [Installation](#installation) |
| 22 | +- [Quick Start](#quick-start) |
| 23 | +- [Module Configuration](#module-configuration) |
| 24 | +- [Features](#features) |
| 25 | +- [Database Adapters](#database-adapters) |
| 26 | +- [API Routes](#api-routes) |
| 27 | +- [Events](#events) |
| 28 | +- [Configuration Options](#configuration-options) |
| 29 | +- [Testing](#testing) |
| 30 | +- [Changelog](#changelog) |
| 31 | +- [Contributing](#contributing) |
| 32 | +- [License](#license) |
| 33 | + |
| 34 | +## Installation |
| 35 | + |
| 36 | +```bash |
| 37 | +# pnpm |
| 38 | +pnpm add @nestbolt/authentication |
| 39 | + |
| 40 | +# npm |
| 41 | +npm install @nestbolt/authentication |
| 42 | + |
| 43 | +# yarn |
| 44 | +yarn add @nestbolt/authentication |
| 45 | +``` |
| 46 | + |
| 47 | +### Peer Dependencies |
| 48 | + |
| 49 | +```bash |
| 50 | +pnpm add @nestjs/passport @nestjs/jwt passport passport-jwt passport-local class-validator class-transformer reflect-metadata |
| 51 | +``` |
| 52 | + |
| 53 | +Optional: |
| 54 | +```bash |
| 55 | +pnpm add @nestjs/event-emitter @nestjs/throttler |
| 56 | +``` |
| 57 | + |
| 58 | +## Quick Start |
| 59 | + |
| 60 | +1. **Implement the `UserRepository` interface** for your database: |
| 61 | + |
| 62 | +```typescript |
| 63 | +import { Injectable } from "@nestjs/common"; |
| 64 | +import { UserRepository, AuthUser } from "@nestbolt/authentication"; |
| 65 | + |
| 66 | +@Injectable() |
| 67 | +export class MyUserRepository implements UserRepository { |
| 68 | + async findById(id: string): Promise<AuthUser | null> { /* ... */ } |
| 69 | + async findByField(field: string, value: string): Promise<AuthUser | null> { /* ... */ } |
| 70 | + async save(user: Partial<AuthUser> & { id: string }): Promise<AuthUser> { /* ... */ } |
| 71 | + async create(data: Omit<AuthUser, "id">): Promise<AuthUser> { /* ... */ } |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +2. **Import `AuthenticationModule`** in your app module: |
| 76 | + |
| 77 | +```typescript |
| 78 | +import { AuthenticationModule, Feature } from "@nestbolt/authentication"; |
| 79 | +import { MyUserRepository } from "./my-user.repository"; |
| 80 | + |
| 81 | +@Module({ |
| 82 | + imports: [ |
| 83 | + AuthenticationModule.forRoot({ |
| 84 | + features: [ |
| 85 | + Feature.REGISTRATION, |
| 86 | + Feature.RESET_PASSWORDS, |
| 87 | + Feature.EMAIL_VERIFICATION, |
| 88 | + Feature.UPDATE_PROFILE_INFORMATION, |
| 89 | + Feature.UPDATE_PASSWORDS, |
| 90 | + Feature.TWO_FACTOR_AUTHENTICATION, |
| 91 | + ], |
| 92 | + userRepository: MyUserRepository, |
| 93 | + jwtSecret: process.env.JWT_SECRET!, |
| 94 | + refreshSecret: process.env.REFRESH_SECRET!, |
| 95 | + encryptionKey: process.env.ENCRYPTION_KEY!, // 32-byte base64 |
| 96 | + appName: "MyApp", |
| 97 | + }), |
| 98 | + ], |
| 99 | +}) |
| 100 | +export class AppModule {} |
| 101 | +``` |
| 102 | + |
| 103 | +3. **That's it!** All 17 auth routes are now available. |
| 104 | + |
| 105 | +## Module Configuration |
| 106 | + |
| 107 | +### Synchronous |
| 108 | + |
| 109 | +```typescript |
| 110 | +AuthenticationModule.forRoot({ |
| 111 | + features: [Feature.REGISTRATION, Feature.TWO_FACTOR_AUTHENTICATION], |
| 112 | + userRepository: TypeOrmUserRepository, |
| 113 | + passwordResetRepository: TypeOrmPasswordResetRepository, |
| 114 | + jwtSecret: "your-jwt-secret", |
| 115 | + refreshSecret: "your-refresh-secret", |
| 116 | + encryptionKey: "base64-encoded-32-byte-key", |
| 117 | + appName: "MyApp", |
| 118 | +}); |
| 119 | +``` |
| 120 | + |
| 121 | +### Asynchronous |
| 122 | + |
| 123 | +```typescript |
| 124 | +AuthenticationModule.forRootAsync({ |
| 125 | + imports: [ConfigModule], |
| 126 | + inject: [ConfigService], |
| 127 | + useFactory: (config: ConfigService) => ({ |
| 128 | + features: [Feature.REGISTRATION, Feature.TWO_FACTOR_AUTHENTICATION], |
| 129 | + userRepository: TypeOrmUserRepository, |
| 130 | + jwtSecret: config.get("JWT_SECRET"), |
| 131 | + refreshSecret: config.get("REFRESH_SECRET"), |
| 132 | + encryptionKey: config.get("ENCRYPTION_KEY"), |
| 133 | + }), |
| 134 | +}); |
| 135 | +``` |
| 136 | + |
| 137 | +## Features |
| 138 | + |
| 139 | +Enable or disable features via the `features` array: |
| 140 | + |
| 141 | +| Feature | Description | |
| 142 | +|---------|-------------| |
| 143 | +| `Feature.REGISTRATION` | User registration (POST /register) | |
| 144 | +| `Feature.RESET_PASSWORDS` | Password reset flow (POST /forgot-password, POST /reset-password) | |
| 145 | +| `Feature.EMAIL_VERIFICATION` | Email verification (GET /email/verify/:id/:hash) | |
| 146 | +| `Feature.UPDATE_PROFILE_INFORMATION` | Profile updates (PUT /user/profile-information) | |
| 147 | +| `Feature.UPDATE_PASSWORDS` | Password updates (PUT /user/password) | |
| 148 | +| `Feature.TWO_FACTOR_AUTHENTICATION` | Full 2FA with TOTP, QR codes, and recovery codes | |
| 149 | + |
| 150 | +## Database Adapters |
| 151 | + |
| 152 | +The package is **database-agnostic**. Implement `UserRepository` and optionally `PasswordResetRepository` for any database: |
| 153 | + |
| 154 | +### TypeORM (SQL) |
| 155 | +```typescript |
| 156 | +@Injectable() |
| 157 | +export class TypeOrmUserRepository implements UserRepository { |
| 158 | + constructor(@InjectRepository(User) private repo: Repository<User>) {} |
| 159 | + findById(id: string) { return this.repo.findOneBy({ id }); } |
| 160 | + findByField(field: string, value: string) { return this.repo.findOneBy({ [field]: value }); } |
| 161 | + save(user) { return this.repo.save(user); } |
| 162 | + create(data) { return this.repo.save(this.repo.create(data)); } |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +### Mongoose (MongoDB) |
| 167 | +```typescript |
| 168 | +@Injectable() |
| 169 | +export class MongooseUserRepository implements UserRepository { |
| 170 | + constructor(@InjectModel(User.name) private model: Model<UserDocument>) {} |
| 171 | + findById(id: string) { return this.model.findById(id).lean().exec(); } |
| 172 | + findByField(field: string, value: string) { return this.model.findOne({ [field]: value }).lean().exec(); } |
| 173 | + save(user) { return this.model.findByIdAndUpdate(user.id, user, { new: true }).lean().exec(); } |
| 174 | + create(data) { return this.model.create(data); } |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +### Prisma, MikroORM, DynamoDB, etc. |
| 179 | +Same pattern - implement the interface for your ORM/driver. |
| 180 | + |
| 181 | +## API Routes |
| 182 | + |
| 183 | +| Method | Route | Description | Auth | |
| 184 | +|--------|-------|-------------|------| |
| 185 | +| POST | /login | Authenticate user | No | |
| 186 | +| POST | /logout | Log out | JWT | |
| 187 | +| POST | /register | Create new user | No | |
| 188 | +| POST | /forgot-password | Send reset link | No | |
| 189 | +| POST | /reset-password | Reset password | No | |
| 190 | +| GET | /email/verify/:id/:hash | Verify email | JWT | |
| 191 | +| POST | /email/verification-notification | Resend verification | JWT | |
| 192 | +| PUT | /user/profile-information | Update profile | JWT | |
| 193 | +| PUT | /user/password | Change password | JWT | |
| 194 | +| POST | /user/confirm-password | Confirm password | JWT | |
| 195 | +| GET | /user/confirmed-password-status | Check confirmation | JWT | |
| 196 | +| POST | /user/two-factor-authentication | Enable 2FA | JWT | |
| 197 | +| DELETE | /user/two-factor-authentication | Disable 2FA | JWT | |
| 198 | +| POST | /user/confirmed-two-factor-authentication | Confirm 2FA setup | JWT | |
| 199 | +| GET | /user/two-factor-qr-code | Get QR code SVG | JWT | |
| 200 | +| GET | /user/two-factor-secret-key | Get TOTP secret | JWT | |
| 201 | +| GET/POST | /user/two-factor-recovery-codes | Get/regenerate codes | JWT | |
| 202 | +| POST | /two-factor-challenge | Complete 2FA login | No | |
| 203 | + |
| 204 | +## Events |
| 205 | + |
| 206 | +Subscribe to authentication events using `@nestjs/event-emitter`: |
| 207 | + |
| 208 | +```typescript |
| 209 | +import { OnEvent } from "@nestjs/event-emitter"; |
| 210 | +import { AUTH_EVENTS, UserEvent } from "@nestbolt/authentication"; |
| 211 | + |
| 212 | +@Injectable() |
| 213 | +export class AuthListener { |
| 214 | + @OnEvent(AUTH_EVENTS.LOGIN) |
| 215 | + handleLogin(payload: UserEvent) { |
| 216 | + console.log(`User ${payload.user.email} logged in`); |
| 217 | + } |
| 218 | +} |
| 219 | +``` |
| 220 | + |
| 221 | +Available events: `auth.login`, `auth.logout`, `auth.registered`, `auth.lockout`, `auth.password-reset`, `auth.password-updated`, `auth.email-verified`, `auth.two-factor-enabled`, `auth.two-factor-disabled`, `auth.two-factor-confirmed`, `auth.two-factor-challenged`, `auth.two-factor-failed`, `auth.valid-two-factor-code`, `auth.recovery-code-replaced`, `auth.recovery-codes-generated` |
| 222 | + |
| 223 | +## Configuration Options |
| 224 | + |
| 225 | +| Option | Type | Default | Description | |
| 226 | +|--------|------|---------|-------------| |
| 227 | +| `features` | `Feature[]` | *required* | Enabled features | |
| 228 | +| `userRepository` | `Type<UserRepository>` | *required* | User repository class | |
| 229 | +| `passwordResetRepository` | `Type<PasswordResetRepository>` | - | Password reset token storage | |
| 230 | +| `jwtSecret` | `string` | *required* | JWT signing secret | |
| 231 | +| `refreshSecret` | `string` | *required* | Refresh token secret | |
| 232 | +| `encryptionKey` | `string` | *required* | 32-byte base64 key for 2FA encryption | |
| 233 | +| `jwtExpiresIn` | `string` | `"15m"` | Access token TTL | |
| 234 | +| `refreshExpiresIn` | `string` | `"7d"` | Refresh token TTL | |
| 235 | +| `usernameField` | `string` | `"email"` | Login username field | |
| 236 | +| `lowercaseUsernames` | `boolean` | `true` | Lowercase usernames on login | |
| 237 | +| `loginRateLimit` | `{ ttl, limit }` | `{ 60000, 5 }` | Login rate limiting | |
| 238 | +| `passwordTimeout` | `number` | `900` | Password confirmation timeout (seconds) | |
| 239 | +| `appName` | `string` | `"NestBolt"` | App name for TOTP QR codes | |
| 240 | +| `twoFactorOptions.confirm` | `boolean` | `false` | Require 2FA confirmation step | |
| 241 | +| `twoFactorOptions.confirmPassword` | `boolean` | `false` | Require password before 2FA changes | |
| 242 | +| `twoFactorOptions.window` | `number` | `1` | TOTP time window | |
| 243 | +| `twoFactorOptions.secretLength` | `number` | `20` | TOTP secret key length | |
| 244 | + |
| 245 | +## Testing |
| 246 | + |
| 247 | +```bash |
| 248 | +pnpm test # Run tests |
| 249 | +pnpm test:watch # Watch mode |
| 250 | +pnpm test:cov # Coverage report |
| 251 | +``` |
| 252 | + |
| 253 | +## Changelog |
| 254 | + |
| 255 | +See [CHANGELOG.md](CHANGELOG.md). |
| 256 | + |
| 257 | +## Contributing |
| 258 | + |
| 259 | +See [CONTRIBUTING.md](CONTRIBUTING.md). |
| 260 | + |
| 261 | +## Security |
| 262 | + |
| 263 | +For security-related issues, please use the **security** label on [GitHub Issues](https://github.com/nestbolt/authentication/issues). |
| 264 | + |
| 265 | +## Credits |
| 266 | + |
| 267 | +- Inspired by [Laravel Fortify](https://github.com/laravel/fortify) by Taylor Otwell |
| 268 | + |
| 269 | +## License |
| 270 | + |
| 271 | +[MIT License](LICENSE.md) |
0 commit comments