@ambushsoftworks/nestjs-auth-graphql 0.12.0 → 0.14.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/README.md +281 -1
  3. package/dist/auth.module.d.ts +9 -0
  4. package/dist/auth.module.d.ts.map +1 -1
  5. package/dist/auth.module.js +69 -4
  6. package/dist/auth.module.js.map +1 -1
  7. package/dist/constants.d.ts +1 -0
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/constants.js +2 -1
  10. package/dist/constants.js.map +1 -1
  11. package/dist/index.d.ts +3 -6
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +3 -6
  14. package/dist/index.js.map +1 -1
  15. package/dist/interfaces/biometric-repository.interface.d.ts +29 -17
  16. package/dist/interfaces/biometric-repository.interface.d.ts.map +1 -1
  17. package/dist/interfaces/biometric-verifier.interface.d.ts +10 -0
  18. package/dist/interfaces/biometric-verifier.interface.d.ts.map +1 -0
  19. package/dist/interfaces/{magic-link-repository.interface.js → biometric-verifier.interface.js} +1 -1
  20. package/dist/interfaces/biometric-verifier.interface.js.map +1 -0
  21. package/dist/services/biometric-auth.service.d.ts +47 -17
  22. package/dist/services/biometric-auth.service.d.ts.map +1 -1
  23. package/dist/services/biometric-auth.service.js +109 -68
  24. package/dist/services/biometric-auth.service.js.map +1 -1
  25. package/dist/services/biometric-challenge.service.d.ts +27 -0
  26. package/dist/services/biometric-challenge.service.d.ts.map +1 -0
  27. package/dist/services/biometric-challenge.service.js +116 -0
  28. package/dist/services/biometric-challenge.service.js.map +1 -0
  29. package/dist/services/biometric-verification.service.d.ts.map +1 -1
  30. package/dist/services/biometric-verification.service.js +4 -0
  31. package/dist/services/biometric-verification.service.js.map +1 -1
  32. package/dist/test-utils/mock-repositories.d.ts.map +1 -1
  33. package/dist/test-utils/mock-repositories.js +8 -7
  34. package/dist/test-utils/mock-repositories.js.map +1 -1
  35. package/dist/verifiers/es256-device-key.verifier.d.ts +14 -0
  36. package/dist/verifiers/es256-device-key.verifier.d.ts.map +1 -0
  37. package/dist/verifiers/es256-device-key.verifier.js +42 -0
  38. package/dist/verifiers/es256-device-key.verifier.js.map +1 -0
  39. package/package.json +8 -2
  40. package/dist/interfaces/magic-link-repository.interface.d.ts +0 -6
  41. package/dist/interfaces/magic-link-repository.interface.d.ts.map +0 -1
  42. package/dist/interfaces/magic-link-repository.interface.js.map +0 -1
  43. package/dist/interfaces/password-reset-strategy.interface.d.ts +0 -7
  44. package/dist/interfaces/password-reset-strategy.interface.d.ts.map +0 -1
  45. package/dist/interfaces/password-reset-strategy.interface.js +0 -3
  46. package/dist/interfaces/password-reset-strategy.interface.js.map +0 -1
  47. package/dist/repositories/noop-biometric.repository.d.ts +0 -26
  48. package/dist/repositories/noop-biometric.repository.d.ts.map +0 -1
  49. package/dist/repositories/noop-biometric.repository.js +0 -56
  50. package/dist/repositories/noop-biometric.repository.js.map +0 -1
  51. package/dist/repositories/noop-magic-link.repository.d.ts +0 -9
  52. package/dist/repositories/noop-magic-link.repository.d.ts.map +0 -1
  53. package/dist/repositories/noop-magic-link.repository.js +0 -37
  54. package/dist/repositories/noop-magic-link.repository.js.map +0 -1
  55. package/dist/strategies/magic-link.strategy.d.ts +0 -16
  56. package/dist/strategies/magic-link.strategy.d.ts.map +0 -1
  57. package/dist/strategies/magic-link.strategy.js +0 -80
  58. package/dist/strategies/magic-link.strategy.js.map +0 -1
  59. package/dist/strategies/verification-code.strategy.d.ts +0 -11
  60. package/dist/strategies/verification-code.strategy.d.ts.map +0 -1
  61. package/dist/strategies/verification-code.strategy.js +0 -44
  62. package/dist/strategies/verification-code.strategy.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -27,6 +27,169 @@ for buried obligations. **The marker was introduced in 0.9.0 and has not been
27
27
  retro-applied**, so it is reliable from 0.9.0 onward only. Convention adopted
