@techelusive/nest-auth 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/LICENSE +21 -0
- package/README.md +204 -2
- package/api/openapi.json +4399 -0
- package/api/techelusive-authentication.postman_collection.json +3178 -0
- package/dist/auth-options.d.ts +156 -0
- package/dist/auth-options.js +3 -0
- package/dist/auth-options.js.map +1 -0
- package/dist/auth-options.validation.d.ts +9 -0
- package/dist/auth-options.validation.js +74 -0
- package/dist/auth-options.validation.js.map +1 -0
- package/dist/auth.module.d.ts +38 -0
- package/dist/auth.module.js +173 -0
- package/dist/auth.module.js.map +1 -0
- package/dist/common/auth-error.d.ts +16 -0
- package/dist/common/auth-error.js +23 -0
- package/dist/common/auth-error.js.map +1 -0
- package/dist/common/auth-exception.filter.d.ts +24 -0
- package/dist/common/auth-exception.filter.js +66 -0
- package/dist/common/auth-exception.filter.js.map +1 -0
- package/dist/common/crypto/jwt.d.ts +89 -0
- package/dist/common/crypto/jwt.js +144 -0
- package/dist/common/crypto/jwt.js.map +1 -0
- package/dist/common/crypto/random-secrets.d.ts +78 -0
- package/dist/common/crypto/random-secrets.js +113 -0
- package/dist/common/crypto/random-secrets.js.map +1 -0
- package/dist/common/crypto/remote-public-keys.d.ts +49 -0
- package/dist/common/crypto/remote-public-keys.js +105 -0
- package/dist/common/crypto/remote-public-keys.js.map +1 -0
- package/dist/common/crypto/totp.d.ts +68 -0
- package/dist/common/crypto/totp.js +148 -0
- package/dist/common/crypto/totp.js.map +1 -0
- package/dist/common/encryption.service.d.ts +36 -0
- package/dist/common/encryption.service.js +111 -0
- package/dist/common/encryption.service.js.map +1 -0
- package/dist/common/identifiers.d.ts +48 -0
- package/dist/common/identifiers.js +89 -0
- package/dist/common/identifiers.js.map +1 -0
- package/dist/common/network-safety.d.ts +19 -0
- package/dist/common/network-safety.js +72 -0
- package/dist/common/network-safety.js.map +1 -0
- package/dist/common/rate-limiter.service.d.ts +63 -0
- package/dist/common/rate-limiter.service.js +158 -0
- package/dist/common/rate-limiter.service.js.map +1 -0
- package/dist/common/refresh-token-cookie.d.ts +78 -0
- package/dist/common/refresh-token-cookie.js +185 -0
- package/dist/common/refresh-token-cookie.js.map +1 -0
- package/dist/common/request-context.d.ts +14 -0
- package/dist/common/request-context.js +22 -0
- package/dist/common/request-context.js.map +1 -0
- package/dist/common/strict-validation.pipe.d.ts +8 -0
- package/dist/common/strict-validation.pipe.js +16 -0
- package/dist/common/strict-validation.pipe.js.map +1 -0
- package/dist/common/time.d.ts +15 -0
- package/dist/common/time.js +34 -0
- package/dist/common/time.js.map +1 -0
- package/dist/constants.d.ts +1326 -0
- package/dist/constants.js +1142 -0
- package/dist/constants.js.map +1 -0
- package/dist/database/create-auth-tables.migration.d.ts +26 -0
- package/dist/database/create-auth-tables.migration.js +291 -0
- package/dist/database/create-auth-tables.migration.js.map +1 -0
- package/dist/database/email-change-and-retention.migration.d.ts +25 -0
- package/dist/database/email-change-and-retention.migration.js +54 -0
- package/dist/database/email-change-and-retention.migration.js.map +1 -0
- package/dist/database/entities/api-key.entity.d.ts +17 -0
- package/dist/database/entities/api-key.entity.js +80 -0
- package/dist/database/entities/api-key.entity.js.map +1 -0
- package/dist/database/entities/audit-log.entity.d.ts +13 -0
- package/dist/database/entities/audit-log.entity.js +64 -0
- package/dist/database/entities/audit-log.entity.js.map +1 -0
- package/dist/database/entities/backup-code.entity.d.ts +9 -0
- package/dist/database/entities/backup-code.entity.js +52 -0
- package/dist/database/entities/backup-code.entity.js.map +1 -0
- package/dist/database/entities/index.d.ts +43 -0
- package/dist/database/entities/index.js +82 -0
- package/dist/database/entities/index.js.map +1 -0
- package/dist/database/entities/json-object.d.ts +12 -0
- package/dist/database/entities/json-object.js +3 -0
- package/dist/database/entities/json-object.js.map +1 -0
- package/dist/database/entities/link-token.entity.d.ts +15 -0
- package/dist/database/entities/link-token.entity.js +64 -0
- package/dist/database/entities/link-token.entity.js.map +1 -0
- package/dist/database/entities/one-time-code.entity.d.ts +16 -0
- package/dist/database/entities/one-time-code.entity.js +69 -0
- package/dist/database/entities/one-time-code.entity.js.map +1 -0
- package/dist/database/entities/organization-invitation.entity.d.ts +14 -0
- package/dist/database/entities/organization-invitation.entity.js +73 -0
- package/dist/database/entities/organization-invitation.entity.js.map +1 -0
- package/dist/database/entities/organization-member.entity.d.ts +9 -0
- package/dist/database/entities/organization-member.entity.js +48 -0
- package/dist/database/entities/organization-member.entity.js.map +1 -0
- package/dist/database/entities/organization.entity.d.ts +12 -0
- package/dist/database/entities/organization.entity.js +58 -0
- package/dist/database/entities/organization.entity.js.map +1 -0
- package/dist/database/entities/passkey.entity.d.ts +19 -0
- package/dist/database/entities/passkey.entity.js +78 -0
- package/dist/database/entities/passkey.entity.js.map +1 -0
- package/dist/database/entities/password.entity.d.ts +6 -0
- package/dist/database/entities/password.entity.js +36 -0
- package/dist/database/entities/password.entity.js.map +1 -0
- package/dist/database/entities/refresh-token.entity.d.ts +9 -0
- package/dist/database/entities/refresh-token.entity.js +53 -0
- package/dist/database/entities/refresh-token.entity.js.map +1 -0
- package/dist/database/entities/session.entity.d.ts +20 -0
- package/dist/database/entities/session.entity.js +87 -0
- package/dist/database/entities/session.entity.js.map +1 -0
- package/dist/database/entities/settings.entity.d.ts +7 -0
- package/dist/database/entities/settings.entity.js +41 -0
- package/dist/database/entities/settings.entity.js.map +1 -0
- package/dist/database/entities/sign-in-challenge.entity.d.ts +23 -0
- package/dist/database/entities/sign-in-challenge.entity.js +80 -0
- package/dist/database/entities/sign-in-challenge.entity.js.map +1 -0
- package/dist/database/entities/signing-key.entity.d.ts +11 -0
- package/dist/database/entities/signing-key.entity.js +54 -0
- package/dist/database/entities/signing-key.entity.js.map +1 -0
- package/dist/database/entities/social-account.entity.d.ts +13 -0
- package/dist/database/entities/social-account.entity.js +54 -0
- package/dist/database/entities/social-account.entity.js.map +1 -0
- package/dist/database/entities/two-factor-secret.entity.d.ts +10 -0
- package/dist/database/entities/two-factor-secret.entity.js +48 -0
- package/dist/database/entities/two-factor-secret.entity.js.map +1 -0
- package/dist/database/entities/user.entity.d.ts +34 -0
- package/dist/database/entities/user.entity.js +129 -0
- package/dist/database/entities/user.entity.js.map +1 -0
- package/dist/database/entities/webhook-delivery.entity.d.ts +17 -0
- package/dist/database/entities/webhook-delivery.entity.js +79 -0
- package/dist/database/entities/webhook-delivery.entity.js.map +1 -0
- package/dist/database/entities/webhook.entity.d.ts +12 -0
- package/dist/database/entities/webhook.entity.js +58 -0
- package/dist/database/entities/webhook.entity.js.map +1 -0
- package/dist/database/typeorm-row.d.ts +21 -0
- package/dist/database/typeorm-row.js +29 -0
- package/dist/database/typeorm-row.js.map +1 -0
- package/dist/features/account/account.controller.d.ts +259 -0
- package/dist/features/account/account.controller.js +372 -0
- package/dist/features/account/account.controller.js.map +1 -0
- package/dist/features/account/account.dto.d.ts +16 -0
- package/dist/features/account/account.dto.js +61 -0
- package/dist/features/account/account.dto.js.map +1 -0
- package/dist/features/account/data-export.service.d.ts +85 -0
- package/dist/features/account/data-export.service.js +125 -0
- package/dist/features/account/data-export.service.js.map +1 -0
- package/dist/features/account/health.service.d.ts +30 -0
- package/dist/features/account/health.service.js +59 -0
- package/dist/features/account/health.service.js.map +1 -0
- package/dist/features/admin/admin.controller.d.ts +382 -0
- package/dist/features/admin/admin.controller.js +623 -0
- package/dist/features/admin/admin.controller.js.map +1 -0
- package/dist/features/admin/admin.dto.d.ts +61 -0
- package/dist/features/admin/admin.dto.js +253 -0
- package/dist/features/admin/admin.dto.js.map +1 -0
- package/dist/features/admin/admin.service.d.ts +142 -0
- package/dist/features/admin/admin.service.js +227 -0
- package/dist/features/admin/admin.service.js.map +1 -0
- package/dist/features/api-keys/api-keys.service.d.ts +67 -0
- package/dist/features/api-keys/api-keys.service.js +144 -0
- package/dist/features/api-keys/api-keys.service.js.map +1 -0
- package/dist/features/email-change/email-change.controller.d.ts +38 -0
- package/dist/features/email-change/email-change.controller.js +85 -0
- package/dist/features/email-change/email-change.controller.js.map +1 -0
- package/dist/features/email-change/email-change.dto.d.ts +10 -0
- package/dist/features/email-change/email-change.dto.js +43 -0
- package/dist/features/email-change/email-change.dto.js.map +1 -0
- package/dist/features/email-change/email-change.service.d.ts +74 -0
- package/dist/features/email-change/email-change.service.js +179 -0
- package/dist/features/email-change/email-change.service.js.map +1 -0
- package/dist/features/email-links/email-verification.service.d.ts +46 -0
- package/dist/features/email-links/email-verification.service.js +94 -0
- package/dist/features/email-links/email-verification.service.js.map +1 -0
- package/dist/features/email-links/link-tokens.service.d.ts +44 -0
- package/dist/features/email-links/link-tokens.service.js +122 -0
- package/dist/features/email-links/link-tokens.service.js.map +1 -0
- package/dist/features/events/auth-events.service.d.ts +52 -0
- package/dist/features/events/auth-events.service.js +117 -0
- package/dist/features/events/auth-events.service.js.map +1 -0
- package/dist/features/maintenance/data-retention.service.d.ts +60 -0
- package/dist/features/maintenance/data-retention.service.js +182 -0
- package/dist/features/maintenance/data-retention.service.js.map +1 -0
- package/dist/features/notifications/notifications.service.d.ts +40 -0
- package/dist/features/notifications/notifications.service.js +94 -0
- package/dist/features/notifications/notifications.service.js.map +1 -0
- package/dist/features/one-time-codes/one-time-codes.service.d.ts +40 -0
- package/dist/features/one-time-codes/one-time-codes.service.js +102 -0
- package/dist/features/one-time-codes/one-time-codes.service.js.map +1 -0
- package/dist/features/organizations/organizations.controller.d.ts +220 -0
- package/dist/features/organizations/organizations.controller.js +337 -0
- package/dist/features/organizations/organizations.controller.js.map +1 -0
- package/dist/features/organizations/organizations.dto.d.ts +31 -0
- package/dist/features/organizations/organizations.dto.js +100 -0
- package/dist/features/organizations/organizations.dto.js.map +1 -0
- package/dist/features/organizations/organizations.service.d.ts +274 -0
- package/dist/features/organizations/organizations.service.js +481 -0
- package/dist/features/organizations/organizations.service.js.map +1 -0
- package/dist/features/passkeys/passkeys.controller.d.ts +92 -0
- package/dist/features/passkeys/passkeys.controller.js +183 -0
- package/dist/features/passkeys/passkeys.controller.js.map +1 -0
- package/dist/features/passkeys/passkeys.dto.d.ts +13 -0
- package/dist/features/passkeys/passkeys.dto.js +50 -0
- package/dist/features/passkeys/passkeys.dto.js.map +1 -0
- package/dist/features/passkeys/passkeys.service.d.ts +159 -0
- package/dist/features/passkeys/passkeys.service.js +350 -0
- package/dist/features/passkeys/passkeys.service.js.map +1 -0
- package/dist/features/passwordless/passwordless-auth.service.d.ts +152 -0
- package/dist/features/passwordless/passwordless-auth.service.js +304 -0
- package/dist/features/passwordless/passwordless-auth.service.js.map +1 -0
- package/dist/features/passwordless/passwordless.controller.d.ts +75 -0
- package/dist/features/passwordless/passwordless.controller.js +163 -0
- package/dist/features/passwordless/passwordless.controller.js.map +1 -0
- package/dist/features/passwordless/passwordless.dto.d.ts +29 -0
- package/dist/features/passwordless/passwordless.dto.js +101 -0
- package/dist/features/passwordless/passwordless.dto.js.map +1 -0
- package/dist/features/passwords/password-auth.service.d.ts +154 -0
- package/dist/features/passwords/password-auth.service.js +317 -0
- package/dist/features/passwords/password-auth.service.js.map +1 -0
- package/dist/features/passwords/password-policy.service.d.ts +63 -0
- package/dist/features/passwords/password-policy.service.js +175 -0
- package/dist/features/passwords/password-policy.service.js.map +1 -0
- package/dist/features/passwords/password.controller.d.ts +95 -0
- package/dist/features/passwords/password.controller.js +193 -0
- package/dist/features/passwords/password.controller.js.map +1 -0
- package/dist/features/passwords/password.dto.d.ts +36 -0
- package/dist/features/passwords/password.dto.js +140 -0
- package/dist/features/passwords/password.dto.js.map +1 -0
- package/dist/features/sessions/session.service.d.ts +132 -0
- package/dist/features/sessions/session.service.js +308 -0
- package/dist/features/sessions/session.service.js.map +1 -0
- package/dist/features/sessions/session.types.d.ts +34 -0
- package/dist/features/sessions/session.types.js +3 -0
- package/dist/features/sessions/session.types.js.map +1 -0
- package/dist/features/settings/settings.defaults.d.ts +2 -0
- package/dist/features/settings/settings.defaults.js +7 -0
- package/dist/features/settings/settings.defaults.js.map +1 -0
- package/dist/features/settings/settings.service.d.ts +44 -0
- package/dist/features/settings/settings.service.js +112 -0
- package/dist/features/settings/settings.service.js.map +1 -0
- package/dist/features/settings/settings.types.d.ts +133 -0
- package/dist/features/settings/settings.types.js +3 -0
- package/dist/features/settings/settings.types.js.map +1 -0
- package/dist/features/settings/settings.validation.d.ts +33 -0
- package/dist/features/settings/settings.validation.js +614 -0
- package/dist/features/settings/settings.validation.js.map +1 -0
- package/dist/features/sign-in/sign-in-challenges.service.d.ts +54 -0
- package/dist/features/sign-in/sign-in-challenges.service.js +104 -0
- package/dist/features/sign-in/sign-in-challenges.service.js.map +1 -0
- package/dist/features/sign-in/sign-in.service.d.ts +127 -0
- package/dist/features/sign-in/sign-in.service.js +248 -0
- package/dist/features/sign-in/sign-in.service.js.map +1 -0
- package/dist/features/sign-in/sign-in.types.d.ts +22 -0
- package/dist/features/sign-in/sign-in.types.js +3 -0
- package/dist/features/sign-in/sign-in.types.js.map +1 -0
- package/dist/features/signing-keys/jwks.controller.d.ts +17 -0
- package/dist/features/signing-keys/jwks.controller.js +51 -0
- package/dist/features/signing-keys/jwks.controller.js.map +1 -0
- package/dist/features/signing-keys/signing-keys.service.d.ts +67 -0
- package/dist/features/signing-keys/signing-keys.service.js +142 -0
- package/dist/features/signing-keys/signing-keys.service.js.map +1 -0
- package/dist/features/social/social-auth.service.d.ts +100 -0
- package/dist/features/social/social-auth.service.js +232 -0
- package/dist/features/social/social-auth.service.js.map +1 -0
- package/dist/features/social/social-providers.service.d.ts +97 -0
- package/dist/features/social/social-providers.service.js +214 -0
- package/dist/features/social/social-providers.service.js.map +1 -0
- package/dist/features/social/social.controller.d.ts +54 -0
- package/dist/features/social/social.controller.js +124 -0
- package/dist/features/social/social.controller.js.map +1 -0
- package/dist/features/social/social.dto.d.ts +11 -0
- package/dist/features/social/social.dto.js +38 -0
- package/dist/features/social/social.dto.js.map +1 -0
- package/dist/features/two-factor/two-factor.controller.d.ts +84 -0
- package/dist/features/two-factor/two-factor.controller.js +174 -0
- package/dist/features/two-factor/two-factor.controller.js.map +1 -0
- package/dist/features/two-factor/two-factor.dto.d.ts +10 -0
- package/dist/features/two-factor/two-factor.dto.js +37 -0
- package/dist/features/two-factor/two-factor.dto.js.map +1 -0
- package/dist/features/two-factor/two-factor.service.d.ts +171 -0
- package/dist/features/two-factor/two-factor.service.js +348 -0
- package/dist/features/two-factor/two-factor.service.js.map +1 -0
- package/dist/features/users/sign-in-methods.service.d.ts +40 -0
- package/dist/features/users/sign-in-methods.service.js +81 -0
- package/dist/features/users/sign-in-methods.service.js.map +1 -0
- package/dist/features/users/sign-up-rules.d.ts +12 -0
- package/dist/features/users/sign-up-rules.js +29 -0
- package/dist/features/users/sign-up-rules.js.map +1 -0
- package/dist/features/users/users.service.d.ts +246 -0
- package/dist/features/users/users.service.js +509 -0
- package/dist/features/users/users.service.js.map +1 -0
- package/dist/features/users/users.types.d.ts +24 -0
- package/dist/features/users/users.types.js +3 -0
- package/dist/features/users/users.types.js.map +1 -0
- package/dist/features/webhooks/webhook-delivery.worker.d.ts +55 -0
- package/dist/features/webhooks/webhook-delivery.worker.js +175 -0
- package/dist/features/webhooks/webhook-delivery.worker.js.map +1 -0
- package/dist/features/webhooks/webhook-signature.d.ts +28 -0
- package/dist/features/webhooks/webhook-signature.js +85 -0
- package/dist/features/webhooks/webhook-signature.js.map +1 -0
- package/dist/features/webhooks/webhooks.service.d.ts +144 -0
- package/dist/features/webhooks/webhooks.service.js +238 -0
- package/dist/features/webhooks/webhooks.service.js.map +1 -0
- package/dist/guards/auth.decorators.d.ts +65 -0
- package/dist/guards/auth.decorators.js +68 -0
- package/dist/guards/auth.decorators.js.map +1 -0
- package/dist/guards/auth.guard.d.ts +47 -0
- package/dist/guards/auth.guard.js +132 -0
- package/dist/guards/auth.guard.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/console-email-sender.d.ts +15 -0
- package/dist/integrations/console-email-sender.js +26 -0
- package/dist/integrations/console-email-sender.js.map +1 -0
- package/dist/integrations/msg91-sms-sender.d.ts +28 -0
- package/dist/integrations/msg91-sms-sender.js +46 -0
- package/dist/integrations/msg91-sms-sender.js.map +1 -0
- package/docs/README.md +39 -0
- package/docs/admin-api-keys-webhooks.md +147 -0
- package/docs/api-reference.md +175 -0
- package/docs/architecture.md +119 -0
- package/docs/client-integration.md +192 -0
- package/docs/code-walkthrough.md +273 -0
- package/docs/configuration.md +229 -0
- package/docs/endpoints/README.md +151 -0
- package/docs/endpoints/account.md +363 -0
- package/docs/endpoints/admin.md +422 -0
- package/docs/endpoints/email-password.md +261 -0
- package/docs/endpoints/organizations.md +192 -0
- package/docs/endpoints/passkeys.md +195 -0
- package/docs/endpoints/passwordless.md +214 -0
- package/docs/endpoints/social.md +116 -0
- package/docs/endpoints/two-factor.md +146 -0
- package/docs/error-codes.md +100 -0
- package/docs/events.md +83 -0
- package/docs/getting-started.md +139 -0
- package/docs/manual-testing.md +215 -0
- package/docs/organizations.md +86 -0
- package/docs/production-checklist.md +60 -0
- package/docs/protecting-routes.md +147 -0
- package/docs/security.md +61 -0
- package/docs/sign-in-flows.md +294 -0
- package/package.json +92 -4
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Manual testing (before publishing)
|
|
2
|
+
|
|
3
|
+
The **playground** runs the library on your machine with every feature switched on. Emails and SMS never leave your computer: they land in a dev inbox you can see. Three ways to drive it:
|
|
4
|
+
|
|
5
|
+
| Tool | Best for | Where |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| **Dev dashboard** | Watching what happens: emails, SMS codes, live auth events, received webhooks. Also passkeys and Google sign-in, which need a browser | http://localhost:3000/dev |
|
|
8
|
+
| **Postman** | Running every flow step by step: tokens, codes and links are captured automatically | import `api/techelusive-authentication.postman_collection.json` |
|
|
9
|
+
| **Swagger UI** | Browsing and trying every endpoint in the browser | http://localhost:3000/docs |
|
|
10
|
+
|
|
11
|
+
## 1. Start the playground
|
|
12
|
+
|
|
13
|
+
You need Node.js 22+ and Docker Desktop.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd techelusive-authentication
|
|
17
|
+
npm install # once
|
|
18
|
+
npm run playground:db # starts PostgreSQL (and Redis) in Docker
|
|
19
|
+
npm run playground # builds and starts the API on http://localhost:3000
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
You should see:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
[Playground] Dev dashboard: http://localhost:3000/dev
|
|
26
|
+
[Playground] Swagger UI: http://localhost:3000/docs
|
|
27
|
+
[Playground] Postman: import api/techelusive-authentication.postman_collection.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The database and its tables are created automatically on start. If a PostgreSQL already installed on your Mac (Homebrew, Postgres.app) is using port 5432, the playground uses that one instead of Docker's, which is fine. To use Docker's anyway, stop the local one first (`brew services stop postgresql@16`).
|
|
31
|
+
|
|
32
|
+
To start from an empty database:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
docker compose -f playground/docker-compose.yml down -v && npm run playground:db
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Optional settings: copy `playground/.env.example` to `playground/.env` and uncomment what you need (or set them in the shell). Restart the playground after changing it.
|
|
39
|
+
|
|
40
|
+
| Variable | Effect |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `GOOGLE_CLIENT_ID` | Turns on Google sign-in and shows the Google button on the dashboard. Create a "Web application" OAuth client in Google Cloud Console with `http://localhost:3000` as an authorized JavaScript origin |
|
|
43
|
+
| `APPLE_CLIENT_ID`, `FACEBOOK_APP_ID` + `FACEBOOK_APP_SECRET` | Turn on Apple / Facebook (tokens must then be pasted into Postman's `socialToken`) |
|
|
44
|
+
| `CHECK_BREACHED_PASSWORDS=true` | Turns on the data-breach password check, which is off in the playground so any password meeting the rules works. Apps using the library have it on by default |
|
|
45
|
+
| `REDIS_URL=redis://localhost:6379` | Rate limits counted in Redis instead of memory |
|
|
46
|
+
| `DATABASE_URL` | A different database (default `postgres://postgres:postgres@localhost:5432/techelusive_playground`) |
|
|
47
|
+
|
|
48
|
+
### Real emails, real SMS and Google sign-in (optional)
|
|
49
|
+
|
|
50
|
+
Every message always appears on the dashboard. To also receive them for real, add these to `playground/.env` and restart:
|
|
51
|
+
|
|
52
|
+
**Email (Resend, free):** sign up at resend.com → API Keys → create one → `RESEND_API_KEY=re_...`. Until you verify your own domain, Resend only delivers to the email address you signed up with, so test with that address. Links in the emails open the dashboard and show the token to paste into Postman.
|
|
53
|
+
|
|
54
|
+
**SMS (MSG91):** `MSG91_AUTH_KEY`, `MSG91_TEMPLATE_ID` and `MSG91_CODE_VARIABLE` (the variable in your template, e.g. `otp`). Indian numbers need a DLT-approved template, so SMS is usually the last thing to set up; until then, use the codes on the dashboard.
|
|
55
|
+
|
|
56
|
+
If a real send fails, the terminal shows a warning and the flow carries on: the code is still on the dashboard.
|
|
57
|
+
|
|
58
|
+
**Google with the OAuth 2.0 Playground** (developers.google.com/oauthplayground):
|
|
59
|
+
|
|
60
|
+
1. Google Cloud Console → APIs & Services → Credentials → Create OAuth client ID → **Web application**.
|
|
61
|
+
- Authorized JavaScript origins: `http://localhost:3000` (for the dashboard button)
|
|
62
|
+
- Authorized redirect URIs: `https://developers.google.com/oauthplayground`
|
|
63
|
+
2. Put the client ID in `playground/.env` as `GOOGLE_CLIENT_ID=...` and restart the playground.
|
|
64
|
+
3. In the OAuth Playground, click the gear icon → tick **Use your own OAuth credentials** → paste your client ID and secret. Without this the token is issued for Google's own client and the library rejects it (`INVALID_SOCIAL_TOKEN`), which is correct.
|
|
65
|
+
4. Step 1: in "Input your own scopes" enter `openid email profile` → **Authorize APIs** → sign in.
|
|
66
|
+
5. Step 2: **Exchange authorization code for tokens** → copy the `id_token` value (not the access token).
|
|
67
|
+
6. Postman → folder 5 → **Sign in with provider**, with `provider` = `google` (`POST /auth/social/google`): set `socialToken` to the id_token and send. A new user is created and signed in, or an existing user with the same verified email is linked.
|
|
68
|
+
|
|
69
|
+
ID tokens expire after an hour; repeat step 5 for a fresh one. The **Sign in with Google** button on the dashboard does the same thing in one click.
|
|
70
|
+
|
|
71
|
+
## 2. Use Postman
|
|
72
|
+
|
|
73
|
+
1. Postman → **Import** → choose `api/techelusive-authentication.postman_collection.json`.
|
|
74
|
+
2. Open the collection's **Variables** tab to see `email`, `password`, `secondEmail` and `phoneNumber`. Change them if you like.
|
|
75
|
+
3. Run requests **top to bottom within a folder**. The scripts do the copying for you:
|
|
76
|
+
- sign-in responses save `accessToken`, `refreshToken`, `sessionId`, `userId`, and `challengeToken` when 2FA is on,
|
|
77
|
+
- "(from inbox)" requests read the latest code or link token from the dev inbox before sending,
|
|
78
|
+
- 2FA requests compute the current authenticator code from the saved `totpSecret`,
|
|
79
|
+
- ids (`organizationId`, `webhookId`, `apiKeyId`...) are saved when created.
|
|
80
|
+
4. Open **View → Show Postman Console** to see what was saved and the `code` of any error.
|
|
81
|
+
|
|
82
|
+
Protected requests use the collection's Bearer token `{{accessToken}}`. Public ones are set to "No auth".
|
|
83
|
+
|
|
84
|
+
## 3. Use Swagger
|
|
85
|
+
|
|
86
|
+
Open http://localhost:3000/docs → pick an endpoint → **Try it out** → **Execute**. After any sign-in (Login, verify code, refresh...) Swagger authorizes itself with the returned access token, so protected endpoints work right away. Codes and link tokens are on the dev dashboard.
|
|
87
|
+
|
|
88
|
+
## 4. Test checklist
|
|
89
|
+
|
|
90
|
+
Work through these in order; each line says what you should see. Folder names refer to Postman.
|
|
91
|
+
|
|
92
|
+
### Email + password (folder 1)
|
|
93
|
+
|
|
94
|
+
| # | Do | Expect |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| 1 | Register | `201`, `status: "verification_required"`, `pendingVerifications: ["email"]`; a `verify_email` message in the inbox; event `user.created` |
|
|
97
|
+
| 2 | Register again with the same email | Exactly the same response; no second account (no enumeration) |
|
|
98
|
+
| 3 | Login before verifying | `403 ACCOUNT_NOT_VERIFIED` with `pendingVerifications: ["email"]` |
|
|
99
|
+
| 4 | Login with a wrong password, then with an unknown email | `401 INVALID_CREDENTIALS` both times, same message |
|
|
100
|
+
| 5 | Verify email (token from inbox) | `status: "ACTIVE"`; event `user.email_verified`, `user.activated` |
|
|
101
|
+
| 6 | Verify email again with the same token | `400 INVALID_TOKEN` (links work once) |
|
|
102
|
+
| 7 | Login | `status: "signed_in"`; tokens saved; event `user.signed_in`. On the dashboard, the claims show `roles: ["user"]`, `amr: ["password"]` |
|
|
103
|
+
| 8 | Register with a short password (edit the body: `"password": "short"`) | `400 INVALID_PASSWORD`, `requirements: ["at least 10 characters"]` |
|
|
104
|
+
| 9 | Admin > Change settings `{ "password": { "rejectBreachedPasswords": true } }` (or start with `CHECK_BREACHED_PASSWORDS=true`), then Register with `"password": "password1234"` | `400 INVALID_PASSWORD` with the data-breach message (needs internet) |
|
|
105
|
+
| 10 | Register with an extra field such as `"roles": ["admin"]` | `400 VALIDATION_ERROR`, "property roles should not exist" |
|
|
106
|
+
| 11 | Forgot password, then Reset password (token from inbox) | Success; you are signed out everywhere; `user.password_changed`. Then Login with `{{newPassword}}`, and set `password` = `newPassword` in the variables |
|
|
107
|
+
| 12 | Change password | Success; other devices signed out, this one still works; a `password_changed` notice in the inbox |
|
|
108
|
+
| 13 | Login with a wrong password 11 times | Eventually `429 TOO_MANY_REQUESTS` with `retryAfterSeconds` |
|
|
109
|
+
|
|
110
|
+
### Passwordless (folder 2)
|
|
111
|
+
|
|
112
|
+
| # | Do | Expect |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| 14 | Send email code → Send it again immediately | Second time `429 TOO_MANY_REQUESTS` (`retryAfterSeconds: 60`) |
|
|
115
|
+
| 15 | Verify email code but remove `firstName` from the body | `400 VALIDATION_ERROR` with `missingSignUpDetails`; then send with the names, using the **same** code → signed in (the code was not used up) |
|
|
116
|
+
| 16 | Verify email code with a wrong code | `400 INVALID_OTP` with `attemptsLeft` |
|
|
117
|
+
| 17 | Send magic link → Verify magic link | Signed in; the link fails on a second use |
|
|
118
|
+
| 18 | Send phone code → Verify phone code | The SMS appears in the inbox (`sms_code`); signed in as a new phone-only user |
|
|
119
|
+
| 19 | Send phone code to `+15555550100` after setting `phoneOtp.allowedCountryCodes: ["+91"]` via Admin > Change settings | `400 PHONE_COUNTRY_NOT_ALLOWED` |
|
|
120
|
+
|
|
121
|
+
### Account and sessions (folder 3)
|
|
122
|
+
|
|
123
|
+
| # | Do | Expect |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| 20 | Get my profile, Update my profile | Profile shows the new name and `metadata` |
|
|
126
|
+
| 21 | My devices | Several sessions; the current one has `isCurrentDevice: true` |
|
|
127
|
+
| 22 | Refresh tokens → run it **again** with the old refresh token (Postman saved the new one, so paste the old value) | First succeeds; second `401 INVALID_TOKEN`, the session is ended, and event `session.refresh_token_reused` |
|
|
128
|
+
| 23 | Logout, then Get my profile | `401 INVALID_TOKEN` on `/auth/me` (it checks the session live) |
|
|
129
|
+
| 24 | Deactivate → Login → Reactivate | Login gives `403 ACCOUNT_DEACTIVATED` (reactivationToken saved automatically); Reactivate signs you in |
|
|
130
|
+
| 25 | Delete my account → Login | Login `401 INVALID_CREDENTIALS`; event `user.deleted`. (Do this last, or use a second user) |
|
|
131
|
+
|
|
132
|
+
### Two-factor (folder 4)
|
|
133
|
+
|
|
134
|
+
| # | Do | Expect |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| 26 | Start 2FA setup | `secret` and `authenticatorAppUrl` (scan the URL as a QR code in Google/Microsoft Authenticator to try a real app) |
|
|
137
|
+
| 27 | Enable 2FA | 10 backup codes; other devices signed out |
|
|
138
|
+
| 28 | Login | `status: "two_factor_required"` with `challengeToken` |
|
|
139
|
+
| 29 | Verify 2FA sign-in | `signed_in`; claims show `amr: ["password", "two_factor"]` |
|
|
140
|
+
| 30 | Verify 2FA sign-in again with the same code in the same 30 seconds | `401 INVALID_OTP` (codes can't be replayed) |
|
|
141
|
+
| 31 | Login, then Verify 2FA sign-in with `{{backupCode}}` as the code | Signed in; the same backup code fails next time |
|
|
142
|
+
| 32 | Disable 2FA | Login no longer asks for a code |
|
|
143
|
+
|
|
144
|
+
### Passkeys (dashboard)
|
|
145
|
+
|
|
146
|
+
| # | Do | Expect |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| 33 | Sign in on the dashboard → **Add a passkey** | The browser asks for Touch ID / Windows Hello / phone; "Passkey added"; event `user.passkey_added` |
|
|
149
|
+
| 34 | Logout → **Sign in with a passkey** | Signed in without email or password; no 2FA step even if 2FA is on |
|
|
150
|
+
| 35 | Postman: List my passkeys → Remove passkey | Removed. If it's the only sign-in method left: `409 LAST_SIGN_IN_METHOD` |
|
|
151
|
+
|
|
152
|
+
### Social (dashboard / folder 5)
|
|
153
|
+
|
|
154
|
+
| # | Do | Expect |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| 36 | With `GOOGLE_CLIENT_ID` set: the Google button on the dashboard | Signed in; a new user is created, or an existing user with the same verified email is linked (`user.social_account_linked`) |
|
|
157
|
+
| 37 | Send `{"token": "garbage"}` to Sign in with provider | `401 INVALID_SOCIAL_TOKEN` |
|
|
158
|
+
|
|
159
|
+
### Organizations (folder 7)
|
|
160
|
+
|
|
161
|
+
| # | Do | Expect |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| 38 | Create organization → create it again | Second time `409 ORGANIZATION_SLUG_TAKEN` |
|
|
164
|
+
| 39 | Invite member | An `organization_invitation` message for `{{secondEmail}}` |
|
|
165
|
+
| 40 | Sign in as `{{secondEmail}}` (folder 2, email code) → Accept invitation | Joined as `member` |
|
|
166
|
+
| 41 | Switch active organization | The new access token has `org_id` and `org_role: "member"` (see the claims on the dashboard, or decode at jwt.io) |
|
|
167
|
+
| 42 | As the member: Update organization | `403 FORBIDDEN` |
|
|
168
|
+
| 43 | As the owner (Login as `{{email}}`): Remove member with your own id while you are the only owner | `409 LAST_OWNER` |
|
|
169
|
+
|
|
170
|
+
### Admin, API keys and webhooks (folder 8)
|
|
171
|
+
|
|
172
|
+
| # | Do | Expect |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| 44 | Search users before being admin | `403 FORBIDDEN` |
|
|
175
|
+
| 45 | Folder 10: Make me admin (or the dashboard button) → folder 3: Refresh tokens | Claims now include `admin`; Search users works |
|
|
176
|
+
| 46 | Search users → copy the second user's `id` into the `userId` variable → Suspend user → that user signs in (folder 2) | `403 ACCOUNT_SUSPENDED`; their sessions are ended. Unsuspend user afterwards, and set `userId` back to yours |
|
|
177
|
+
| 47 | Change settings `{ "magicLink": { "enabled": false } }` → Send magic link | `403 SIGN_IN_METHOD_DISABLED`, no restart needed |
|
|
178
|
+
| 48 | Change settings `{ "tokens": { "accessTokenMinutes": 999 } }` | `400 INVALID_SETTINGS` with a `problems` list |
|
|
179
|
+
| 49 | Create webhook (to the playground receiver) → Login | The dashboard's "Webhooks received" shows `user.signed_in` with **signature valid** |
|
|
180
|
+
| 50 | Create webhook with `"url": "https://169.254.169.254/x"` | `400 UNSAFE_WEBHOOK_URL` |
|
|
181
|
+
| 51 | Create API key → folder 10: Call an app route with the API key → Revoke API key → call it again | Key returned once; the call succeeds (`calledBy` shows the key); after revoking `401 INVALID_API_KEY` |
|
|
182
|
+
| 52 | Audit log | Every action above, newest first, with IP and user agent |
|
|
183
|
+
| 53 | Rotate signing key → Get my profile with the old access token | Still works; JWKS now lists two keys |
|
|
184
|
+
| 54 | Stats | Users by status, active sessions, last-24-hours sign-ins and failures |
|
|
185
|
+
| 55 | Run data cleanup now | `removed` counts per step (mostly 0 on a fresh database: rows must be older than the `retention` settings) |
|
|
186
|
+
|
|
187
|
+
### Email change, data export, health (folder 3)
|
|
188
|
+
|
|
189
|
+
| # | Do | Expect |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| 56 | Change my email with a wrong `currentPassword` | `401 INVALID_CREDENTIALS` |
|
|
192
|
+
| 57 | Change my email (to `{{changedEmail}}`) | Same success message whatever the address; a `confirm_email_change` message to the new address |
|
|
193
|
+
| 58 | Change my email to `{{secondEmail}}` (an existing account) | The same success message, but no message arrives (no enumeration) |
|
|
194
|
+
| 59 | Confirm email change (token from inbox) → run it again | First: `user.email` is the new address, verified; the old address gets `email_changed`. Second: `400 INVALID_TOKEN`. Then set the `email` variable to `{{changedEmail}}` |
|
|
195
|
+
| 60 | Sign in with a phone code (folder 2) → Change my email without `currentPassword` | Works right after signing in; more than 10 minutes later `403 RECENT_SIGN_IN_REQUIRED` |
|
|
196
|
+
| 61 | Download my data | Profile, devices, organizations, security history; no hashes, secrets or tokens |
|
|
197
|
+
| 62 | Health check | `200`, `"database": "ok"` (stop PostgreSQL to see `503`) |
|
|
198
|
+
|
|
199
|
+
### Web cookie mode
|
|
200
|
+
|
|
201
|
+
Start with `REFRESH_TOKEN_COOKIE_ORIGINS=http://localhost:3000 npm run playground`, then run `npm run check:cookies` in another terminal. It checks the cookie flags, refresh with the cookie, cross-site refusal, rotation, mobile clients on the same API and logout. Postman doesn't send an `Origin` header, so in Postman everything behaves like a mobile app.
|
|
202
|
+
|
|
203
|
+
When everything matches, record the version and date in `CHANGELOG.md` and publish.
|
|
204
|
+
|
|
205
|
+
## Automated checks (run these too)
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
npm run typecheck && npm test && npm run build
|
|
209
|
+
createdb -h localhost -U postgres auth_e2e # once: an empty database for e2e
|
|
210
|
+
DATABASE_URL=postgres://postgres:postgres@localhost:5432/auth_e2e npm run e2e
|
|
211
|
+
npm run playground # in a second terminal
|
|
212
|
+
DATABASE_URL=postgres://postgres:postgres@localhost:5432/techelusive_playground npm run check:http
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`npm run e2e` creates and drops its own tables, so point it at an empty database (not the playground's). `npm run check:http` runs every flow over HTTP against the running playground (38 checks); start the playground on an empty database for it (`dropdb techelusive_playground`), since it registers fixed test users. CI runs all of these on every change.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Organizations
|
|
2
|
+
|
|
3
|
+
Organizations let users work in teams: companies, workspaces, fleets, households. A user can belong to many organizations, with a different role in each. Turn them on with `organizations.enabled: true`.
|
|
4
|
+
|
|
5
|
+
## Roles
|
|
6
|
+
|
|
7
|
+
| Role | Can |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `owner` | Everything, including deleting the organization and managing other owners |
|
|
10
|
+
| `admin` | Rename the organization, invite people, change roles of members and admins, remove members and admins |
|
|
11
|
+
| `member` | See the organization and its members |
|
|
12
|
+
|
|
13
|
+
Rules the API enforces:
|
|
14
|
+
|
|
15
|
+
- **Creators:** whoever creates an organization becomes its owner.
|
|
16
|
+
- **Owners:** only an owner can make someone an owner, demote an owner, or remove an owner.
|
|
17
|
+
- **Last owner:** an organization always keeps at least one owner (`LAST_OWNER`).
|
|
18
|
+
- **Leaving:** anyone can leave by removing themselves.
|
|
19
|
+
- **Outsiders:** non-members get `NOT_FOUND`, not `FORBIDDEN`, so they can't discover which organizations exist.
|
|
20
|
+
|
|
21
|
+
These organization roles are separate from app-wide roles (`user.roles`, e.g. `admin`), which are managed by app admins.
|
|
22
|
+
|
|
23
|
+
## Invitations
|
|
24
|
+
|
|
25
|
+
```http
|
|
26
|
+
POST /auth/organizations/:id/invitations
|
|
27
|
+
{ "email": "ravi@example.com", "role": "member" }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The library asks your `emailSender` to send an `organization_invitation` email with a `token`. Link it to a page in your app. Once the invited person has signed in, the page calls:
|
|
31
|
+
|
|
32
|
+
```http
|
|
33
|
+
POST /auth/organizations/invitations/accept
|
|
34
|
+
{ "token": "oi_..." }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **Security:** the signed-in user's **verified email must match** the invited email, so a forwarded link is useless to anyone else.
|
|
38
|
+
- **Lifetime:** invitations expire after `organizations.invitationExpiresInDays` (7 days by default).
|
|
39
|
+
- **Replacement:** sending a new invitation to the same email cancels the old one.
|
|
40
|
+
- **Cancelling:** an admin can cancel an invitation with `DELETE .../invitations/:invitationId`.
|
|
41
|
+
|
|
42
|
+
## The active organization
|
|
43
|
+
|
|
44
|
+
A device works in one organization at a time:
|
|
45
|
+
|
|
46
|
+
```http
|
|
47
|
+
POST /auth/organizations/switch
|
|
48
|
+
{ "organizationId": "a1b2..." }
|
|
49
|
+
→ { "accessToken": "eyJ...", "expiresIn": 900, "organizationId": "a1b2..." }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Replace the stored access token with the returned one. From then on, every token for this device (including after refresh) carries:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{ "org_id": "a1b2...", "org_role": "admin" }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Send `{ "organizationId": null }` to leave organization mode.
|
|
59
|
+
|
|
60
|
+
## Scoping your own data
|
|
61
|
+
|
|
62
|
+
Store `organization_id` on your own rows and filter by the claim:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
@Get('vehicles')
|
|
66
|
+
listVehicles(@CurrentUser() user: AccessTokenClaims) {
|
|
67
|
+
if (!user.org_id) throw new AuthError(AUTH_ERRORS.forbidden);
|
|
68
|
+
return this.vehicles.find({ where: { organizationId: user.org_id } });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Delete('vehicles/:id')
|
|
72
|
+
removeVehicle(@CurrentUser() user: AccessTokenClaims) {
|
|
73
|
+
if (user.org_role !== ORGANIZATION_ROLE.owner && user.org_role !== ORGANIZATION_ROLE.admin) throw new AuthError(AUTH_ERRORS.forbidden);
|
|
74
|
+
// ...
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
When a member is removed, their devices leave the organization at the next token refresh. For immediate effect on sensitive routes, add `@CheckSession()` and re-check membership with `OrganizationsService.listMembers()` or your own query.
|
|
79
|
+
|
|
80
|
+
## API keys for an organization
|
|
81
|
+
|
|
82
|
+
An API key created with an `organizationId` acts on behalf of that organization. In routes marked `@ApiKeyScopes()`, read it from `@CurrentCaller().apiKey.organizationId`.
|
|
83
|
+
|
|
84
|
+
## Deleting
|
|
85
|
+
|
|
86
|
+
`DELETE /auth/organizations/:id` (owners only) removes the organization, its memberships, invitations and API keys. Delete your own organization data in an `onEvent` handler for `organization.deleted`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Production checklist
|
|
2
|
+
|
|
3
|
+
Go through this list before launch. Each item says **why**, so you can make an informed exception.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
- [ ] **`NODE_ENV=production`.** At startup, this refuses a local encryption key, a non-https issuer, and a missing `redisUrl` (unless `singleServer: true`). `ConsoleEmailSender` throws on its first send.
|
|
8
|
+
- [ ] **`issuer` is your real https API URL.** It is written into every token. Changing it later signs everyone out, because existing tokens fail the issuer check.
|
|
9
|
+
- [ ] **Encryption uses AWS KMS** (`{ awsKmsKeyId, awsRegion }`).
|
|
10
|
+
- Create a symmetric KMS key and give the API's IAM role only `kms:Encrypt` and `kms:Decrypt` on it.
|
|
11
|
+
- Turn on automatic key rotation in KMS.
|
|
12
|
+
- **Moving an existing database from the local key to KMS:** configure `{ awsKmsKeyId, awsRegion, legacyLocalKey: '<old localKey>' }`. Older values (2FA secrets, webhook secrets, the signing key) still decrypt, and everything new is encrypted with KMS. Then rotate the signing key (`POST /auth/admin/signing-keys/rotate`) so the active key is KMS-protected. Without `legacyLocalKey`, users with 2FA would be locked out.
|
|
13
|
+
- [ ] **A real `emailSender`** (SES, Resend, Postmark...) with SPF, DKIM and DMARC set up for your sending domain, so codes don't land in spam.
|
|
14
|
+
- [ ] **SMS:** a DLT-registered template on MSG91 (India), and `phoneOtp.allowedCountryCodes` limited to the countries you serve. This limits "SMS pumping" fraud costs.
|
|
15
|
+
- [ ] **Secrets come from a secret manager** (AWS Secrets Manager / SSM): database URL, KMS key id, MSG91 key, Facebook app secret.
|
|
16
|
+
- [ ] **Settings reviewed:** sign-in methods, `defaultRoles`, `adminRoles`, token lifetimes, `allowedEmailDomains`.
|
|
17
|
+
|
|
18
|
+
## Infrastructure
|
|
19
|
+
|
|
20
|
+
- [ ] **`app.set('trust proxy', 1)`**, or the exact number of proxies in front of the app.
|
|
21
|
+
- **Too low:** every user shares the load balancer's IP, so rate limits hit everyone at once.
|
|
22
|
+
- **Too high:** attackers can fake their IP with an `X-Forwarded-For` header and dodge rate limits.
|
|
23
|
+
- [ ] **Redis (`redisUrl`)**. Required in production: without it each server counts rate limits separately, which multiplies the allowed attempts. Use a managed Redis (ElastiCache) with TLS (`rediss://`). Only a deployment with exactly one server process may set `singleServer: true` instead.
|
|
24
|
+
- [ ] **Health check:** point the load balancer / uptime monitor at `GET /auth/health` (503 when PostgreSQL or Redis don't answer).
|
|
25
|
+
- [ ] **TLS everywhere:** HTTPS at the load balancer with HSTS, and TLS to Postgres and Redis.
|
|
26
|
+
- [ ] **CORS** allows only your web app origins (with `credentials: true` if you use cookie mode). Mobile apps don't need CORS.
|
|
27
|
+
- [ ] **Web apps use cookie mode** (`refreshTokenCookie`), so the refresh token is out of reach of page scripts. See [Client integration](client-integration.md#web-apps-refresh-token-in-a-cookie).
|
|
28
|
+
- [ ] **`helmet()`** for security headers on any web pages you serve.
|
|
29
|
+
- [ ] **Request body size limit** (Express's default 100 kB is fine; don't raise it for auth routes).
|
|
30
|
+
|
|
31
|
+
## Database
|
|
32
|
+
|
|
33
|
+
- [ ] **Migrations run in your deploy pipeline** (`typeorm migration:run`), never `synchronize`.
|
|
34
|
+
- [ ] **Backups** are encrypted, with point-in-time recovery turned on, and restores are tested.
|
|
35
|
+
- [ ] **Least-privilege database user:** the app needs only read/write on its tables, not superuser.
|
|
36
|
+
- [ ] **Data retention reviewed.** Old sessions, codes, links, webhook deliveries and audit entries are deleted every hour automatically. Check that `retention.auditLogDays` (default 365) matches your legal requirements. See [Data retention](configuration.md#data-retention).
|
|
37
|
+
|
|
38
|
+
## Monitoring and response
|
|
39
|
+
|
|
40
|
+
- [ ] **Alerts on events** (via `onEvent` or a webhook to your alerting):
|
|
41
|
+
- `session.refresh_token_reused`: possible token theft.
|
|
42
|
+
- Spikes of `user.sign_in_failed`: credential stuffing.
|
|
43
|
+
- Spikes of `TOO_MANY_REQUESTS` responses.
|
|
44
|
+
- [ ] **Logs:** forward server logs. Unexpected errors are logged with stack traces, while clients only see `INTERNAL_SERVER_ERROR`.
|
|
45
|
+
- [ ] **Failed webhooks:** watch `GET /auth/admin/webhooks/:id/deliveries` for `failed` deliveries.
|
|
46
|
+
- [ ] **Dashboard numbers:** `GET /auth/admin/stats` gives accounts by status, active sessions and the last 24 hours of sign-ins, failures and refresh-token reuses.
|
|
47
|
+
- [ ] **Incident runbook:**
|
|
48
|
+
- **Leaked signing key:** `POST /auth/admin/signing-keys/rotate`, then rotate again two days later.
|
|
49
|
+
- **Compromised account:** suspend it, then `POST /auth/admin/users/:id/logout-all`.
|
|
50
|
+
- **Leaked API key:** `DELETE /auth/admin/api-keys/:id`.
|
|
51
|
+
- **Leaked webhook secret:** `POST .../rotate-secret`.
|
|
52
|
+
- [ ] **First admin** created, using a strong password plus 2FA or a passkey.
|
|
53
|
+
|
|
54
|
+
## Client apps
|
|
55
|
+
|
|
56
|
+
- [ ] Refresh tokens are stored securely and **only one refresh runs at a time** ([Client integration](client-integration.md#refreshing-tokens)).
|
|
57
|
+
- [ ] Email link pages act on a button press, not on page load (security scanners open links). This includes the `confirm_email_change` page.
|
|
58
|
+
- [ ] A "download my data" button calls `GET /auth/me/export` (and adds your app's own data).
|
|
59
|
+
- [ ] Google, Apple and Facebook client ids for **every** platform are in the settings.
|
|
60
|
+
- [ ] For passkeys, `apple-app-site-association` and `assetlinks.json` are published, and `passkeys.allowedOrigins` lists every web origin and Android app hash.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Protecting routes
|
|
2
|
+
|
|
3
|
+
`AuthModule` installs `AuthGuard` for the whole app, so **every route requires a signed-in user unless you say otherwise**. Forgetting a decorator therefore fails closed, never open.
|
|
4
|
+
|
|
5
|
+
## Decorators
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Public, Roles, CheckSession, ApiKeyScopes, AdminOnly, CurrentUser, CurrentUserId, CurrentCaller, AccessTokenClaims } from '@techelusive/nest-auth';
|
|
9
|
+
|
|
10
|
+
@Controller('orders')
|
|
11
|
+
export class OrdersController {
|
|
12
|
+
@Get('menu')
|
|
13
|
+
@Public() // anyone; @CurrentUser() still works if a token is sent
|
|
14
|
+
getMenu() {}
|
|
15
|
+
|
|
16
|
+
@Get('mine') // any signed-in user (default)
|
|
17
|
+
listMine(@CurrentUser() user: AccessTokenClaims) {}
|
|
18
|
+
|
|
19
|
+
@Post()
|
|
20
|
+
@Roles('seller', 'admin') // at least one of these roles
|
|
21
|
+
create(@CurrentUserId() userId: string) {}
|
|
22
|
+
|
|
23
|
+
@Post(':id/refund')
|
|
24
|
+
@CheckSession() // also check the session is still active right now
|
|
25
|
+
refund() {}
|
|
26
|
+
|
|
27
|
+
@Get('export')
|
|
28
|
+
@ApiKeyScopes('orders:read') // servers with an API key holding this scope (users still allowed)
|
|
29
|
+
export(@CurrentCaller() caller) {} // caller.user or caller.apiKey
|
|
30
|
+
|
|
31
|
+
@Delete(':id')
|
|
32
|
+
@AdminOnly() // roles in settings.admin.adminRoles, checked live
|
|
33
|
+
remove() {}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Decorators work on methods and on whole controllers. A method decorator wins over its controller's.
|
|
38
|
+
|
|
39
|
+
## What's in the token
|
|
40
|
+
|
|
41
|
+
`@CurrentUser()` returns the access-token claims:
|
|
42
|
+
|
|
43
|
+
| Claim | Example | Meaning |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `sub` | `"5f0c..."` | User id. Use it as the foreign key in your tables. |
|
|
46
|
+
| `sid` | `"8c1e..."` | Session (device) id |
|
|
47
|
+
| `roles` | `["user"]` | App-wide roles |
|
|
48
|
+
| `amr` | `["password", "two_factor"]` | How the user signed in. Require `two_factor` for very sensitive actions if you like. |
|
|
49
|
+
| `org_id`, `org_role` | `"a1b2..."`, `"admin"` | The organization chosen on this device, if any ([Organizations](organizations.md)) |
|
|
50
|
+
| `iss`, `aud` | `"https://api.myapp.com"` | Your issuer |
|
|
51
|
+
| `iat`, `exp`, `jti` | | Issued at, expiry, unique id |
|
|
52
|
+
|
|
53
|
+
`@CurrentUserId()` returns just the user id (`sub`).
|
|
54
|
+
|
|
55
|
+
## How fresh is the token?
|
|
56
|
+
|
|
57
|
+
An access token is valid until it expires (`tokens.accessTokenMinutes`, 15 by default). Within that window, a user who signed out, was suspended or lost a role still holds a working token. Two ways to close the gap:
|
|
58
|
+
|
|
59
|
+
- **`@CheckSession()`** on sensitive routes checks in the database that the session is still active and the user is `ACTIVE`. The answer is cached for 5 seconds per server, which costs at most one small query per user every 5 seconds. `@AdminOnly()` always does this.
|
|
60
|
+
- **Shorter access tokens.** For example `tokens.accessTokenMinutes: 5`, at the cost of more refreshes.
|
|
61
|
+
|
|
62
|
+
Role changes appear in the user's **next** access token (at the next refresh).
|
|
63
|
+
|
|
64
|
+
## Opt-in instead of protect-all
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
AuthModule.forRoot({ protectAllRoutes: false, useFactory: ... })
|
|
68
|
+
|
|
69
|
+
@UseGuards(AuthGuard) // then protect controllers explicitly
|
|
70
|
+
@Controller('orders')
|
|
71
|
+
export class OrdersController {}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Verifying tokens in other services
|
|
75
|
+
|
|
76
|
+
Access tokens are standard JWTs signed with ES256. Any service in any language can verify them using the public keys at `GET https://api.myapp.com/.well-known/jwks.json`, with no shared secret.
|
|
77
|
+
|
|
78
|
+
Node.js with [`jose`](https://github.com/panva/jose):
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
82
|
+
|
|
83
|
+
const keys = createRemoteJWKSet(new URL('https://api.myapp.com/.well-known/jwks.json'));
|
|
84
|
+
|
|
85
|
+
export async function verifyAccessToken(token: string) {
|
|
86
|
+
const { payload } = await jwtVerify(token, keys, {
|
|
87
|
+
issuer: 'https://api.myapp.com',
|
|
88
|
+
audience: 'https://api.myapp.com',
|
|
89
|
+
algorithms: ['ES256'],
|
|
90
|
+
typ: 'at+jwt',
|
|
91
|
+
});
|
|
92
|
+
return payload; // payload.sub, payload.roles, ...
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Python with [PyJWT](https://pyjwt.readthedocs.io):
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import jwt
|
|
100
|
+
jwks = jwt.PyJWKClient("https://api.myapp.com/.well-known/jwks.json")
|
|
101
|
+
|
|
102
|
+
def verify_access_token(token: str) -> dict:
|
|
103
|
+
key = jwks.get_signing_key_from_jwt(token).key
|
|
104
|
+
return jwt.decode(token, key, algorithms=["ES256"],
|
|
105
|
+
issuer="https://api.myapp.com", audience="https://api.myapp.com")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Always pin the algorithm (`ES256`) and check `iss` and `aud`. These other services can't see sign-outs before expiry. If they need that, have them call an endpoint on the main API that is marked `@CheckSession()`.
|
|
109
|
+
|
|
110
|
+
## API keys (server to server)
|
|
111
|
+
|
|
112
|
+
Other servers (partners, cron jobs, internal services) call your API with a key instead of a user token:
|
|
113
|
+
|
|
114
|
+
```http
|
|
115
|
+
GET /orders/export
|
|
116
|
+
x-api-key: ak_3Fx9...
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- An admin creates keys with `POST /auth/admin/api-keys` ([details](admin-api-keys-webhooks.md#api-keys)).
|
|
120
|
+
- Keys only work on routes marked `@ApiKeyScopes(...)`, and only if they hold **all** the listed scopes. On any other route, an API key is not accepted.
|
|
121
|
+
- In the route, `@CurrentCaller()` returns `{ apiKey: { id, name, scopes, organizationId } }`.
|
|
122
|
+
|
|
123
|
+
## Using the services from your own code
|
|
124
|
+
|
|
125
|
+
The module is global, so inject its services anywhere:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
@Injectable()
|
|
129
|
+
export class ProfileService {
|
|
130
|
+
constructor(private readonly users: UsersService) {}
|
|
131
|
+
|
|
132
|
+
async finishOnboarding(userId: string, request: RequestContext) {
|
|
133
|
+
await this.users.completeOnboarding(userId, request);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
| Service | Useful methods |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `UsersService` | `findUserById`, `findUserByEmail`, `completeOnboarding`, `updateProfile`, `setRoles`, `suspendUser`, `deleteUser`, `toPublicProfile` |
|
|
141
|
+
| `SessionService` | `listActiveSessions`, `revokeSession`, `revokeAllSessions`, `verifyAccessToken` |
|
|
142
|
+
| `OrganizationsService` | `listMyOrganizations`, `listMembers`, `createOrganization` |
|
|
143
|
+
| `SettingsService` | `getSettings()`, the settings in force right now |
|
|
144
|
+
| `AuthEventsService` | `record()`, to write your own entries to the audit log |
|
|
145
|
+
| `ApiKeysService`, `WebhooksService`, `TwoFactorService`, `SigningKeysService` | Same operations as the admin API |
|
|
146
|
+
|
|
147
|
+
Use `@CurrentRequest()` in controllers to get the `RequestContext` (IP and user agent). Use `SYSTEM_REQUEST` in scripts and jobs.
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Security design
|
|
2
|
+
|
|
3
|
+
This page explains how the library protects users, so you can review it, explain it to auditors, and avoid undoing a protection by accident. To report a vulnerability, see [SECURITY.md](../SECURITY.md).
|
|
4
|
+
|
|
5
|
+
## Secrets at rest
|
|
6
|
+
|
|
7
|
+
| Secret | How it is stored |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Passwords | Argon2id (64 MiB, 3 iterations), re-hashed automatically if the parameters are raised |
|
|
10
|
+
| Refresh, link, challenge and API-key tokens | SHA-256 hash only (they are 256-bit random, so a fast hash is safe) |
|
|
11
|
+
| Email/SMS codes and backup codes | Salted SHA-256, with attempt limits |
|
|
12
|
+
| Token-signing private key, 2FA secrets, webhook secrets | Encrypted with AWS KMS (or AES-256-GCM with a local key in development), bound to their row so a value copied elsewhere won't decrypt |
|
|
13
|
+
|
|
14
|
+
A full database leak therefore exposes no usable password, token, code or key.
|
|
15
|
+
|
|
16
|
+
## Tokens
|
|
17
|
+
|
|
18
|
+
- **Access tokens:** ES256 JWTs. Verification **pins the algorithm** (no `alg: none`, no algorithm confusion) and checks the `typ` (`at+jwt`), issuer, audience and expiry, with 30 seconds of clock tolerance.
|
|
19
|
+
- **Refresh tokens:** opaque, single use and rotated on every refresh. Marking a token used is a single atomic `UPDATE`, so two parallel refreshes can't both succeed. A reused token **ends the session** and records `session.refresh_token_reused`.
|
|
20
|
+
- **Lifetimes:** refresh tokens never outlive their session (`sessionDays`).
|
|
21
|
+
- **Web apps (cookie mode):** the refresh token lives in an `HttpOnly; Secure; SameSite=Strict` cookie that page scripts can't read, and a refresh using the cookie must carry an allowed `Origin` header, so other sites can't trigger it (CSRF).
|
|
22
|
+
- **Email change:** needs the current password (or a sign-in from the last 10 minutes for accounts without one), is confirmed by a link to the new address, and notifies the old address. A stolen access token alone can't take over the account by changing its email.
|
|
23
|
+
- **Data minimization:** old sessions, codes, links and audit entries are deleted on a schedule (`retention` settings), and users can export what is stored about them.
|
|
24
|
+
- **Key rotation:** keys rotate without downtime. Old public keys stay published for 2 days.
|
|
25
|
+
|
|
26
|
+
## Attacks considered
|
|
27
|
+
|
|
28
|
+
| Attack | Defense |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Password guessing | Rate limits per IP and per account; Argon2id; breached-password check (k-anonymity, the password never leaves the server) |
|
|
31
|
+
| Credential stuffing | The same limits, plus `user.sign_in_failed` events for alerting; 2FA; passkeys |
|
|
32
|
+
| Account enumeration | Send/forgot/register answer the same for known and unknown accounts; unknown-email sign-in still runs a full Argon2 check so timing matches; account status errors appear only after the user proved who they are; non-members get `NOT_FOUND` for organizations |
|
|
33
|
+
| Code guessing | 5 attempts per code; 1 code per minute and 5 per hour per destination; codes expire in 10 minutes |
|
|
34
|
+
| Token replay | Single-use links, codes, challenges and refresh tokens, each consumed atomically |
|
|
35
|
+
| TOTP replay | The last accepted time step is stored, so a code can't be used twice |
|
|
36
|
+
| Account pre-hijacking | If an account's email is still unverified after 24 hours, the owner proving it later (code, link or verified social email) removes any password set before and ends all sessions |
|
|
37
|
+
| Social account takeover | A provider email is trusted only if the provider marks it verified. Facebook emails are never trusted for linking |
|
|
38
|
+
| Stolen social token reuse | Tokens must be issued to **your** client ids / app id; optional nonce binding |
|
|
39
|
+
| Mass assignment | Unknown body fields are rejected; roles and status can't be set by users |
|
|
40
|
+
| SSRF via webhooks | https only; DNS resolved and private, loopback, link-local and metadata ranges blocked when saving and before every send; redirects not followed |
|
|
41
|
+
| Webhook spoofing / replay | HMAC-SHA256 signature with timestamp; 5-minute window |
|
|
42
|
+
| Clickjacking / XSS | Not applicable to a JSON API. Protect your web pages with CSP and `frame-ancestors` |
|
|
43
|
+
| Brute force on admin | Admin routes check the session live; all admin actions are audited |
|
|
44
|
+
| Leaked API key | Keys are hashed, scoped, revocable and optionally expiring; `lastUsedAt` shows use |
|
|
45
|
+
|
|
46
|
+
## Rate limiting
|
|
47
|
+
|
|
48
|
+
Limits are counted in Redis when `redisUrl` is set (shared by all servers), otherwise in each server's memory. If Redis is configured but unreachable, limited requests are **refused** (fail closed) rather than allowed. Limits are keyed by `request.ip`, so set `trust proxy` correctly ([Production checklist](production-checklist.md)). If you don't, every user shares the load balancer's IP and its limits.
|
|
49
|
+
|
|
50
|
+
## What the library does not do
|
|
51
|
+
|
|
52
|
+
- **No CAPTCHA.** Add one in front of sign-up and send-code endpoints if bots target you (e.g. Cloudflare Turnstile, verified in a guard or middleware).
|
|
53
|
+
- **No HTTP security headers.** Use `helmet` in your app.
|
|
54
|
+
- **No geo-blocking or IP reputation.** Use your edge provider.
|
|
55
|
+
- **No forced 2FA.** It is the user's choice. To require it, check `amr` contains `two_factor` on the routes that need it.
|
|
56
|
+
|
|
57
|
+
## Data protection
|
|
58
|
+
|
|
59
|
+
- **Deletion:** account deletion wipes personal data and every way to sign in, while keeping the row for referential integrity. The audit log keeps IP addresses and user agents. Prune it to match your retention policy.
|
|
60
|
+
- **Consent:** acceptance times of the terms and privacy policy are stored (`termsAcceptedAt`, `privacyPolicyAcceptedAt`).
|
|
61
|
+
- **Data export:** for data-access requests, export with `GET /auth/admin/users/:id` plus your own data.
|