@ambushsoftworks/nestjs-auth-graphql 0.11.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.
- package/CHANGELOG.md +300 -0
- package/README.md +411 -3
- package/dist/auth.module.d.ts +15 -1
- package/dist/auth.module.d.ts.map +1 -1
- package/dist/auth.module.js +114 -16
- package/dist/auth.module.js.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -1
- package/dist/constants.js.map +1 -1
- package/dist/decorators/require-scopes.decorator.d.ts +2 -0
- package/dist/decorators/require-scopes.decorator.d.ts.map +1 -0
- package/dist/decorators/require-scopes.decorator.js +8 -0
- package/dist/decorators/require-scopes.decorator.js.map +1 -0
- package/dist/guards/csrf.guard.d.ts +2 -0
- package/dist/guards/csrf.guard.d.ts.map +1 -1
- package/dist/guards/csrf.guard.js +9 -2
- package/dist/guards/csrf.guard.js.map +1 -1
- package/dist/guards/scope.guard.d.ts +8 -0
- package/dist/guards/scope.guard.d.ts.map +1 -0
- package/dist/guards/scope.guard.js +53 -0
- package/dist/guards/scope.guard.js.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/interfaces/api-key-repository.interface.d.ts +3 -0
- package/dist/interfaces/api-key-repository.interface.d.ts.map +1 -1
- package/dist/interfaces/biometric-repository.interface.d.ts +29 -17
- package/dist/interfaces/biometric-repository.interface.d.ts.map +1 -1
- package/dist/interfaces/biometric-verifier.interface.d.ts +10 -0
- package/dist/interfaces/biometric-verifier.interface.d.ts.map +1 -0
- package/dist/interfaces/{magic-link-repository.interface.js → biometric-verifier.interface.js} +1 -1
- package/dist/interfaces/biometric-verifier.interface.js.map +1 -0
- package/dist/resolvers/base-auth.resolver.d.ts.map +1 -1
- package/dist/resolvers/base-auth.resolver.js +0 -15
- package/dist/resolvers/base-auth.resolver.js.map +1 -1
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +10 -3
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/biometric-auth.service.d.ts +47 -17
- package/dist/services/biometric-auth.service.d.ts.map +1 -1
- package/dist/services/biometric-auth.service.js +109 -68
- package/dist/services/biometric-auth.service.js.map +1 -1
- package/dist/services/biometric-challenge.service.d.ts +27 -0
- package/dist/services/biometric-challenge.service.d.ts.map +1 -0
- package/dist/services/biometric-challenge.service.js +116 -0
- package/dist/services/biometric-challenge.service.js.map +1 -0
- package/dist/services/biometric-verification.service.d.ts.map +1 -1
- package/dist/services/biometric-verification.service.js +4 -0
- package/dist/services/biometric-verification.service.js.map +1 -1
- package/dist/services/verification.service.d.ts +2 -1
- package/dist/services/verification.service.d.ts.map +1 -1
- package/dist/services/verification.service.js +38 -8
- package/dist/services/verification.service.js.map +1 -1
- package/dist/strategies/api-key.strategy.d.ts +6 -1
- package/dist/strategies/api-key.strategy.d.ts.map +1 -1
- package/dist/strategies/api-key.strategy.js +40 -6
- package/dist/strategies/api-key.strategy.js.map +1 -1
- package/dist/strategies/external-jwt.strategy.d.ts +19 -0
- package/dist/strategies/external-jwt.strategy.d.ts.map +1 -0
- package/dist/strategies/external-jwt.strategy.js +124 -0
- package/dist/strategies/external-jwt.strategy.js.map +1 -0
- package/dist/strategies/jwt.strategy.d.ts.map +1 -1
- package/dist/strategies/jwt.strategy.js +1 -0
- package/dist/strategies/jwt.strategy.js.map +1 -1
- package/dist/test-utils/mock-repositories.d.ts.map +1 -1
- package/dist/test-utils/mock-repositories.js +8 -7
- package/dist/test-utils/mock-repositories.js.map +1 -1
- package/dist/utils/provider-helpers.d.ts +6 -0
- package/dist/utils/provider-helpers.d.ts.map +1 -1
- package/dist/utils/provider-helpers.js +4 -0
- package/dist/utils/provider-helpers.js.map +1 -1
- package/dist/utils/verification-base-url.d.ts +2 -0
- package/dist/utils/verification-base-url.d.ts.map +1 -0
- package/dist/utils/verification-base-url.js +19 -0
- package/dist/utils/verification-base-url.js.map +1 -0
- package/dist/verifiers/es256-device-key.verifier.d.ts +14 -0
- package/dist/verifiers/es256-device-key.verifier.d.ts.map +1 -0
- package/dist/verifiers/es256-device-key.verifier.js +42 -0
- package/dist/verifiers/es256-device-key.verifier.js.map +1 -0
- package/package.json +8 -2
- package/dist/interfaces/magic-link-repository.interface.d.ts +0 -6
- package/dist/interfaces/magic-link-repository.interface.d.ts.map +0 -1
- package/dist/interfaces/magic-link-repository.interface.js.map +0 -1
- package/dist/interfaces/password-reset-strategy.interface.d.ts +0 -7
- package/dist/interfaces/password-reset-strategy.interface.d.ts.map +0 -1
- package/dist/interfaces/password-reset-strategy.interface.js +0 -3
- package/dist/interfaces/password-reset-strategy.interface.js.map +0 -1
- package/dist/repositories/noop-biometric.repository.d.ts +0 -26
- package/dist/repositories/noop-biometric.repository.d.ts.map +0 -1
- package/dist/repositories/noop-biometric.repository.js +0 -56
- package/dist/repositories/noop-biometric.repository.js.map +0 -1
- package/dist/repositories/noop-magic-link.repository.d.ts +0 -9
- package/dist/repositories/noop-magic-link.repository.d.ts.map +0 -1
- package/dist/repositories/noop-magic-link.repository.js +0 -37
- package/dist/repositories/noop-magic-link.repository.js.map +0 -1
- package/dist/strategies/magic-link.strategy.d.ts +0 -16
- package/dist/strategies/magic-link.strategy.d.ts.map +0 -1
- package/dist/strategies/magic-link.strategy.js +0 -80
- package/dist/strategies/magic-link.strategy.js.map +0 -1
- package/dist/strategies/verification-code.strategy.d.ts +0 -11
- package/dist/strategies/verification-code.strategy.d.ts.map +0 -1
- package/dist/strategies/verification-code.strategy.js +0 -44
- package/dist/strategies/verification-code.strategy.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -27,6 +27,306 @@ 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
|
+
|
|
193
|
+
## [0.12.0] - 2026-09-20
|
|
194
|
+
|
|
195
|
+
Ambush Desk asked for two things: verifying JWTs that other systems sign, and API
|
|
196
|
+
keys with scopes, expiry and prefixes. They also reported duplicate security
|
|
197
|
+
events, and checking that turned up a false one. Jobsites then filed two more:
|
|
198
|
+
a verification link that cannot follow a realm, and a password reset that
|
|
199
|
+
silently skips accounts it should not. See **Fixed**.
|
|
200
|
+
|
|
201
|
+
### Fixed
|
|
202
|
+
- **Every signup, login and logout was logged twice.** `BaseAuthResolver` logged
|
|
203
|
+
`SIGNUP_SUCCESS`, `LOGIN_SUCCESS` and `LOGOUT_SUCCESS` after `AuthService` had
|
|
204
|
+
already logged each, so an `IAuthLogger` writing to an audit table stored two
|
|
205
|
+
rows per operation. `AuthService` keeps its copies: they also cover flows that
|
|
206
|
+
never reach the resolver. Reported by Ambush Desk, and reproduced end to end
|
|
207
|
+
before the fix.
|
|
208
|
+
- **A signup refused by `features.preventEnumerationOnSignup` logged
|
|
209
|
+
`SIGNUP_SUCCESS`** for an account that was never created, carrying the
|
|
210
|
+
synthetic response's random user ID. The resolver logged it without being able
|
|
211
|
+
to tell the signup apart from a real one; `AuthService`, which can, does not.
|
|
212
|
+
- **A password reset for an account with no password sent nothing, silently.**
|
|
213
|
+
Both `requestPasswordReset` and `resetPassword` tested `passwordHash == null`
|
|
214
|
+
and read it as "this user signed in with Google". It is also how an
|
|
215
|
+
operator-provisioned account starts, so an account created by an admin CLI
|
|
216
|
+
got no email — and the request returned the same generic success as a real
|
|
217
|
+
send, so the operator saw success for an account nobody could ever sign into.
|
|
218
|
+
Both guards now test for an actual social identity via the exported
|
|
219
|
+
`hasSocialIdentity(user)` (`googleId`, `facebookId` or `appleId`). A
|
|
220
|
+
social-only account behaves exactly as before. **The two guards have to agree**
|
|
221
|
+
— correcting only the first would email a credential the second refuses, which
|
|
222
|
+
is worse than the silence. A skip is now reported through `IAuthLogger` as
|
|
223
|
+
`PASSWORD_RESET_REQUESTED` with `result: 'SKIPPED_SOCIAL_ONLY'`; the public
|
|
224
|
+
response is deliberately unchanged, since `performRequestPasswordReset` is
|
|
225
|
+
reachable unauthenticated and a distinguishable return value would be an
|
|
226
|
+
enumeration oracle. Reported by jobsites.
|
|
227
|
+
- Consequence worth deciding on rather than discovering: a half-finished
|
|
228
|
+
signup row with no password and no social identity can now be claimed by
|
|
229
|
+
whoever controls the address. Previously it was unreachable, which is not
|
|
230
|
+
the same as safe.
|
|
231
|
+
|
|
232
|
+
### Added
|
|
233
|
+
- **`createExternalJwtStrategy(name, options | { inject, useFactory })`**: a
|
|
234
|
+
Passport strategy for JWTs another system signs, with
|
|
235
|
+
`secretProvider(kid, request)`, required `algorithms`, `issuer`, `audience`,
|
|
236
|
+
`clockToleranceSec`, `maxTokenBytes` (default 8192), `mapClaims` and
|
|
237
|
+
`jwtFromRequest`. Separate from the package's `jwt` strategy: no realm check
|
|
238
|
+
and no user lookup. Every rejection fails softly, so it composes in
|
|
239
|
+
`createAuthGuard`. Requested by Ambush Desk.
|
|
240
|
+
- Unsafe configuration fails boot: missing or empty `algorithms`, `none`, an
|
|
241
|
+
algorithm jsonwebtoken does not know, HMAC mixed with asymmetric families
|
|
242
|
+
(which lets a public key be used as an HMAC secret), or one of the package's
|
|
243
|
+
own strategy names.
|
|
244
|
+
- The `{ inject, useFactory }` form wires constructor injection, so
|
|
245
|
+
`secretProvider` can use application services without a subclass.
|
|
246
|
+
- **API key scopes**: `@RequireScopes(...)` and `ScopeGuard`, with
|
|
247
|
+
`IApiKeyAccount.scopes?: string[]`. A key needs every listed scope, and
|
|
248
|
+
otherwise gets 403 `INSUFFICIENT_SCOPE` naming what the operation requires. A
|
|
249
|
+
key with no `scopes` has none. Signed-in people pass, since their access is
|
|
250
|
+
`PermissionGuard`'s job. Requested by Ambush Desk.
|
|
251
|
+
- **API key expiry**: `IApiKeyAccount.expiresAt?: Date | null`. A key past it is
|
|
252
|
+
refused with `401 API key has expired`. Requested by Ambush Desk.
|
|
253
|
+
- **`apiKey.headerName`**, such as `'X-API-Key'`, read before
|
|
254
|
+
`Authorization: Bearer`, which keeps working. `CsrfGuard` treats a request
|
|
255
|
+
carrying it as credentialed, as it already does for `Authorization`. Requested
|
|
256
|
+
by Ambush Desk.
|
|
257
|
+
- **`apiKey.prefix`**, such as `'ait_'`. A token without it is not an API key and
|
|
258
|
+
goes to the next strategy with no lookup; one with it that matches no key is
|
|
259
|
+
refused with `401 Invalid API key`, since it cannot be a JWT. That replaces the
|
|
260
|
+
requested `onUnknownKey: 'fail' | 'error'` switch. Requested by Ambush Desk.
|
|
261
|
+
- **`IApiKeyRepository.touchLastUsed?(id)`**, called once per successful match
|
|
262
|
+
and never for a refused key. It is not awaited, and a failure is logged rather
|
|
263
|
+
than failing the request. Requested by Ambush Desk.
|
|
264
|
+
- **`verification.baseUrl` accepts a resolver**: `string | ((realm?: string) =>
|
|
265
|
+
string)`. One deployment serving several brands needs a link that opens on the
|
|
266
|
+
site the user came from; the realm already reached the credential and was
|
|
267
|
+
dropped one line before the URL. It applies to both verification modes, since
|
|
268
|
+
the token link and the code page link are built from the same resolved base.
|
|
269
|
+
Purely additive — a string behaves exactly as before. Requested by jobsites.
|
|
270
|
+
- Called once per link and synchronous: resolve from a map, not a query.
|
|
271
|
+
- Must return a URL for `realm === undefined` too — a request exempted by
|
|
272
|
+
`realm.skip`, or a single-realm deployment, has no realm.
|
|
273
|
+
- Boot validation is necessarily weaker for a resolver: it is probed once with
|
|
274
|
+
`undefined`, because there is no request at startup to supply a realm. A
|
|
275
|
+
per-realm value is checked when the link is built, where token mode throws
|
|
276
|
+
(the link carries the only copy of the credential) and code mode warns and
|
|
277
|
+
sends without a link (the code is in the body).
|
|
278
|
+
|
|
279
|
+
### Changed
|
|
280
|
+
- **The `jwt` strategy pins `algorithms: ['HS256']`**, the algorithm the module's
|
|
281
|
+
`JwtModule` signs with. Unpinned, jsonwebtoken 9 also accepted HS384 and HS512
|
|
282
|
+
tokens signed with `jwtSecret`. Tokens this package issues are unaffected.
|
|
283
|
+
- **`ApiKeyStrategy`'s constructor takes the `apiKey` options** as an optional
|
|
284
|
+
second argument. This matters only if you construct the strategy yourself.
|
|
285
|
+
|
|
286
|
+
### Declined
|
|
287
|
+
- **Publishable, origin-restricted API keys.** A key an app embeds is public by
|
|
288
|
+
construction: it identifies an app rather than authenticating anyone, and
|
|
289
|
+
`Origin` is set freely by any non-browser client, so restricting it is abuse
|
|
290
|
+
control rather than authentication. Keeping them out means every credential
|
|
291
|
+
this package accepts is a secret one. Requested by Ambush Desk, which keeps
|
|
292
|
+
ingest keys as its own concept.
|
|
293
|
+
|
|
294
|
+
### Documentation
|
|
295
|
+
- README: **External JWTs**; **API Key Authentication** extended with the prefix,
|
|
296
|
+
header, expiry, scopes and last-used tracking; `ScopeGuard` and
|
|
297
|
+
`@RequireScopes` in the guard and decorator tables; the new options; and
|
|
298
|
+
**Migrating to v0.12.0**.
|
|
299
|
+
- CLAUDE.md: **API Keys, Scopes and External JWTs**, and the rule that each
|
|
300
|
+
security event is logged once, by the layer that performs the action.
|
|
301
|
+
- README: **One API, several brands: `baseUrl` per realm**, the widened
|
|
302
|
+
`verification.baseUrl` row, and both new **Migrating to v0.12.0** notes.
|
|
303
|
+
- CLAUDE.md: base URL resolution is realm-aware through one seam, with the
|
|
304
|
+
warning not to answer it by handing the raw token to the email renderer; and
|
|
305
|
+
the rule that password reset tests for a social identity, never for a missing
|
|
306
|
+
password hash.
|
|
307
|
+
|
|
308
|
+
### Testing
|
|
309
|
+
- **End-to-end** (`test/e2e/tokens-and-keys.e2e-spec.ts`), on both NestJS majors:
|
|
310
|
+
a customer token through `createAuthGuard(['customer-jwt'])` on an HTTP route
|
|
311
|
+
and in a resolver, both guard orders with staff tokens, and refusals for
|
|
312
|
+
another algorithm, an unknown key ID, another audience and an expired token.
|
|
313
|
+
For API keys: a staff JWT accepted with a prefix set, a key in `X-API-Key` with
|
|
314
|
+
its use recorded, an unknown prefixed key, an expired key, `@RequireScopes`
|
|
315
|
+
against keys and people, and the CSRF skip.
|
|
316
|
+
- **Signup, login and logout now run end to end through `BaseAuthResolver`**,
|
|
317
|
+
counting the events each produces, including a signup refused by
|
|
318
|
+
`preventEnumerationOnSignup`.
|
|
319
|
+
- **Per-realm links**, in `auth.service.verification-mode.spec.ts` against a real
|
|
320
|
+
`VerificationService`, and end to end in `auth-module.e2e-spec.ts` — the realm
|
|
321
|
+
is attached by `RealmMiddleware`, so only an assembled app proves it survives
|
|
322
|
+
the whole path from header to emailed link. Each test parses the rendered URL
|
|
323
|
+
and feeds the credential back through `resetPassword`, rather than asserting
|
|
324
|
+
that a send happened.
|
|
325
|
+
- **43 mutation checks**, each reintroducing one defect: every one failed a test.
|
|
326
|
+
The five added here include reverting each password-reset guard separately —
|
|
327
|
+
reverting only `resetPassword` is caught by the close-the-loop test, which is
|
|
328
|
+
the half-fix worth guarding against.
|
|
329
|
+
|
|
30
330
|
## [0.11.0] - 2026-09-15
|
|
31
331
|
|
|
32
332
|
Ambush Desk asked for two things: refresh retries that survive a second API
|