Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,10 @@ SUPERADMIN_PASSWORD=SuperAdmin123!
SUPERADMIN_FULL_NAME=Super Admin

# Request logging (set to false to disable)
ENABLE_REQUEST_LOGGING=true
ENABLE_REQUEST_LOGGING=true

# Passkey (WebAuthn)
PASSKEY_RP_NAME=My App
PASSKEY_RP_ID=localhost
PASSKEY_ORIGIN=http://localhost:3000
PASSKEY_ANDROID_KEY_HASHES=
58 changes: 57 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Claude Code Documentation

NestJS backend template with authentication, user management, and layered architecture.
NestJS backend template with authentication (JWT + Passkey/WebAuthn), user management, and layered architecture.

## Quick Commands

Expand Down Expand Up @@ -275,6 +275,62 @@ export class GetEntitiesQueryDto extends PaginationQueryDto {

`Role.superadmin` | `Role.admin` | `Role.user`

## Passkey (WebAuthn) Authentication

Passkey support via `@simplewebauthn/server`. Users register passkeys after signing in with password, then can authenticate using passkeys.

### Passkey Endpoints

| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | `/auth/passkey/auth/options` | Public | Generate authentication options |
| POST | `/auth/passkey/auth/verify` | Public | Verify passkey authentication (returns tokens) |
| POST | `/auth/passkey/register/options` | JWT | Generate registration options |
| POST | `/auth/passkey/register/verify` | JWT | Verify registration (stores passkey) |
| GET | `/auth/passkeys` | JWT | List current user's passkeys |
| DELETE | `/auth/passkeys/:id` | JWT | Delete a passkey |

### Passkey Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `PASSKEY_RP_NAME` | Relying party display name | My App |
| `PASSKEY_RP_ID` | Relying party ID (domain) | localhost |
| `PASSKEY_ORIGIN` | Expected origin URL | http://localhost:3000 |

### Passkey Database Models

```prisma
model Passkey {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
credentialId String @unique @map("credential_id")
publicKey String @map("public_key")
counter Int @default(0)
transports String[] @default([])
deviceType String? @map("device_type")
backedUp Boolean @default(false) @map("backed_up")
name String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

user User @relation(fields: [userId], references: [id])
@@map("passkeys")
}

model PasskeyChallenge {
id String @id @default(uuid()) @db.Uuid
userId String? @map("user_id") @db.Uuid
challenge String
type String
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")

user User? @relation(fields: [userId], references: [id])
@@map("passkey_challenges")
}
```

## Module Generation

```bash
Expand Down
80 changes: 68 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# NestJS Backend Template

Production-ready NestJS backend template with authentication, user management, and layered architecture.
Production-ready NestJS backend template with authentication (JWT + Passkey/WebAuthn), user management, and layered architecture.

## Quick Start

Expand Down Expand Up @@ -33,6 +33,7 @@ npm run start:dev
| **Helmet.js** | HTTP security headers (CSP, XSS protection, etc.) |
| **Rate Limiting** | Global: 100 req/min, Auth endpoints: 3-15 req/min |
| **JWT Authentication** | Access tokens (15min) + Refresh tokens (7d) |
| **Passkey (WebAuthn)** | Passwordless authentication via @simplewebauthn |
| **Password Hashing** | Bcrypt with salt rounds |
| **CORS** | Configurable whitelist |
| **Soft Deletes** | Data preservation for audit trails |
Expand All @@ -50,6 +51,7 @@ npm run start:dev
| `/auth/refresh` | 30/min | Token refresh |
| `/auth/reset-password` | 5/min | Reset attempts |
| `/auth/verify-email` | 5/min | Verification attempts |
| `/auth/passkey/auth/*` | 10/min | Passkey authentication |

### Observability

Expand Down Expand Up @@ -476,16 +478,22 @@ Each feature module follows this layered architecture:

### Auth Module (`/api/auth`)

| Method | Endpoint | Auth | Rate Limit | Description |
| ------ | -------------------- | ------ | ---------- | ------------------------- |
| POST | /signup | Public | 5/min | Register new user |
| POST | /signin | Public | 15/min | Login with credentials |
| POST | /logout | JWT | - | Logout current session |
| POST | /refresh | Public | 30/min | Refresh access token |
| POST | /forgot-password | Public | 3/min | Request password reset |
| POST | /reset-password | Public | 5/min | Reset password with token |
| POST | /verify-email | Public | 5/min | Verify email with token |
| POST | /resend-verification | Public | 3/min | Resend verification email |
| Method | Endpoint | Auth | Rate Limit | Description |
| ------ | ------------------------- | ------ | ---------- | ---------------------------- |
| POST | /signup | Public | 5/min | Register new user |
| POST | /signin | Public | 15/min | Login with credentials |
| POST | /logout | JWT | - | Logout current session |
| POST | /refresh | Public | 30/min | Refresh access token |
| POST | /forgot-password | Public | 3/min | Request password reset |
| POST | /reset-password | Public | 5/min | Reset password with token |
| POST | /verify-email | Public | 5/min | Verify email with token |
| POST | /resend-verification | Public | 3/min | Resend verification email |
| POST | /passkey/auth/options | Public | 10/min | Generate passkey auth options |
| POST | /passkey/auth/verify | Public | 10/min | Verify passkey authentication |
| POST | /passkey/register/options | JWT | - | Generate registration options |
| POST | /passkey/register/verify | JWT | - | Verify passkey registration |
| GET | /passkeys | JWT | - | List user's passkeys |
| DELETE | /passkeys/:id | JWT | - | Delete a passkey |

### Users Module (`/api/users`)

Expand Down Expand Up @@ -516,15 +524,60 @@ Each feature module follows this layered architecture:
| GET | /live | Public | Liveness probe |
| GET | /ready | Public | Readiness probe |

### Passkey Model

| Field | Type | Description |
| ------------ | -------- | -------------------------------- |
| id | UUID | Primary key |
| userId | UUID | Foreign key to User |
| credentialId | String | Unique WebAuthn credential ID |
| publicKey | String | Base64-encoded public key |
| counter | Int | Signature counter |
| transports | String[] | Authenticator transports |
| deviceType | String? | Device type (singleDevice/multi) |
| backedUp | Boolean | Whether credential is backed up |
| name | String? | User-given name for the passkey |
| createdAt | DateTime | Creation timestamp |
| updatedAt | DateTime | Last update timestamp |

### PasskeyChallenge Model

| Field | Type | Description |
| --------- | --------- | --------------------------------------- |
| id | UUID | Primary key |
| userId | UUID? | Optional foreign key to User |
| challenge | String | WebAuthn challenge string |
| type | String | 'registration' or 'authentication' |
| expiresAt | DateTime | Challenge expiry (5 minutes) |
| createdAt | DateTime | Creation timestamp |

## Authentication Flow

### JWT (Password-based)

1. JWT tokens stored in Authorization header as Bearer token
2. Access token expires in 15 minutes (configurable)
3. Refresh token expires in 7 days (configurable)
4. Refresh tokens are bcrypt hashed before storage
5. Logout invalidates session by nulling refresh token
6. JwtAuthGuard is applied globally, use @Public() for public routes

### Passkey (WebAuthn)

Passkeys provide passwordless authentication using biometrics, security keys, or platform authenticators.

**Registration flow (authenticated user):**
1. Client requests registration options from `POST /auth/passkey/register/options`
2. Client calls `navigator.credentials.create()` with the options
3. Client sends credential response to `POST /auth/passkey/register/verify`
4. Server verifies and stores the passkey

**Authentication flow (public):**
1. Client requests authentication options from `POST /auth/passkey/auth/options` (optional email for non-discoverable credentials)
2. Client calls `navigator.credentials.get()` with the options
3. Client sends credential response to `POST /auth/passkey/auth/verify`
4. Server verifies the credential, identifies the user, and returns JWT tokens

## Adding New Modules

1. Create module directory: `src/modules/{module-name}/`
Expand Down Expand Up @@ -662,6 +715,9 @@ getRawData() {}
| SUPERADMIN_PASSWORD | Initial superadmin password | - | No |
| SUPERADMIN_FULL_NAME | Initial superadmin name | - | No |
| ENABLE_REQUEST_LOGGING | Enable HTTP request logging | true | No |
| PASSKEY_RP_NAME | Passkey relying party display name | My App | No |
| PASSKEY_RP_ID | Passkey relying party ID (domain) | localhost | No |
| PASSKEY_ORIGIN | Passkey expected origin URL | http://localhost:3000 | No |

## Path Aliases

Expand Down Expand Up @@ -823,7 +879,7 @@ npm run prisma:studio # Open Prisma Studio
- NestJS 11.x
- PostgreSQL 16 + Prisma 6.x
- Redis 7 (ioredis)
- Passport.js + JWT
- Passport.js + JWT + @simplewebauthn/server (Passkey/WebAuthn)
- class-validator + class-transformer
- Swagger/OpenAPI
- Pino logger
Expand Down
Loading