28
28
  from `@ambushsoftworks/nestjs-payments-graphql`.
29
29
 
30
+ ## [0.14.0] - 2026-09-29
31
+
32
+ Biometric authentication works. It has been exported since v0.1.3 and never did.
33
+
34
+ ### ⚠ Security
35
+
36
+ - **The previous `authenticateWithBiometric` verified nothing.** It took
37
+ `(userId, deviceId)`, checked only that an enrolled credential's id equalled the
38
+ supplied `deviceId`, and then issued an access and refresh token. No signature,
39
+ no challenge, no nonce; the public key `enableBiometric` stored was never read
40
+ by anything. Neither input is a secret, so anyone who knew a user id and an
41
+ enrolled device id could obtain a session. It also minted its own tokens, so it
42
+ bypassed the account-status check every other login goes through — a suspended
43
+ user could sign in.
44
+
45
+ **If you wired biometric authentication on 0.13.0 or earlier, treat any session
46
+ it issued as unauthenticated**, and revoke refresh tokens for affected users.
47
+ We know of no consumer that did: `IBiometricRepository` had no implementations,
48
+ and LiftIQ, the only app with biometrics, built its own rather than use this.
49
+
50
+ ### Added
51
+
52
+ - **A working challenge-response flow.**
53
+ ```
54
+ enrolCredential({ userId, publicKey, deviceName }) -> credentialId
55
+ requestChallenge(credentialId) -> { challengeId, challenge, expiresAt }
56
+ authenticateWithBiometric({ challengeId, credentialId, signature }) -> session
57
+ ```
58
+ 32-byte single-use challenges, 60-second default expiry, ES256 signature
59
+ verification, and a session issued through `AuthService.issueAuthSession` — the
60
+ same sink as every other login, so `assertUserActive`, cookie auth, realm claims
61
+ and `SESSION_ISSUED` all apply without being reimplemented.
62
+ - **`IBiometricVerifier`**, with `Es256DeviceKeyVerifier` as the default. The
63
+ verifier is selected by each credential's stored `algorithm`, so a credential
64
+ enrolled as ES256 is only ever verified as ES256 — the same pinning `JwtStrategy`
65
+ gained in 0.12.0. WebAuthn can be added later against this interface without
66
+ reshaping the port.
67
+ - **`biometric` module options**: `verifiers`, `challengeExpirySeconds`,
68
+ `challengeRateLimit`. Boot refuses an empty verifier list, two verifiers claiming
69
+ one algorithm, a non-positive expiry, and a rate limit without
70
+ `rateLimiterInstance`.
71
+ - **A per-IP rate limit** on challenge issuance alongside the per-credential one.
72
+ The per-credential limit cannot see a caller varying the credential id.
73
+
74
+ ### Changed — breaking
75
+
76
+ - **`IBiometricRepository` is reshaped**, composed from a new
77
+ `IBiometricChallengeStore`. It had no implementations, so this breaks nobody in
78
+ practice. Credentials now carry `algorithm`, `isActive`, `lastUsedAt` and
79
+ optional `metadata`; challenges are keyed on `credentialId`.
80
+
81
+ Its one hard contract: **`consumeChallenge` must be atomic.** A read, a check and
82
+ then a write lets two concurrent requests with one challenge both succeed. Use a
83
+ single conditional update and check the affected count — the README has the
84
+ Prisma form.
85
+
86
+ Two schema notes: `credentialId` on a challenge is **not** a foreign key (a
87
+ challenge is stored for ids that do not exist, so the signed-out request cannot
88
+ reveal which credentials are enrolled), and there is **no uniqueness** on any
89
+ device string.
90
+ - **`authenticateWithBiometric`'s signature changed** to
91
+ `({ challengeId, credentialId, signature }, opts)`.
92
+ - **`enableBiometric`, `disableBiometric` and `updateLastBiometricLogin` are gone
93
+ from `IUserRepositoryBiometric`.** Having credentials in two ports is why
94
+ enrolment and authentication used different stores and never met.
95
+ - **`NoOpBiometricRepository` is removed.** A no-op that accepts enrolments and
96
+ returns no credentials turns a missing implementation into a login that silently
97
+ never works. Without `biometricRepositoryInstance` the services are not
98
+ registered.
99
+ - **`credentialId` is generated by the package**, not by the client or the adapter.
100
+ It travels on an unauthenticated request, so it must be unguessable.
101
+
102
+ ### Security properties worth knowing
103
+
104
+ - **What it proves.** Possession of the enrolled private key — **not** that a
105
+ biometric check happened. Nothing in an ECDSA signature attests to that, and the
106
+ server cannot tell a hardware-backed key from a software one. Most mobile
107
+ biometric APIs accept a device PIN by default. Weigh that before making it a
108
+ sole factor.
109
+ - **Every biometric failure returns one 401.** Unknown credential, deactivated
110
+ credential, replayed or mismatched challenge, bad signature, missing verifier.
111
+ The route is reachable signed-out, so anything distinguishable reveals which
112
+ credentials exist. An inactive *account* is deliberately not flattened into it.
113
+ - **A failed attempt still burns the challenge**, so one challenge cannot be used
114
+ to try many signatures.
115
+
116
+ ### Documentation
117
+
118
+ - README: **Biometric Authentication**, leading with what a successful
119
+ authentication does and does not prove, the `IBiometricRepository` contract with
120
+ the atomic-consume example, and a `biometric` options table.
121
+ - CLAUDE.md: replaced a flow description that named three methods which never
122
+ existed and described verification the code never performed.
123
+
124
+ ### Testing
125
+
126
+ - 24 unit tests on the flow with real P-256 keys, real signatures and a real
127
+ `AuthService` — so the suspended-user case exercises `assertUserActive` rather
128
+ than a double. The spec they replace mocked both sides of every seam and
129
+ asserted the method returned tokens, which was true and hid that it returned
130
+ them without verifying anything.
131
+ - 9 end-to-end tests on both NestJS majors, including that the issued token works
132
+ on a guarded route, which is what proves the session came from the shared sink.
133
+ - 13 on the challenge service, including twenty concurrent claims on one challenge
134
+ yielding exactly one winner.
135
+ - Mutation checks: dropping the challenge/credential binding, forcing the verifier
136
+ true, ignoring `isActive`, minting tokens locally instead of via
137
+ `issueAuthSession`, and making the test store non-atomic. Each compiles; each
138
+ fails the suite.
139
+ ## [0.13.0] - 2026-09-29
140
+
141
+ **Never published separately — these changes ship inside `0.14.0`.** Both landed
142
+ on master before either was released, and `npm version` writes one version per
143
+ release, so `0.13.0` does not exist on npm. The entry stays because the changes
144
+ are real and worth reading on their own; if you are upgrading, everything here is
145
+ in `0.14.0`.
146
+
147
+ A documentation release, with one removal. Nothing in the runtime behaviour of
148
+ any supported path changes.
149
+
150
+ ### Removed
151
+
152
+ - **`MagicLinkStrategy`, `VerificationCodeStrategy`, `IPasswordResetStrategy`,
153
+ `IMagicLinkRepository` and `NoOpMagicLinkRepository`** are no longer exported,
154
+ and their files are gone. Nothing in the package consumed
155
+ `IPasswordResetStrategy` — `AuthService` calls `VerificationService` directly —
156
+ and `MagicLinkStrategy` could not be constructed through Nest DI at all,
157
+ because it injects a `'MAGIC_LINK_REPOSITORY'` token that `AuthModule` never
158
+ registered. The `NoOpMagicLinkRepository` docblock even showed a
159
+ `magicLinkRepositoryInstance` option that does not exist in
160
+ `AuthModuleOptions`.
161
+
162
+ This is a public API removal, hence the minor bump, but it cannot break a
163
+ working consumer: there was no supported way to reach any of it. If you
164
+ constructed `MagicLinkStrategy` yourself by registering that token, the file is
165
+ small and self-contained — copy it into your application. Password reset
166
+ through the package remains the 6-digit code flow, unchanged.
167
+
168
+ ### Documentation
169
+
170
+ - **README: new `Password Reset` section.** The feature had one passing mention,
171
+ despite the properties a consumer cannot infer and must not discover in
172
+ production: 3 attempts before the code is destroyed, the 15-minute expiry, the
173
+ 60-second per-user cooldown, the per-IP limit of 5/hour checked *before* the
174
+ user lookup for enumeration safety, and that a successful reset **revokes every
175
+ refresh token for that user**. It also states plainly that the response is
176
+ identical whether the address was sent a code, skipped or does not exist, and
177
+ that the distinguishing signal is `PASSWORD_RESET_REQUESTED` with
178
+ `result: 'SKIPPED_SOCIAL_ONLY'` on your `IAuthLogger`.
179
+ - **README: new `Phone & SMS Verification` section.** Five `perform*` methods
180
+ that previously appeared only as rows in a table. Records that SMS is
181
+ code-only — `features.verificationMode: 'token'` does not apply to it — and
182
+ that omitting `smsServiceInstance` substitutes a no-op, so codes are stored
183
+ but never delivered.
184
+ - **README: `sendgrid` and `twilio` option tables.** The only two of 42
185
+ `AuthModuleOptions` fields documented nowhere. Both are read solely by the
186
+ bundled services; a custom `IEmailService` or `ISmsService` ignores them.
187
+ - **`verification.baseUrl`** now names its exported type,
188
+ `VerificationBaseUrlResolver`.
189
+ - **`package.json` gains `homepage` and `bugs`.** npmjs.com renders both in the
190
+ sidebar, so until now the package page offered no route to the repository or
191
+ to filing an issue.
192
+
30
193
  ## [0.12.0] - 2026-09-20
31
194
 
32
195
  Ambush Desk asked for two things: verifying JWTs that other systems sign, and API
package/README.md CHANGED
@@ -16,8 +16,11 @@ Production-grade authentication module for NestJS GraphQL + REST APIs. JWT, cook
16
16
  - [External JWTs](#external-jwts)
17
17
  - [Email System](#email-system)
18
18
  - [Verification Modes](#verification-modes)
19
+ - [Phone \& SMS Verification](#phone--sms-verification)
19
20
  - [OAuth](#oauth)
21
+ - [Biometric Authentication](#biometric-authentication)
20
22
  - [Brute Force Protection](#brute-force-protection)
23
+ - [Password Reset](#password-reset)
21
24
  - [Password Policy](#password-policy)
22
25
  - [Lifecycle Hooks](#lifecycle-hooks)
23
26
  - [Account Status](#account-status)
@@ -34,12 +37,16 @@ Production-grade authentication module for NestJS GraphQL + REST APIs. JWT, cook
34
37
  - [CSRF Options](#csrf-options-csrf)
35
38
  - [Composable Email](#composable-email-email)
36
39
  - [Verification Options](#verification-options-verification)
40
+ - [Biometric Options (`biometric`)](#biometric-options-biometric)
41
+ - [SendGrid Options (`sendgrid`)](#sendgrid-options-sendgrid)
42
+ - [Twilio Options (`twilio`)](#twilio-options-twilio)
37
43
  - [Optional Instance Options](#optional-instance-options)
38
44
  - [Decorators](#decorators)
39
45
  - [Guards](#guards)
40
46
  - [GraphQL Subscriptions](#graphql-subscriptions)
41
47
  - [Utilities](#utilities)
42
48
  - [Security Features](#security-features)
49
+ - [Upgrading from 0.9.x](#upgrading-from-09x)
43
50
  - [Migrating to v0.12.0](#migrating-to-v0120)
44
51
  - [Migrating to v0.11.0](#migrating-to-v0110)
45
52
  - [Migrating to v0.10.0](#migrating-to-v0100)
@@ -805,6 +812,45 @@ link. `expiresInMinutes` reflects the mode's real expiry, so render it rather
805
812
  than hardcoding a duration. Both trailing parameters are optional, so existing
806
813
  implementations continue to compile.
807
814
 
815
+ ### Phone & SMS Verification
816
+
817
+ An authenticated user adds a phone number, receives a 6-digit code by SMS, and
818
+ confirms it. All five methods take the current user, so they sit behind your
819
+ authentication guard — this is account management, not a sign-in path.
820
+
821
+ | Method | Purpose |
822
+ |---|---|
823
+ | `performSendPhoneVerification(input, user)` | Store the number and send a code |
824
+ | `performVerifyPhone(input, user)` | Confirm the code, mark the number verified |
825
+ | `performResendPhoneVerification(phoneNumber, user)` | Send again, subject to the 60-second cooldown |
826
+ | `performRemovePhoneNumber(user)` | Clear the number and its verified state |
827
+ | `performPhoneVerificationStatus(user)` | Whether a number is present and verified |
828
+
829
+ Provide `smsServiceInstance` and `verificationRepositoryInstance`. `ISmsService`
830
+ has a single method, so the seam is small:
831
+
832
+ ```typescript
833
+ export class TwilioSmsService implements ISmsService {
834
+ async sendVerificationSms(phoneNumber: string, code: string): Promise<void> {
835
+ await this.client.messages.create({ to: phoneNumber, body: `Your code is ${code}` });
836
+ }
837
+ }
838
+ ```
839
+
840
+ Two things worth knowing:
841
+
842
+ - **SMS is code-only.** `features.verificationMode: 'token'` governs email
843
+ verification and password reset. A phone always receives a 6-digit code,
844
+ because a link is not usable from a text message in the same way — so
845
+ `verification.baseUrl` is irrelevant here.
846
+ - **Omitting `smsServiceInstance` does not fail loudly.** The package substitutes
847
+ a no-op that logs, so codes are generated and stored but never delivered.
848
+ Verification then only succeeds for someone reading your application logs.
849
+
850
+ The number's realm is scoped like any other identity field: with realms enabled,
851
+ `findByPhoneNumber` receives the realm, so the same number can exist once per
852
+ realm.
853
+
808
854
  ### OAuth
809
855
 
810
856
  Google and Facebook OAuth with encrypted token storage (AES-256-GCM).
@@ -858,6 +904,110 @@ It sits beside `useFactory` because Nest fixes a module's controllers before asy
858
904
 
859
905
  With realms enabled, a request without a realm gets `400 Realm required` first. Before 0.10.0 the lockout, inactive-account, rate-limit and unconfigured-provider cases answered 500.
860
906
 
907
+ ### Biometric Authentication
908
+
909
+ A device holds an ECDSA P-256 keypair, gated behind the OS biometric prompt. The
910
+ server stores the public key and verifies a signature over a challenge it issued.
911
+
912
+ ```typescript
913
+ biometricRepositoryInstance: myBiometricRepo, // enables the capability
914
+ biometric: {
915
+ challengeExpirySeconds: 60, // default
916
+ challengeRateLimit: { maxAttempts: 5, windowMs: 60_000 }, // needs rateLimiterInstance
917
+ },
918
+ ```
919
+
920
+ #### ⚠ What a successful authentication actually proves
921
+
922
+ **It proves the client still holds the private key. It does not prove a biometric
923
+ check happened.** Nothing in an ECDSA signature attests to that, and the server
924
+ cannot distinguish a key held in a hardware enclave from one generated in
925
+ software. Whether a fingerprint was scanned — or a PIN accepted instead, which
926
+ most mobile biometric APIs allow by default — is the client's word.
927
+
928
+ Treat this as *possession of a device-bound key that the client promises to
929
+ gate*. It is a good second factor and a good convenience login. Before making it
930
+ the sole factor for something sensitive, be clear that you are trusting the app,
931
+ not the biometric. WebAuthn's `userVerification` flag is what actually attests to
932
+ user verification, and is not this.
933
+
934
+ #### The flow
935
+
936
+ ```typescript
937
+ // 1. Enrol, authenticated. The package generates the credential id — keep it
938
+ // on the device alongside the private key.
939
+ const { credentialId } = await biometricAuth.enrolCredential({
940
+ userId: user.id,
941
+ publicKey, // PEM, SPKI
942
+ deviceName: 'iPhone 15',
943
+ metadata: { yourDeviceString: '...' }, // optional, opaque to the package
944
+ });
945
+
946
+ // 2. Request a challenge. No user id — the caller is usually signed out.
947
+ const challenge = await biometricAuth.requestChallenge(credentialId, {
948
+ ipAddress: req.ip,
949
+ });
950
+ if (!challenge) { /* rate limited */ }
951
+
952
+ // 3. The client signs `challenge.challenge` (base64) with the private key.
953
+
954
+ // 4. Verify and receive a session.
955
+ const session = await biometricAuth.authenticateWithBiometric(
956
+ { challengeId: challenge.challengeId, credentialId, signature },
957
+ { res, realm, ipAddress: req.ip },
958
+ );
959
+ ```
960
+
961
+ Also `listCredentials(userId)` for a device list and `removeCredential(userId,
962
+ credentialId)` to deactivate one.
963
+
964
+ #### What the package guarantees
965
+
966
+ | | |
967
+ |---|---|
968
+ | Challenge | 32 random bytes, single use, 60s default |
969
+ | Replay | A challenge is consumed **atomically** before anything is checked, so a retry loses the race — and a failed attempt still burns it |
970
+ | Cross-credential | A challenge issued for one credential cannot be answered with another's key |
971
+ | Algorithm | Pinned **per credential**. A credential enrolled as ES256 is only verified as ES256; no verifier for its algorithm means refusal, not a fallback |
972
+ | Account status | The session comes from the same sink as every other login, so a suspended or soft-deleted user is refused **after** a valid signature |
973
+ | Enumeration | Every biometric failure returns the same 401, and a challenge is issued even for a credential that does not exist |
974
+ | Rate limiting | Per credential, and per IP so varying the credential id cannot evade it |
975
+
976
+ #### Implementing `IBiometricRepository`
977
+
978
+ One method has a contract you cannot satisfy with a read followed by a write:
979
+
980
+ ```typescript
981
+ async consumeChallenge(challengeId: string) {
982
+ // Atomic: mark used and return it, only if it was unused and unexpired.
983
+ const { count } = await prisma.biometricChallenge.updateMany({
984
+ where: { id: challengeId, used: false, expiresAt: { gt: new Date() } },
985
+ data: { used: true, usedAt: new Date() },
986
+ });
987
+ if (count !== 1) return null; // someone else claimed it, or it expired
988
+ return this.load(challengeId);
989
+ }
990
+ ```
991
+
992
+ Read-then-write lets two concurrent requests with one challenge both succeed,
993
+ which is the replay single use exists to prevent.
994
+
995
+ Two more things your schema needs to know:
996
+
997
+ - **`credentialId` on a challenge is not a foreign key.** A challenge is stored
998
+ for credential ids that do not exist, so that the signed-out request cannot be
999
+ used to discover which credentials are enrolled.
1000
+ - **No uniqueness on `deviceName` or any device string.** It is a display label,
1001
+ never matched on. Key rotation is enrol-new then deactivate-old.
1002
+
1003
+ `getCredential` must return deactivated credentials rather than hiding them — the
1004
+ package decides, so that a deactivated credential and an unknown one produce the
1005
+ same answer.
1006
+
1007
+ **There is no no-op fallback.** Omit `biometricRepositoryInstance` and the
1008
+ biometric services are not registered at all, rather than silently accepting
1009
+ enrolments that can never authenticate.
1010
+
861
1011
  ### Brute Force Protection
862
1012
 
863
1013
  Provide a `bruteForceRepositoryInstance` and the module tracks failed login attempts and temporarily locks accounts. Without one, `NoOpBruteForceRepository` is used and no account is ever locked; there is no flag to set. The lockout policy (attempt thresholds, lockout duration) is determined by your `IBruteForceRepository` implementation.
@@ -962,6 +1112,78 @@ async changePassword(
962
1112
 
963
1113
  `issueAuthSession(userId, res?, realm?)` mints a new access + refresh pair, persists the refresh token, and (when `features.cookieAuth` is enabled and `res` is supplied) sets the auth cookies on the response. The caller is responsible for verifying the user's identity — `issueAuthSession` takes a `userId` and trusts it. Pair it only with flows that have already authenticated the user (`changePassword`, MFA enrollment completion, admin-impersonation reissue, etc.). Emits `SecurityEvent.SESSION_ISSUED`.
964
1114
 
1115
+ ### Password Reset
1116
+
1117
+ A reset is two calls: `performRequestPasswordReset` sends a 6-digit code, and
1118
+ `performResetPassword` redeems it. `performChangePassword` is the separate,
1119
+ authenticated path for a signed-in user who knows their current password.
1120
+
1121
+ ```typescript
1122
+ @Mutation(() => PasswordResetResponse)
1123
+ requestPasswordReset(@Args('input') input: RequestPasswordResetInput, @Context() ctx: any) {
1124
+ return this.performRequestPasswordReset(input, ctx);
1125
+ }
1126
+
1127
+ @Mutation(() => PasswordResetResponse)
1128
+ resetPassword(@Args('input') input: ResetPasswordInput, @Context() ctx: any) {
1129
+ return this.performResetPassword(input, ctx);
1130
+ }
1131
+ ```
1132
+
1133
+ It needs `verificationRepositoryInstance` to store the code and
1134
+ `emailServiceInstance` to deliver it. Without the first, codes are logged rather
1135
+ than stored and no reset can complete.
1136
+
1137
+ #### What the package enforces for you
1138
+
1139
+ None of this is configurable, and most of it is invisible until it bites:
1140
+
1141
+ | | |
1142
+ |---|---|
1143
+ | Code | 6 digits, HMAC-SHA256 hashed at rest, never logged |
1144
+ | Expiry | 15 minutes |
1145
+ | Attempts | **3**, then the code is destroyed and a new one must be requested |
1146
+ | Per-user cooldown | 60 seconds, tracked on `passwordResetSentAt` |
1147
+ | Per-IP limit | 5 per hour, checked **before** the user lookup |
1148
+ | On success | **every refresh token for that user is revoked** — all devices are signed out |
1149
+ | Comparison | constant-time |
1150
+
1151
+ The IP limit runs before the lookup deliberately: checking it afterwards would
1152
+ make a request for a non-existent address measurably faster than one for a real
1153
+ address, which is the enumeration leak the generic message exists to prevent.
1154
+
1155
+ #### The response never tells you what happened
1156
+
1157
+ `requestPasswordReset` returns the same success payload whether the address does
1158
+ not exist, belongs to a social-login account, or was actually sent a code. That
1159
+ is deliberate — the mutation is reachable unauthenticated, so any distinguishable
1160
+ result is an oracle for whether an address is registered.
1161
+
1162
+ If you need to know, read your `IAuthLogger`. A skip is reported as
1163
+ `PASSWORD_RESET_REQUESTED` with `result: 'SKIPPED_SOCIAL_ONLY'` and the user ID.
1164
+ An operator tool that creates accounts and invites by reset should assert on that
1165
+ event, not on the mutation's return value.
1166
+
1167
+ #### Accounts with no password
1168
+
1169
+ An account is refused only if it has an actual social identity — `googleId`,
1170
+ `facebookId` or `appleId`. A password-less account with none of those, which is
1171
+ how an operator-provisioned account starts, receives a code and can redeem it.
1172
+ This is the documented "invite by password reset" flow.
1173
+
1174
+ **This changed in 0.12.0.** Before then the test was `passwordHash == null`, so
1175
+ operator-created accounts were skipped silently and could never sign in. See
1176
+ [Migrating to v0.12.0](#migrating-to-v0120) for the consequence worth deciding
1177
+ on. The predicate is exported as `hasSocialIdentity(user)` if you want the same
1178
+ test in your own code.
1179
+
1180
+ #### Token mode
1181
+
1182
+ With `features.verificationMode: 'token'` the email carries a link rather than a
1183
+ code, and `performResetPassword` takes the token lifted from that link in place
1184
+ of the 6-digit code. Everything above still applies. See
1185
+ [Verification Modes](#verification-modes).
1186
+
965
1187
  ### Password Policy
966
1188
 
967
1189
  ```typescript
@@ -1199,7 +1421,40 @@ When `email` is provided, it takes precedence over `emailServiceInstance`.
1199
1421
  |--------|------|---------|-------------|
1200
1422
  | `tokenLength` | `number` | `64` | Token length in bytes (token mode) |
1201
1423
  | `tokenExpiresInMinutes` | `number` | `60` | Token expiration (token mode) |
1202
- | `baseUrl` | `string \| (realm?: string) => string` | — | Absolute http(s) base URL for verification/reset links, or a resolver called with the request's realm. **Required when `verificationMode` is `'token'`** (validated at boot); optional in code mode |
1424
+ | `baseUrl` | `string \| VerificationBaseUrlResolver` | — | Absolute http(s) base URL for verification/reset links, or a resolver called with the request's realm. **Required when `verificationMode` is `'token'`** (validated at boot); optional in code mode |
1425
+
1426
+ ### SendGrid Options (`sendgrid`)
1427
+
1428
+ Read only by the bundled `SendGridEmailService`. A custom `IEmailService` ignores
1429
+ this block entirely.
1430
+
1431
+ | Option | Type | Required | Description |
1432
+ |--------|------|----------|-------------|
1433
+ | `apiKey` | `string` | yes | SendGrid API key |
1434
+ | `fromEmail` | `string` | yes | Verified sender address |
1435
+ | `fromName` | `string` | — | Display name on the From header |
1436
+
1437
+ ### Twilio Options (`twilio`)
1438
+
1439
+ Read only by the bundled `TwilioSmsService`. A custom `ISmsService` ignores this
1440
+ block entirely.
1441
+
1442
+ | Option | Type | Required | Description |
1443
+ |--------|------|----------|-------------|
1444
+ | `accountSid` | `string` | yes | Twilio account SID |
1445
+ | `authToken` | `string` | yes | Twilio auth token |
1446
+ | `phoneNumber` | `string` | yes | Sending number, E.164 |
1447
+
1448
+ ### Biometric Options (`biometric`)
1449
+
1450
+ Read only when `biometricRepositoryInstance` is provided. Passing this block
1451
+ without it is reported at boot, not refused.
1452
+
1453
+ | Option | Type | Default | Description |
1454
+ |--------|------|---------|-------------|
1455
+ | `verifiers` | `IBiometricVerifier[]` | `[new Es256DeviceKeyVerifier()]` | One per credential algorithm. Two claiming the same algorithm is refused at boot — resolution would depend on array order |
1456
+ | `challengeExpirySeconds` | `number` | `60` | Short on purpose: it bounds replay |
1457
+ | `challengeRateLimit` | `{ maxAttempts, windowMs }` | `5 / 60s` | Per credential. **Requires `rateLimiterInstance`** — the same guardrail as `bruteForce.ipRateLimit`, because per-replica counters would make the limit silently looser. A per-IP limit at ten times this bound also applies when you pass `ipAddress` |
1203
1458
 
1204
1459
  ### Optional Instance Options
1205
1460
 
@@ -1343,6 +1598,31 @@ export class CustomGuard implements CanActivate {
1343
1598
  - **API keys with scopes and expiry**, refused hard when they carry your prefix and match no key, so no other strategy accepts them
1344
1599
  - **No raw personal data in package logs** — log lines use the user ID or a salted hash (`emailHash=`, `ipHash=`, under `AUTH_IP_HASH_SALT` or `jwtSecret`), phone numbers keep only their last two digits, and the default `ConsoleAuthLogger` redacts security-event metadata. A custom `authLoggerInstance` receives the raw fields; pass them through `redactSecurityEventMetadata` to store them redacted.
1345
1600
 
1601
+ ## Upgrading from 0.9.x
1602
+
1603
+ Four releases separate 0.9 from 0.14 and each has its own note below. If you are
1604
+ jumping the whole way, this is the order things will bite, and the short answer
1605
+ for each.
1606
+
1607
+ | Release | What you have to do |
1608
+ |---|---|
1609
+ | **0.10.0** | Usually nothing. If you relied on `['api-key','jwt']` rejecting JWTs, it no longer does — an unknown key now fails softly so the next strategy runs |
1610
+ | **0.11.0** | `getRequestFromContext` can return `undefined`; TypeScript flags every call that assumed otherwise. If you use subscriptions with cookie auth, set `csrf.webSocket` |
1611
+ | **0.12.0** | The `jwt` strategy pins HS256 — check anything minting staff tokens elsewhere. Security events are logged once instead of twice, so counts built on them halve. Password reset now reaches password-less accounts with no social identity |
1612
+ | **0.13.0** | Nothing — and it was never published separately; its changes ship inside 0.14.0 |
1613
+ | **0.14.0** | Biometric authentication is rebuilt and breaking. If you never wired `biometricRepositoryInstance`, nothing changes. If you did, read the security note in the changelog first |
1614
+
1615
+ Two things that are easy to miss because nothing errors:
1616
+
1617
+ - **Security-event counts.** 0.12.0 stopped double-logging `SIGNUP_SUCCESS`,
1618
+ `LOGIN_SUCCESS` and `LOGOUT_SUCCESS`. Dashboards built before it were reading
1619
+ double and will step down on upgrade rather than when you fix the data.
1620
+ - **`verification.baseUrl`.** If you run realms and serve more than one frontend,
1621
+ it accepts a resolver from 0.13.0. Before that, every realm got the same host.
1622
+
1623
+ Take them in order rather than jumping straight to 0.14.0 — each note assumes the
1624
+ one before it.
1625
+
1346
1626
  ## Migrating to v0.12.0
1347
1627
 
1348
1628
  **Password reset now reaches accounts that have no password and no social login.** The guard on `requestPasswordReset` and `resetPassword` tested `passwordHash == null` and treated it as "this user signed in with Google". That is also how an operator-provisioned account starts, so an account created by an admin CLI was skipped silently — the request returned the same success message as a real send, and the reset would have been refused even if a code had arrived. Both guards now test for an actual social identity (`googleId`, `facebookId` or `appleId`), so a social-only account behaves exactly as before, and a password-less account with no social identity receives a reset code and can redeem it.
@@ -14,6 +14,7 @@ import { ITenantRepository } from './interfaces/tenant-repository.interface';
14
14
  import { ITenantExtractor } from './interfaces/tenant-extractor.interface';
15
15
  import { IRealmExtractor } from './interfaces/realm-extractor.interface';
16
16
  import { IResourcePermissionRepository } from './interfaces/resource-permission-repository.interface';
17
+ import { IBiometricVerifier } from './interfaces/biometric-verifier.interface';
17
18
  import { IJwtPayloadFactory } from './interfaces/jwt-payload-factory.interface';
18
19
  import { IApiKeyRepository } from './interfaces/api-key-repository.interface';
19
20
  import { PasswordPolicyConfig } from './interfaces/password-policy-config.interface';
@@ -121,6 +122,14 @@ export interface AuthModuleOptions {
121
122
  branding?: EmailBrandingConfig;
122
123
  templateRenderer?: IEmailTemplateRenderer;
123
124
  };
125
+ biometric?: {
126
+ verifiers?: IBiometricVerifier[];
127
+ challengeExpirySeconds?: number;
128
+ challengeRateLimit?: {
129
+ maxAttempts: number;
130
+ windowMs: number;
131
+ };
132
+ };
124
133
  verification?: {
125
134
  tokenLength?: number;
126
135
  tokenExpiresInMinutes?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../src/auth.module.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../src/auth.module.ts"],"names":[],"mappings":"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"}