@ambushsoftworks/nestjs-auth-graphql 0.9.1 → 0.11.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 +231 -0
- package/README.md +164 -8
- package/dist/auth.module.d.ts +10 -0
- package/dist/auth.module.d.ts.map +1 -1
- package/dist/auth.module.js +69 -2
- package/dist/auth.module.js.map +1 -1
- package/dist/exceptions/oauth.exceptions.d.ts +4 -1
- package/dist/exceptions/oauth.exceptions.d.ts.map +1 -1
- package/dist/exceptions/oauth.exceptions.js +12 -1
- package/dist/exceptions/oauth.exceptions.js.map +1 -1
- package/dist/guards/create-auth-guard.d.ts.map +1 -1
- package/dist/guards/create-auth-guard.js +3 -6
- package/dist/guards/create-auth-guard.js.map +1 -1
- 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 +26 -5
- package/dist/guards/csrf.guard.js.map +1 -1
- package/dist/guards/jwt-auth.guard.d.ts +2 -1
- package/dist/guards/jwt-auth.guard.d.ts.map +1 -1
- package/dist/guards/jwt-auth.guard.js +6 -3
- package/dist/guards/jwt-auth.guard.js.map +1 -1
- package/dist/guards/permission.guard.js +2 -2
- package/dist/guards/permission.guard.js.map +1 -1
- package/dist/guards/tenant.guard.d.ts.map +1 -1
- package/dist/guards/tenant.guard.js +3 -0
- package/dist/guards/tenant.guard.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/refresh-retry-cache.interface.d.ts +6 -0
- package/dist/interfaces/refresh-retry-cache.interface.d.ts.map +1 -0
- package/dist/interfaces/refresh-retry-cache.interface.js +3 -0
- package/dist/interfaces/refresh-retry-cache.interface.js.map +1 -0
- package/dist/interfaces/refresh-token-repository.interface.d.ts.map +1 -1
- package/dist/middleware/realm.middleware.d.ts +7 -2
- package/dist/middleware/realm.middleware.d.ts.map +1 -1
- package/dist/middleware/realm.middleware.js +60 -14
- package/dist/middleware/realm.middleware.js.map +1 -1
- package/dist/resolvers/base-auth.resolver.d.ts.map +1 -1
- package/dist/resolvers/base-auth.resolver.js +2 -1
- package/dist/resolvers/base-auth.resolver.js.map +1 -1
- package/dist/resolvers/oauth.controller.d.ts.map +1 -1
- package/dist/resolvers/oauth.controller.js +27 -17
- package/dist/resolvers/oauth.controller.js.map +1 -1
- package/dist/resolvers/rest-error.interceptor.d.ts +7 -0
- package/dist/resolvers/rest-error.interceptor.d.ts.map +1 -0
- package/dist/resolvers/rest-error.interceptor.js +36 -0
- package/dist/resolvers/rest-error.interceptor.js.map +1 -0
- package/dist/services/auth.service.d.ts +2 -3
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +15 -10
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/in-memory-refresh-retry-cache.d.ts +11 -0
- package/dist/services/in-memory-refresh-retry-cache.d.ts.map +1 -0
- package/dist/services/in-memory-refresh-retry-cache.js +45 -0
- package/dist/services/in-memory-refresh-retry-cache.js.map +1 -0
- package/dist/services/refresh-token.service.d.ts +13 -14
- package/dist/services/refresh-token.service.d.ts.map +1 -1
- package/dist/services/refresh-token.service.js +112 -33
- package/dist/services/refresh-token.service.js.map +1 -1
- package/dist/strategies/api-key.strategy.d.ts.map +1 -1
- package/dist/strategies/api-key.strategy.js +1 -1
- package/dist/strategies/api-key.strategy.js.map +1 -1
- package/dist/strategies/jwt.strategy.d.ts +3 -1
- package/dist/strategies/jwt.strategy.d.ts.map +1 -1
- package/dist/strategies/jwt.strategy.js +15 -11
- package/dist/strategies/jwt.strategy.js.map +1 -1
- package/dist/strategies/noop-facebook.strategy.d.ts.map +1 -1
- package/dist/strategies/noop-facebook.strategy.js +2 -1
- package/dist/strategies/noop-facebook.strategy.js.map +1 -1
- package/dist/strategies/noop-google.strategy.d.ts.map +1 -1
- package/dist/strategies/noop-google.strategy.js +2 -1
- package/dist/strategies/noop-google.strategy.js.map +1 -1
- package/dist/test-utils/execution-contexts.d.ts +7 -0
- package/dist/test-utils/execution-contexts.d.ts.map +1 -0
- package/dist/test-utils/execution-contexts.js +41 -0
- package/dist/test-utils/execution-contexts.js.map +1 -0
- package/dist/utils/cookies.d.ts +2 -0
- package/dist/utils/cookies.d.ts.map +1 -0
- package/dist/utils/cookies.js +34 -0
- package/dist/utils/cookies.js.map +1 -0
- package/dist/utils/execution-context.d.ts +3 -1
- package/dist/utils/execution-context.d.ts.map +1 -1
- package/dist/utils/execution-context.js +39 -4
- package/dist/utils/execution-context.js.map +1 -1
- package/dist/utils/request-realm.d.ts +2 -0
- package/dist/utils/request-realm.d.ts.map +1 -0
- package/dist/utils/request-realm.js +12 -0
- package/dist/utils/request-realm.js.map +1 -0
- package/package.json +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -27,6 +27,237 @@ 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.11.0] - 2026-09-15
|
|
31
|
+
|
|
32
|
+
Ambush Desk asked for two things: refresh retries that survive a second API
|
|
33
|
+
instance, and guards that work on GraphQL subscriptions. Checking the first one
|
|
34
|
+
turned up a retry bug that had been there since the first release. See
|
|
35
|
+
**Fixed**.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- **Guards failed on GraphQL subscriptions over `graphql-ws`.** Guards read
|
|
39
|
+
`context.req`, which a subscription does not have. With `GraphQLModule`'s
|
|
40
|
+
`subscriptions: { 'graphql-ws': true }`, Nest puts graphql-ws's connection
|
|
41
|
+
object there instead. With an app's own `useServer`, there is no `req` at all.
|
|
42
|
+
Run over a real `graphql-ws` client on 0.10.0, the guard chain
|
|
43
|
+
`CsrfGuard` → `createAuthGuard(['jwt'])` → `TenantGuard` → `PermissionGuard`
|
|
44
|
+
refused every subscription:
|
|
45
|
+
- with `GraphQLModule` subscriptions, `CsrfGuard` answered `CSRF validation failed`
|
|
46
|
+
- with an own server, it threw `TypeError`s: `Cannot read properties of
|
|
47
|
+
undefined (reading 'headers')` with a context function, `(reading 'req')`
|
|
48
|
+
without one
|
|
49
|
+
|
|
50
|
+
Guards now read the WebSocket upgrade request. Authentication, realm, tenant
|
|
51
|
+
and permission checks all work on subscriptions, and with no request to read,
|
|
52
|
+
guards answer 401 or 403. Reported by Ambush Desk; Ariadne had been working
|
|
53
|
+
around it with a subclass.
|
|
54
|
+
- **A retried token refresh was always rejected when the repository deletes
|
|
55
|
+
used tokens.** Refresh deletes the token it used and keeps the result for 10
|
|
56
|
+
seconds, so that a client that lost the response can retry. The retry then
|
|
57
|
+
looked up the used token to find the user, found nothing, and answered
|
|
58
|
+
`401 Invalid refresh token`. That covers every `IRefreshTokenRepository` whose
|
|
59
|
+
`delete` removes the row, which is what the method is for. The lookup has been
|
|
60
|
+
there since `v0.1.3`. The kept result now carries the user ID, and the retry
|
|
61
|
+
still re-checks account status. Reproduced on 0.10.0 before the fix, with a
|
|
62
|
+
repository that deletes.
|
|
63
|
+
- **A retry arriving between deleting the used token and keeping the result was
|
|
64
|
+
rejected.** The result is now kept first.
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
- **`csrf.webSocket: 'reject' | 'skip'`**, default `'reject'`. Browsers cannot
|
|
68
|
+
send the CSRF header on a WebSocket, so this decides for cookie-authenticated
|
|
69
|
+
subscriptions: 403, or allow. `'skip'` is safe only with an `Origin` check
|
|
70
|
+
when the socket connects. A subscription whose upgrade request carries an
|
|
71
|
+
`Authorization` header is allowed either way. Any other value fails boot.
|
|
72
|
+
Requested by Ambush Desk.
|
|
73
|
+
- **The realm on subscriptions.** `RealmMiddleware` never sees a WebSocket
|
|
74
|
+
upgrade, so `JwtStrategy` resolves the realm from the upgrade request itself,
|
|
75
|
+
once per connection, with the same `realm.skip` and extractor rules. A realm
|
|
76
|
+
the app sets itself (in `onConnect`, say) is kept. Requested by Ambush Desk.
|
|
77
|
+
- **`isWebSocketContext(context)`, `requireRequestFromContext(context)` and
|
|
78
|
+
`readCookie(request, name)`**, exported. `readCookie` falls back to parsing
|
|
79
|
+
the `Cookie` header, which is all an upgrade request has.
|
|
80
|
+
- **`refreshRetryCacheInstance?: IRefreshRetryCache`.** A retry that reached a
|
|
81
|
+
different instance than the first refresh got `401` and a
|
|
82
|
+
`TOKEN_REUSE_DETECTED` event, because the retry cache lived in one process.
|
|
83
|
+
Requested by Ambush Desk.
|
|
84
|
+
- Implement `get`, `set(key, value, ttlMs)` and `delete` over a store the
|
|
85
|
+
instances share. The interface's JSDoc has a Postgres example.
|
|
86
|
+
- Entries hold live tokens, so for any store but the in-memory one they are
|
|
87
|
+
AES-256-GCM encrypted with a key derived from `encryptionKey`, which then
|
|
88
|
+
becomes required and must match on every instance.
|
|
89
|
+
- The package enforces expiry itself, and treats store errors as a miss.
|
|
90
|
+
- **`refreshGracePeriodSeconds`**, default 10. `0` turns retries off. Requested
|
|
91
|
+
by Ambush Desk.
|
|
92
|
+
- **`InMemoryRefreshRetryCache`**, the default, exported so a single-instance app
|
|
93
|
+
can pass it explicitly.
|
|
94
|
+
- **Boot warnings for in-memory defaults** that only work within one instance:
|
|
95
|
+
`rateLimiterInstance` (password-reset throttling) and
|
|
96
|
+
`refreshRetryCacheInstance`. Pass `new InMemoryRateLimiterService()` and
|
|
97
|
+
`new InMemoryRefreshRetryCache()` to confirm a single instance and silence
|
|
98
|
+
them. Requested by Ambush Desk.
|
|
99
|
+
|
|
100
|
+
### Changed
|
|
101
|
+
- **`getRequestFromContext` returns `Record<string, any> | undefined`.** It finds a
|
|
102
|
+
subscription's upgrade request, and returns `undefined` when a GraphQL context
|
|
103
|
+
holds no request. It did before too, while typed as never doing so. TypeScript
|
|
104
|
+
now flags call sites that assume a request.
|
|
105
|
+
- **Cookie auth no longer requires cookie-parser.** The JWT cookie extractor and
|
|
106
|
+
`readRefreshTokenFromCookie` fall back to the `Cookie` header.
|
|
107
|
+
- **`CsrfGuard` checks `exemptOperations` before reading any header**, and
|
|
108
|
+
answers 403 rather than throwing when a GraphQL operation's context holds no
|
|
109
|
+
request. HTTP outcomes are unchanged.
|
|
110
|
+
- **`RefreshTokenService`'s retry cache methods are async and take the user ID:**
|
|
111
|
+
- `cacheRefreshResult(usedTokenHash, { userId, accessToken, refreshToken })`
|
|
112
|
+
- `getCachedRefreshResult(usedTokenHash)`, which resolves to that shape or `null`
|
|
113
|
+
- `invalidateCachedRefresh(usedTokenHash)`
|
|
114
|
+
|
|
115
|
+
The class no longer runs a cleanup timer, and `onModuleDestroy` is gone. This
|
|
116
|
+
only affects code that calls these methods directly or subclasses the service.
|
|
117
|
+
Implement `IRefreshRetryCache` instead.
|
|
118
|
+
|
|
119
|
+
### Documentation
|
|
120
|
+
- **CLAUDE.md described a `gracePeriodSeconds` option that never existed**, and a
|
|
121
|
+
Redis subclass of `RefreshTokenService` that could not work. It overrode the
|
|
122
|
+
synchronous cache methods with async ones, and the truthy Promise would have
|
|
123
|
+
sent every refresh down the cached branch. Both are replaced by the port's real
|
|
124
|
+
design.
|
|
125
|
+
- README: **Refresh Retries**, **GraphQL Subscriptions**, **Migrating to
|
|
126
|
+
v0.11.0**, and rows for the new options and utilities.
|
|
127
|
+
|
|
128
|
+
### Testing
|
|
129
|
+
- **End-to-end subscriptions.** The suite drives real `graphql-ws` clients
|
|
130
|
+
against three server setups on both NestJS majors: `GraphQLModule`-managed, an
|
|
131
|
+
own `useServer` with a context, and one without. Against each, it checks
|
|
132
|
+
delivery, a missing cookie, a wrong realm, a missing permission, another
|
|
133
|
+
tenant, CSRF `'reject'` and `'skip'`, and a bearer token on the upgrade.
|
|
134
|
+
- **Refresh retries end to end:**
|
|
135
|
+
- a retry gets the same pair
|
|
136
|
+
- two instances sharing a store
|
|
137
|
+
- without a shared store: `TOKEN_REUSE_DETECTED`
|
|
138
|
+
- after the grace period: 401
|
|
139
|
+
- a user suspended between the refresh and its retry is refused
|
|
140
|
+
|
|
141
|
+
## [0.10.0] - 2026-09-15
|
|
142
|
+
|
|
143
|
+
Ambush Desk, a new consumer on NestJS 11, asked for routes that need no realm, a
|
|
144
|
+
way to leave the OAuth REST routes unmounted, and CI on NestJS 11. Building those
|
|
145
|
+
turned up defects on the OAuth REST routes that affect every app mounting them
|
|
146
|
+
today. See **Fixed** first.
|
|
147
|
+
|
|
148
|
+
Behaviour changes, so the minor version moves. See **Migrating to v0.10.0** in
|
|
149
|
+
the README.
|
|
150
|
+
|
|
151
|
+
### Fixed
|
|
152
|
+
- **The OAuth REST routes answered 500 for four ordinary failures.** On
|
|
153
|
+
`POST /auth/google/token` and `POST /auth/facebook/token`:
|
|
154
|
+
|
|
155
|
+
| Situation | Before | Now |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| Provider not configured | 500 | 404 `OAUTH_PROVIDER_NOT_CONFIGURED`, with `provider` |
|
|
158
|
+
| Account locked | 500 | 403 `ACCOUNT_LOCKED`, with `remainingLockoutTime` |
|
|
159
|
+
| Account suspended or deleted | 500 | 403 `ACCOUNT_INACTIVE` |
|
|
160
|
+
| Client IP over `bruteForce.ipRateLimit` | 500 | 429 `RATE_LIMIT_EXCEEDED` |
|
|
161
|
+
|
|
162
|
+
The no-op strategies and the controller's lockout and rate-limit checks threw
|
|
163
|
+
plain `Error`. `AccountLockedException` and `AccountInactiveException` extend
|
|
164
|
+
`GraphQLError`, which Nest's HTTP layer does not recognise, so since 0.9.0 a
|
|
165
|
+
suspended user signing in with Google also got a 500. The bodies now use the
|
|
166
|
+
root-level shape of the other OAuth errors: `message`, `code`, `statusCode`,
|
|
167
|
+
plus fields. An internal `RestErrorInterceptor` converts the GraphQL-shaped
|
|
168
|
+
errors on these routes only; GraphQL responses are unchanged. The lockout
|
|
169
|
+
message is now the one `login` uses.
|
|
170
|
+
- **`createAuthGuard(['api-key', 'jwt'])` rejected every JWT.** `ApiKeyStrategy`
|
|
171
|
+
threw for a bearer token that matched no key, and a throw stops Passport's
|
|
172
|
+
strategy chain, so JWT was never tried. That is the order the README and
|
|
173
|
+
`createAuthGuard`'s JSDoc showed. An unknown token now fails softly and the next
|
|
174
|
+
strategy runs, so either order accepts either credential. A key whose account is
|
|
175
|
+
inactive still fails with 401 `API key is inactive`. Reported by Ambush Desk.
|
|
176
|
+
- **The OAuth routes checked lockout and the IP rate limit, and reset failed
|
|
177
|
+
attempts, without the realm.** With realms enabled, `isAccountLocked`,
|
|
178
|
+
`getRemainingLockoutTime`, `checkIpRateLimit` and `resetFailedAttempts` were
|
|
179
|
+
called with no realm. Depending on how your repository treats a missing realm,
|
|
180
|
+
a lock could apply to the same email in another realm, and a successful sign-in
|
|
181
|
+
could clear another realm's failed attempts. They now receive the request's
|
|
182
|
+
realm, as `login` already did.
|
|
183
|
+
|
|
184
|
+
### Added
|
|
185
|
+
- **`realm.skip: (path, request) => boolean`.** Requests that have no realm by
|
|
186
|
+
nature, such as health checks, webhooks and public downloads, pass
|
|
187
|
+
`RealmMiddleware` without calling the extractor, get no `_realm`, and never
|
|
188
|
+
answer `400 Realm required`. Requested by Ambush Desk.
|
|
189
|
+
- `path` is the requested path, including any global prefix and without the
|
|
190
|
+
query string. Do not match on `request.path` or `request.url`: Express
|
|
191
|
+
rewrites both to `/` inside middleware.
|
|
192
|
+
- Only a synchronous `true` skips. A throw or any other return value fails
|
|
193
|
+
closed and is logged.
|
|
194
|
+
- It is a predicate rather than route patterns because the same Nest route
|
|
195
|
+
pattern matches different paths on NestJS 10 and 11: `hooks/*` excludes
|
|
196
|
+
`/hooks/glitchtip/issue` on 11 but not on 10.
|
|
197
|
+
- With realms enabled, a `skip` that is not a function fails boot with
|
|
198
|
+
`InvalidAuthConfigException`. `skip` without `realmExtractorInstance` logs a
|
|
199
|
+
warning.
|
|
200
|
+
- This closes the `@SkipRealm()` follow-up for requests with no realm. A
|
|
201
|
+
decorator cannot work, because middleware runs before Nest resolves the
|
|
202
|
+
handler.
|
|
203
|
+
- **`oauthController: false`** on `AuthModule.forRootAsync`, beside `useFactory`,
|
|
204
|
+
leaves `OAuthController` unmounted, so both routes answer 404 like any unknown
|
|
205
|
+
path. Defaults to `true`. Requested by Ambush Desk.
|
|
206
|
+
- `OAuthProviderNotConfiguredException` (404, `OAUTH_PROVIDER_NOT_CONFIGURED`),
|
|
207
|
+
thrown by the no-op strategies.
|
|
208
|
+
- `AuthService.realmsEnabled`.
|
|
209
|
+
|
|
210
|
+
### Changed
|
|
211
|
+
- **Realm-scoped entry points refuse a request without a realm.** With realms
|
|
212
|
+
enabled, `BaseAuthResolver` flows (through `getRealmFromContext`) and the OAuth
|
|
213
|
+
routes answer `400 Realm required` when the request has no realm, instead of
|
|
214
|
+
running with `realm` undefined, which many repositories read as "any realm".
|
|
215
|
+
Before `realm.skip` the middleware ruled this out over HTTP; now it also
|
|
216
|
+
contains a `skip` predicate that matches more than intended. Code that calls
|
|
217
|
+
`perform*` methods without the GraphQL context gets the 400 too when realms are
|
|
218
|
+
enabled.
|
|
219
|
+
- **The OAuth routes' IP rate-limit keys include the realm**
|
|
220
|
+
(`auth:ipRate:<realm>:<ipHash>`, as for `login`), so existing counts for those
|
|
221
|
+
routes start again after upgrading.
|
|
222
|
+
- With `['api-key']` alone, an unknown key's 401 now carries the guard's generic
|
|
223
|
+
`Unauthorized` message instead of `Invalid API key`. That also stops confirming
|
|
224
|
+
to the caller that the token was checked as an API key.
|
|
225
|
+
- `JwtAuthGuard` reads the request through `getRequestFromContext`, as
|
|
226
|
+
`createAuthGuard` does, instead of through `GqlExecutionContext`. There is no
|
|
227
|
+
behaviour change: the old lookup also returned the HTTP request on HTTP routes,
|
|
228
|
+
checked on NestJS 10 and 11.
|
|
229
|
+
|
|
230
|
+
### Documentation
|
|
231
|
+
- README: routes without a realm, `oauthController`, the OAuth routes' error
|
|
232
|
+
table, guard order for API keys and JWTs, a **Module Options** table, NestJS 10
|
|
233
|
+
and 11 support, and **Migrating to v0.10.0**. The OAuth section no longer calls
|
|
234
|
+
the routes callback routes; they exchange tokens.
|
|
235
|
+
- `IRefreshTokenRepository` documents that entities returned by
|
|
236
|
+
`findByHashedToken` must carry `id`, `userId` and `createdAt`, and that refresh
|
|
237
|
+
measures expiry from `createdAt`. `create` is not given `createdAt`, so a store
|
|
238
|
+
with no default for it fails on refresh. Found by the new end-to-end suite.
|
|
239
|
+
|
|
240
|
+
### Testing
|
|
241
|
+
- **CI runs on NestJS 11.** A `test:nest11` job runs the build, unit tests and
|
|
242
|
+
end-to-end suite on NestJS 11, `@nestjs/graphql` 13, Apollo Server 5 and
|
|
243
|
+
Express 5, beside the NestJS 10 job. Run locally before release: 982 unit tests
|
|
244
|
+
and 29 end-to-end tests pass on NestJS 11.2.5, `@nestjs/graphql` 13.4.5, Apollo
|
|
245
|
+
Server 5.5.1 and Express 5.2.1, with no source change for NestJS 11. On Express,
|
|
246
|
+
`@nestjs/apollo` 13 also needs `@as-integrations/express5`. Requested by Ambush
|
|
247
|
+
Desk.
|
|
248
|
+
- **An end-to-end suite** (`npm run test:e2e`, in `test/e2e`) boots a real HTTP
|
|
249
|
+
and GraphQL (Apollo) application against the built package, with in-memory
|
|
250
|
+
repositories. It covers:
|
|
251
|
+
- realm skipping and rejection
|
|
252
|
+
- the OAuth routes mounted, unmounted, and failing with each status above
|
|
253
|
+
- `createAuthGuard` with API keys and JWTs in both orders, on HTTP and in a
|
|
254
|
+
resolver
|
|
255
|
+
- `JwtAuthGuard` on HTTP
|
|
256
|
+
- login and refresh through a `BaseAuthResolver` subclass
|
|
257
|
+
|
|
258
|
+
Each fix above was checked by reintroducing the defect and watching a unit or
|
|
259
|
+
end-to-end test fail.
|
|
260
|
+
|
|
30
261
|
## [0.9.1] - 2026-09-14
|
|
31
262
|
|
|
32
263
|
Documentation only — no code changes. npm shows the README of the latest
|
package/README.md
CHANGED
|
@@ -20,10 +20,12 @@ Production-grade authentication module for NestJS GraphQL + REST APIs. JWT, cook
|
|
|
20
20
|
- [Password Policy](#password-policy)
|
|
21
21
|
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
22
22
|
- [Account Status](#account-status)
|
|
23
|
+
- [Refresh Retries](#refresh-retries)
|
|
23
24
|
- [JWT Validation Modes](#jwt-validation-modes)
|
|
24
25
|
- [JWT Payload Factory](#jwt-payload-factory)
|
|
25
26
|
- [Configuration Reference](#configuration-reference)
|
|
26
27
|
- [Required Options](#required-options)
|
|
28
|
+
- [Module Options](#module-options-forrootasync)
|
|
27
29
|
- [Common Options](#common-options)
|
|
28
30
|
- [Feature Flags](#feature-flags-features)
|
|
29
31
|
- [Security Secrets](#security-secrets)
|
|
@@ -34,8 +36,11 @@ Production-grade authentication module for NestJS GraphQL + REST APIs. JWT, cook
|
|
|
34
36
|
- [Optional Instance Options](#optional-instance-options)
|
|
35
37
|
- [Decorators](#decorators)
|
|
36
38
|
- [Guards](#guards)
|
|
39
|
+
- [GraphQL Subscriptions](#graphql-subscriptions)
|
|
37
40
|
- [Utilities](#utilities)
|
|
38
41
|
- [Security Features](#security-features)
|
|
42
|
+
- [Migrating to v0.11.0](#migrating-to-v0110)
|
|
43
|
+
- [Migrating to v0.10.0](#migrating-to-v0100)
|
|
39
44
|
- [Migrating to v0.9.0](#migrating-to-v090)
|
|
40
45
|
- [License](#license)
|
|
41
46
|
|
|
@@ -65,6 +70,8 @@ npm install @nestjs/common @nestjs/core @nestjs/graphql @nestjs/jwt @nestjs/pass
|
|
|
65
70
|
|
|
66
71
|
`resend` is an optional peer dependency -- install it only if using the Resend email sender.
|
|
67
72
|
|
|
73
|
+
Supports NestJS 10 and NestJS 11. CI runs the unit and end-to-end suites on both: NestJS 10 with `@nestjs/graphql` 12, Express 4 and Apollo Server 4, and NestJS 11 with `@nestjs/graphql` 13, Express 5 and Apollo Server 5.
|
|
74
|
+
|
|
68
75
|
## Quick Start
|
|
69
76
|
|
|
70
77
|
Minimal setup: JWT authentication with email/password login.
|
|
@@ -469,6 +476,29 @@ AuthModule.forRootAsync({
|
|
|
469
476
|
|
|
470
477
|
`RealmMiddleware` is automatically registered by `AuthModule` — no manual middleware registration needed. When a `realmExtractorInstance` is provided, the middleware calls `extractor.extractRealm(request)` on every request before any guards run. If the extractor returns `null`, the request is rejected with `400 Bad Request` (strict mode).
|
|
471
478
|
|
|
479
|
+
**Routes without a realm.** Health checks, inbound webhooks and public downloads have no realm by nature. Match them with `realm.skip`, and the middleware lets them through without calling the extractor:
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
AuthModule.forRootAsync({
|
|
483
|
+
// ...
|
|
484
|
+
useFactory: (usersRepo, tokenRepo, realmExtractor, config) => ({
|
|
485
|
+
// ...required options
|
|
486
|
+
realmExtractorInstance: realmExtractor,
|
|
487
|
+
realm: {
|
|
488
|
+
skip: (path) => path === '/health' || path.startsWith('/hooks/'),
|
|
489
|
+
},
|
|
490
|
+
}),
|
|
491
|
+
})
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
- `path` is the path the client requested, including any global prefix and without the query string. Match on it, not on `request.path` or `request.url`: Express rewrites both to `/` inside middleware. The request itself is the second argument.
|
|
495
|
+
- A skipped request has no realm: `request._realm` stays undefined.
|
|
496
|
+
- **Skip only routes that are unauthenticated or authenticate by other means.** A JWT's realm claim cannot match a request with no realm, so `JwtStrategy` answers 401. `BaseAuthResolver` flows and the OAuth routes answer `400 Realm required` rather than run outside a realm, which also contains a predicate that matches more than you meant.
|
|
497
|
+
- `skip` must return a boolean synchronously. If it throws or returns anything else, such as the Promise from an `async` function, the request is not skipped and the error is logged.
|
|
498
|
+
- Without `realmExtractorInstance`, `realm.skip` is ignored and a warning is logged at boot. A `realm.skip` that is not a function fails boot with `InvalidAuthConfigException`.
|
|
499
|
+
|
|
500
|
+
Why a predicate rather than route patterns: the same Nest route pattern matches different paths on NestJS 10 and NestJS 11 — `hooks/*` excludes `/hooks/glitchtip/issue` on 11 but not on 10 — so a pattern list would behave differently depending on your Nest version. A `@SkipRealm()` decorator cannot work either, because middleware runs before Nest resolves the route handler.
|
|
501
|
+
|
|
472
502
|
**Use `@CurrentRealm()` in resolvers:**
|
|
473
503
|
|
|
474
504
|
```typescript
|
|
@@ -525,7 +555,9 @@ Then use a multi-strategy guard:
|
|
|
525
555
|
{ provide: APP_GUARD, useClass: createAuthGuard(['api-key', 'jwt'], { allowPublic: true }) }
|
|
526
556
|
```
|
|
527
557
|
|
|
528
|
-
The `ApiKeyStrategy` hashes the bearer token with SHA-256 and calls `findByKeyHash()` on your repository. API keys and
|
|
558
|
+
The `ApiKeyStrategy` hashes the bearer token with SHA-256 and calls `findByKeyHash()` on your repository. API keys and JWTs share the `Authorization: Bearer` header, and strategies are tried in the order listed. A request with no bearer token, or with one that matches no key, falls through to the next strategy, so `['api-key', 'jwt']` and `['jwt', 'api-key']` both accept either credential. A key that matches an inactive account is rejected with 401 and no other strategy runs. When every strategy fails, the guard answers 401.
|
|
559
|
+
|
|
560
|
+
Before 0.10.0 the API key strategy rejected an unknown bearer token itself, which stopped the chain: `['api-key', 'jwt']` rejected every JWT.
|
|
529
561
|
|
|
530
562
|
### Email System
|
|
531
563
|
|
|
@@ -695,7 +727,33 @@ AuthModule.forRootAsync({
|
|
|
695
727
|
})
|
|
696
728
|
```
|
|
697
729
|
|
|
698
|
-
|
|
730
|
+
`OAuthController` mounts two token-exchange routes for native mobile sign-in: `POST /auth/google/token` with body `{ "idToken": "..." }`, and `POST /auth/facebook/token` with body `{ "accessToken": "..." }`. Both return `{ accessToken, refreshToken, user }`.
|
|
731
|
+
|
|
732
|
+
**Not using them?** Leave them unmounted, and they answer 404 like any unknown path:
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
AuthModule.forRootAsync({
|
|
736
|
+
oauthController: false, // beside useFactory, not inside it
|
|
737
|
+
useFactory: () => ({ /* ... */ }),
|
|
738
|
+
})
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
It sits beside `useFactory` because Nest fixes a module's controllers before async options resolve. GraphQL account linking does not use these routes and keeps working.
|
|
742
|
+
|
|
743
|
+
**Errors.** The routes answer with an HTTP status and a body carrying `message`, `code` and `statusCode` at the root, plus the fields listed:
|
|
744
|
+
|
|
745
|
+
| Status | `code` | When | Other fields |
|
|
746
|
+
|--------|--------|------|--------------|
|
|
747
|
+
| 404 | `OAUTH_PROVIDER_NOT_CONFIGURED` | The provider has no credentials in this deployment | `provider` |
|
|
748
|
+
| 401 | `INVALID_OAUTH_TOKEN` | The provider rejected the token | `provider` |
|
|
749
|
+
| 400 | `OAUTH_MISSING_DATA` | The provider returned no email, or an unverified one | `provider`, `missingField`; for a new Facebook user, `fallbackToken` and `providerId` |
|
|
750
|
+
| 409 | `EMAIL_EXISTS_WITH_PASSWORD` | The email belongs to a password account | `email`, `linkingToken` |
|
|
751
|
+
| 409 | `OAUTH_ACCOUNT_ALREADY_LINKED` | The account has a different ID linked for this provider | `provider` |
|
|
752
|
+
| 403 | `ACCOUNT_LOCKED` | Brute-force lockout | `remainingLockoutTime` (seconds) |
|
|
753
|
+
| 403 | `ACCOUNT_INACTIVE` | The account is suspended or deleted | -- |
|
|
754
|
+
| 429 | `RATE_LIMIT_EXCEEDED` | The client IP exceeded `bruteForce.ipRateLimit` | -- |
|
|
755
|
+
|
|
756
|
+
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.
|
|
699
757
|
|
|
700
758
|
### Brute Force Protection
|
|
701
759
|
|
|
@@ -871,6 +929,35 @@ It must be synchronous and pure: it runs on every authenticated request, against
|
|
|
871
929
|
|
|
872
930
|
**Your repository must return inactive users.** `findById` and `findByEmail` should return suspended and soft-deleted users like any other. A repository that hides them turns `ACCOUNT_INACTIVE` into a "not found" failure, and the package can no longer tell the client why.
|
|
873
931
|
|
|
932
|
+
### Refresh Retries
|
|
933
|
+
|
|
934
|
+
Refresh tokens rotate: each refresh deletes the token it used. A client that loses the response — a dropped connection, a timeout, an HTTP client that retries — sends the used token again, which looks exactly like a stolen token being replayed. For `refreshGracePeriodSeconds` (default 10) the package remembers the result and answers the retry with the same token pair. `0` turns this off.
|
|
935
|
+
|
|
936
|
+
**One instance:** nothing to do.
|
|
937
|
+
|
|
938
|
+
**More than one instance:** give every instance the same store, or a retry that reaches a different instance gets `401` and signs the user out:
|
|
939
|
+
|
|
940
|
+
```typescript
|
|
941
|
+
AuthModule.forRootAsync({
|
|
942
|
+
inject: [PgRefreshRetryCache, PgRateLimiter, ConfigService],
|
|
943
|
+
useFactory: (retryCache, rateLimiter, config) => ({
|
|
944
|
+
// ...required options
|
|
945
|
+
refreshRetryCacheInstance: retryCache,
|
|
946
|
+
encryptionKey: config.getOrThrow('ENCRYPTION_KEY'), // required with a shared store, same on every instance
|
|
947
|
+
rateLimiterInstance: rateLimiter, // password-reset throttling needs sharing for the same reason
|
|
948
|
+
}),
|
|
949
|
+
})
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
- Implement `IRefreshRetryCache` (`get`, `set(key, value, ttlMs)`, `delete`) over Redis, Postgres, or any store the instances share. Its JSDoc has a Postgres example.
|
|
953
|
+
- The values are AES-256-GCM ciphertext, because they hold live tokens. Boot fails with `InvalidAuthConfigException` when `encryptionKey` is missing.
|
|
954
|
+
- The package enforces expiry itself, so the store's TTL can be approximate.
|
|
955
|
+
- A store that errors costs retries, never the refresh itself.
|
|
956
|
+
|
|
957
|
+
At boot the package warns for each in-memory default in use, `rateLimiterInstance` and `refreshRetryCacheInstance`, because it cannot tell how many instances run. Pass `new InMemoryRateLimiterService()` and `new InMemoryRefreshRetryCache()` explicitly to confirm a single instance and silence them.
|
|
958
|
+
|
|
959
|
+
Before 0.11.0 a retry got `401` whenever `IRefreshTokenRepository.delete` removed the used token, even on a single instance.
|
|
960
|
+
|
|
874
961
|
### JWT Validation Modes
|
|
875
962
|
|
|
876
963
|
- **`'full'`** (default) -- Calls `userRepository.findById()` on every request, rejecting the request if the user no longer exists or is no longer active (see [Account Status](#account-status)).
|
|
@@ -907,6 +994,17 @@ jwtPayloadFactoryInstance: {
|
|
|
907
994
|
| `refreshTokenRepositoryInstance` | `IRefreshTokenRepository` | Token persistence |
|
|
908
995
|
| `jwtSecret` | `string` | JWT signing secret |
|
|
909
996
|
|
|
997
|
+
### Module Options (`forRootAsync`)
|
|
998
|
+
|
|
999
|
+
These sit beside `useFactory`, not in the object it returns.
|
|
1000
|
+
|
|
1001
|
+
| Option | Type | Default | Description |
|
|
1002
|
+
|--------|------|---------|-------------|
|
|
1003
|
+
| `useFactory` | `(...deps) => AuthModuleOptions` | -- | Returns the options in the other tables. Required |
|
|
1004
|
+
| `inject` | `any[]` | `[]` | Providers passed to `useFactory` |
|
|
1005
|
+
| `imports` | `any[]` | `[]` | Modules that export those providers |
|
|
1006
|
+
| `oauthController` | `boolean` | `true` | Mount the REST OAuth token routes. See [OAuth](#oauth) |
|
|
1007
|
+
|
|
910
1008
|
### Common Options
|
|
911
1009
|
|
|
912
1010
|
| Option | Type | Default | Description |
|
|
@@ -920,6 +1018,8 @@ jwtPayloadFactoryInstance: {
|
|
|
920
1018
|
| `passwordPolicy` | `PasswordPolicyConfig` | See [Password Policy](#password-policy) | Password strength rules |
|
|
921
1019
|
| `encryptionKey` | `string` | -- | 32-byte hex string for AES-256-GCM (OAuth token encryption) |
|
|
922
1020
|
| `bruteForce.ipRateLimit` | `{ maxAttempts, windowMs }` | -- | IP rate-limit policy for `checkIpRateLimit`. When set, `rateLimiterInstance` MUST also be provided explicitly (boot fails with `InvalidAuthConfigException` otherwise). See [IP Rate Limiting](#ip-rate-limiting). |
|
|
1021
|
+
| `realm.skip` | `(path, request) => boolean` | -- | Requests that need no realm, such as health checks and webhooks. Read only with `realmExtractorInstance`. See [Realm-Based Identity Isolation](#realm-based-identity-isolation) |
|
|
1022
|
+
| `refreshGracePeriodSeconds` | `number` | `10` | How long a retried refresh gets the same token pair. `0` turns retries off. See [Refresh Retries](#refresh-retries) |
|
|
923
1023
|
|
|
924
1024
|
### Feature Flags (`features`)
|
|
925
1025
|
|
|
@@ -975,6 +1075,7 @@ Separate HMAC secrets for security isolation. All fall back to `jwtSecret` if no
|
|
|
975
1075
|
| `headerName` | `string` | `'X-Requested-With'` | Header to check |
|
|
976
1076
|
| `requireInProduction` | `boolean` | `true` | Require CSRF header in production |
|
|
977
1077
|
| `exemptOperations` | `string[]` | `[]` | GraphQL operations to skip |
|
|
1078
|
+
| `webSocket` | `'reject' \| 'skip'` | `'reject'` | Cookie-authenticated GraphQL subscriptions: refuse with 403, or allow. `'skip'` needs an `Origin` check when the socket connects. See [GraphQL Subscriptions](#graphql-subscriptions) |
|
|
978
1079
|
|
|
979
1080
|
### Composable Email (`email`)
|
|
980
1081
|
|
|
@@ -1012,6 +1113,7 @@ When `email` is provided, it takes precedence over `emailServiceInstance`.
|
|
|
1012
1113
|
| `jwtPayloadFactoryInstance` | `IJwtPayloadFactory` | `DefaultJwtPayloadFactory` |
|
|
1013
1114
|
| `apiKeyRepositoryInstance` | `IApiKeyRepository` | `null` |
|
|
1014
1115
|
| `rateLimiterInstance` | `IRateLimiter` | `InMemoryRateLimiterService` (Note: this fallback does NOT apply when `bruteForce.ipRateLimit` is configured — boot fails with `InvalidAuthConfigException` and an explicit `rateLimiterInstance` is required. See [IP Rate Limiting](#ip-rate-limiting) above.) |
|
|
1116
|
+
| `refreshRetryCacheInstance` | `IRefreshRetryCache` | `InMemoryRefreshRetryCache`, which answers retries on the same instance only. A shared store requires `encryptionKey`. See [Refresh Retries](#refresh-retries) |
|
|
1015
1117
|
| `realmExtractorInstance` | `IRealmExtractor` | `null` (realms disabled) |
|
|
1016
1118
|
|
|
1017
1119
|
---
|
|
@@ -1037,7 +1139,7 @@ When `email` is provided, it takes precedence over `emailServiceInstance`.
|
|
|
1037
1139
|
| `CsrfGuard` | CSRF header validation for cookie auth |
|
|
1038
1140
|
| `TenantGuard` | Multi-tenant context resolution |
|
|
1039
1141
|
| `PermissionGuard` | Permission checking against tenant context |
|
|
1040
|
-
| `JwtAuthGuard` |
|
|
1142
|
+
| `JwtAuthGuard` | JWT-only guard for GraphQL and HTTP routes (prefer `createAuthGuard` for several strategies or `@Public()`) |
|
|
1041
1143
|
|
|
1042
1144
|
### Recommended Guard Chains
|
|
1043
1145
|
|
|
@@ -1057,13 +1159,45 @@ When realm support is enabled, `RealmMiddleware` runs before all guards (as Nest
|
|
|
1057
1159
|
{ provide: APP_GUARD, useClass: PermissionGuard },
|
|
1058
1160
|
```
|
|
1059
1161
|
|
|
1162
|
+
### GraphQL Subscriptions
|
|
1163
|
+
|
|
1164
|
+
The guards also protect subscriptions over `graphql-ws`. A socket has no HTTP request, so they read the WebSocket upgrade request instead: the headers and cookies the client opened the connection with.
|
|
1165
|
+
|
|
1166
|
+
| Guard | On a subscription |
|
|
1167
|
+
|-------|-------------------|
|
|
1168
|
+
| `createAuthGuard`, `JwtAuthGuard` | Authenticates from the upgrade request's `Authorization` header or session cookie. `RealmMiddleware` never sees the upgrade, so the realm is resolved from it once per connection |
|
|
1169
|
+
| `TenantGuard`, `PermissionGuard` | Resolve the tenant and check permissions from the upgrade request |
|
|
1170
|
+
| `CsrfGuard` | Browsers cannot send the CSRF header on a WebSocket, so `csrf.webSocket` decides: `'reject'` (default) answers 403, `'skip'` allows. A connection with an `Authorization` header is allowed either way |
|
|
1171
|
+
|
|
1172
|
+
**Use `'skip'` only with an `Origin` check when the socket connects.** That check is what stops another site from opening a socket with your users' cookies.
|
|
1173
|
+
|
|
1174
|
+
With your own `graphql-ws` server, also pass the connection through as the context. Without it, guards receive no request and refuse every subscription with 401:
|
|
1175
|
+
|
|
1176
|
+
```typescript
|
|
1177
|
+
useServer(
|
|
1178
|
+
{
|
|
1179
|
+
schema,
|
|
1180
|
+
context: (ctx) => ctx,
|
|
1181
|
+
onConnect: (ctx) => ALLOWED_ORIGINS.includes(ctx.extra.request.headers.origin ?? ''),
|
|
1182
|
+
},
|
|
1183
|
+
wsServer,
|
|
1184
|
+
);
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
With `GraphQLModule`'s `subscriptions: { 'graphql-ws': { onConnect } }`, the guards find the upgrade request on their own; put the same `onConnect` there.
|
|
1188
|
+
|
|
1189
|
+
`isWebSocketContext(context)` and `readCookie(request, name)` are exported for your own guards and connection handlers.
|
|
1190
|
+
|
|
1060
1191
|
## Utilities
|
|
1061
1192
|
|
|
1062
1193
|
Helper functions exported for use in custom guards, middleware and loggers:
|
|
1063
1194
|
|
|
1064
1195
|
| Function | Description |
|
|
1065
1196
|
|----------|-------------|
|
|
1066
|
-
| `getRequestFromContext(context)` |
|
|
1197
|
+
| `getRequestFromContext(context)` | The request behind an `ExecutionContext`: HTTP, GraphQL, or a `graphql-ws` subscription's upgrade request. `undefined` when there is none |
|
|
1198
|
+
| `requireRequestFromContext(context)` | The same, but throws `UnauthorizedException` when there is no request |
|
|
1199
|
+
| `isWebSocketContext(context)` | Whether the context is a GraphQL subscription over a WebSocket |
|
|
1200
|
+
| `readCookie(request, name)` | A cookie's value from `request.cookies`, or parsed from the `Cookie` header (a WebSocket upgrade request has only the header) |
|
|
1067
1201
|
| `extractIpAddress(request)` | Extract client IP (checks `clientIp`, `req.ip`, falls back to `'unknown'`) |
|
|
1068
1202
|
| `isUserActiveByDefault(user)` | The default account-active predicate (see [Account Status](#account-status)) |
|
|
1069
1203
|
| `emailForLog(email)`, `ipForLog(ip)` | An email or IP as it may appear in a log line — `emailHash=…` / `ipHash=…`, salted like the package's own logs |
|
|
@@ -1072,12 +1206,12 @@ Helper functions exported for use in custom guards, middleware and loggers:
|
|
|
1072
1206
|
| `hashIp(ip, salt)` | HMAC-SHA256 IP hash (16 hex characters) used for rate-limit keys |
|
|
1073
1207
|
|
|
1074
1208
|
```typescript
|
|
1075
|
-
import {
|
|
1209
|
+
import { requireRequestFromContext, extractIpAddress } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
1076
1210
|
|
|
1077
1211
|
@Injectable()
|
|
1078
1212
|
export class CustomGuard implements CanActivate {
|
|
1079
1213
|
canActivate(context: ExecutionContext): boolean {
|
|
1080
|
-
const request =
|
|
1214
|
+
const request = requireRequestFromContext(context);
|
|
1081
1215
|
const ip = extractIpAddress(request);
|
|
1082
1216
|
// your logic
|
|
1083
1217
|
return true;
|
|
@@ -1087,7 +1221,7 @@ export class CustomGuard implements CanActivate {
|
|
|
1087
1221
|
|
|
1088
1222
|
## Security Features
|
|
1089
1223
|
|
|
1090
|
-
- **Refresh token rotation** with HMAC-SHA256 hashing
|
|
1224
|
+
- **Refresh token rotation** with HMAC-SHA256 hashing; a retried refresh gets the same pair for 10 seconds (configurable, shareable across instances, encrypted at rest)
|
|
1091
1225
|
- **Bcrypt password hashing** with configurable rounds (default: 12)
|
|
1092
1226
|
- **Account status enforcement** -- suspended and deleted users are rejected at login, on refresh and on every authenticated request (`ACCOUNT_INACTIVE`)
|
|
1093
1227
|
- **AES-256-GCM encryption** for OAuth tokens at rest
|
|
@@ -1096,9 +1230,31 @@ export class CustomGuard implements CanActivate {
|
|
|
1096
1230
|
- **Stateless CSRF protection** for OAuth flows via `OAuthStateService`
|
|
1097
1231
|
- **`__Host-` cookie prefix** support for enhanced cookie security
|
|
1098
1232
|
- **Separate HMAC secrets** for refresh tokens, verification codes, and OAuth state
|
|
1099
|
-
- **Realm-based identity isolation** with JWT realm claim validation
|
|
1233
|
+
- **Realm-based identity isolation** with JWT realm claim validation, realm-scoped rate limiting and lockout, and auth flows that refuse a request without a realm
|
|
1100
1234
|
- **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.
|
|
1101
1235
|
|
|
1236
|
+
## Migrating to v0.11.0
|
|
1237
|
+
|
|
1238
|
+
**`getRequestFromContext` may return `undefined`.** It now also finds a `graphql-ws` subscription's upgrade request, and returns `undefined` where there is no request at all. TypeScript flags each call that assumes a request. In a guard that cannot decide without one, use `requireRequestFromContext`.
|
|
1239
|
+
|
|
1240
|
+
**Subscriptions through `CsrfGuard`.** With cookie auth they answer 403 unless you set `csrf.webSocket: 'skip'`, together with an `Origin` check when the socket connects. They failed before 0.11.0 as well: with that same 403 on `GraphQLModule` subscriptions, or a `TypeError` on your own `graphql-ws` server. See [GraphQL Subscriptions](#graphql-subscriptions).
|
|
1241
|
+
|
|
1242
|
+
**Cookie auth no longer needs cookie-parser.** The access and refresh token cookies are read from the `Cookie` header when `request.cookies` is absent.
|
|
1243
|
+
|
|
1244
|
+
**More than one instance.** Set `refreshRetryCacheInstance` and `encryptionKey`, or retried refreshes that reach another instance sign users out; see [Refresh Retries](#refresh-retries). Boot now warns for each in-memory default in use. `RefreshTokenService`'s retry cache methods became async, which only matters if you call or override them.
|
|
1245
|
+
|
|
1246
|
+
## Migrating to v0.10.0
|
|
1247
|
+
|
|
1248
|
+
No code change is required. Check the items that apply to you.
|
|
1249
|
+
|
|
1250
|
+
**Clients of the OAuth REST routes.** Four failures that answered 500 now answer 404, 403 or 429 with a `code`; see the table under [OAuth](#oauth). A client that shows a generic error for any failure keeps working. One that branched on 500, or on the old message `Account is locked. Please try again in N minutes.`, should read `code` instead. The lockout message is now the one `login` uses.
|
|
1251
|
+
|
|
1252
|
+
**API key guards.** `createAuthGuard(['api-key', 'jwt'])` now accepts JWTs. With `['api-key']` alone, an unknown key still gets 401, now with the guard's generic `Unauthorized` message rather than `Invalid API key`.
|
|
1253
|
+
|
|
1254
|
+
**Realms.** With realms enabled, a `BaseAuthResolver` flow or OAuth route reached by a request without a realm answers `400 Realm required`. Before `realm.skip`, `RealmMiddleware` made that impossible, so in practice this affects only code calling `perform*` methods without the GraphQL context, which now counts as having no realm. The OAuth routes also pass the realm to lockout checks, IP rate limiting and the failed-attempt reset. Their IP rate-limit counters move to the realm-prefixed keys `login` already uses (`auth:ipRate:<realm>:<ipHash>`), so existing counts for those routes start again.
|
|
1255
|
+
|
|
1256
|
+
**NestJS 11.** The package needs no change to run on it; CI now tests both majors.
|
|
1257
|
+
|
|
1102
1258
|
## Migrating to v0.9.0
|
|
1103
1259
|
|
|
1104
1260
|
**Behaviour change — suspended and deleted accounts are rejected.** Before
|
package/dist/auth.module.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { IJwtPayloadFactory } from './interfaces/jwt-payload-factory.interface';
|
|
|
18
18
|
import { IApiKeyRepository } from './interfaces/api-key-repository.interface';
|
|
19
19
|
import { PasswordPolicyConfig } from './interfaces/password-policy-config.interface';
|
|
20
20
|
import { IRateLimiter } from './interfaces/rate-limiter.interface';
|
|
21
|
+
import { IRefreshRetryCache } from './interfaces/refresh-retry-cache.interface';
|
|
21
22
|
import { IEmailSender } from './interfaces/email-sender.interface';
|
|
22
23
|
import { IEmailTemplateRenderer } from './interfaces/email-template-renderer.interface';
|
|
23
24
|
import { EmailBrandingConfig } from './interfaces/email-branding-config.interface';
|
|
@@ -49,6 +50,9 @@ export interface AuthModuleOptions {
|
|
|
49
50
|
tenantRepositoryInstance?: ITenantRepository;
|
|
50
51
|
tenantExtractorInstance?: ITenantExtractor;
|
|
51
52
|
realmExtractorInstance?: IRealmExtractor;
|
|
53
|
+
realm?: {
|
|
54
|
+
skip?: (path: string, request: Record<string, any>) => boolean;
|
|
55
|
+
};
|
|
52
56
|
resourcePermissionRepositoryInstance?: IResourcePermissionRepository;
|
|
53
57
|
jwtPayloadFactoryInstance?: IJwtPayloadFactory;
|
|
54
58
|
apiKeyRepositoryInstance?: IApiKeyRepository;
|
|
@@ -76,6 +80,7 @@ export interface AuthModuleOptions {
|
|
|
76
80
|
headerName?: string;
|
|
77
81
|
requireInProduction?: boolean;
|
|
78
82
|
exemptOperations?: string[];
|
|
83
|
+
webSocket?: 'reject' | 'skip';
|
|
79
84
|
};
|
|
80
85
|
jwtSecret: string;
|
|
81
86
|
jwtExpiresIn?: string;
|
|
@@ -95,6 +100,8 @@ export interface AuthModuleOptions {
|
|
|
95
100
|
encryptionKey?: string;
|
|
96
101
|
passwordPolicy?: PasswordPolicyConfig;
|
|
97
102
|
rateLimiterInstance?: IRateLimiter;
|
|
103
|
+
refreshGracePeriodSeconds?: number;
|
|
104
|
+
refreshRetryCacheInstance?: IRefreshRetryCache;
|
|
98
105
|
bruteForce?: {
|
|
99
106
|
ipRateLimit?: {
|
|
100
107
|
maxAttempts: number;
|
|
@@ -119,9 +126,12 @@ export interface AuthModuleOptions {
|
|
|
119
126
|
export interface AuthModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
|
120
127
|
useFactory: (...args: any[]) => Promise<AuthModuleOptions> | AuthModuleOptions;
|
|
121
128
|
inject?: any[];
|
|
129
|
+
oauthController?: boolean;
|
|
122
130
|
}
|
|
123
131
|
export declare function validateAuthModuleOptions(opts: AuthModuleOptions): void;
|
|
124
132
|
export declare function describeIgnoredFeatureFlags(opts: AuthModuleOptions): string[];
|
|
133
|
+
export declare function describeIgnoredOptions(opts: AuthModuleOptions): string[];
|
|
134
|
+
export declare function describeInMemoryDefaults(opts: AuthModuleOptions): string[];
|
|
125
135
|
export { AUTH_MODULE_OPTIONS, USER_REPOSITORY, REFRESH_TOKEN_REPOSITORY, EMAIL_SERVICE, SMS_SERVICE, AUTH_LIFECYCLE_HOOKS, VERIFICATION_REPOSITORY, BRUTE_FORCE_REPOSITORY, BIOMETRIC_REPOSITORY, AUTH_LOGGER, RATE_LIMITER, TENANT_REPOSITORY, TENANT_EXTRACTOR, RESOURCE_PERMISSION_REPOSITORY, JWT_PAYLOAD_FACTORY, API_KEY_REPOSITORY, } from './constants';
|
|
126
136
|
export declare class AuthModule implements NestModule {
|
|
127
137
|
configure(consumer: MiddlewareConsumer): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../src/auth.module.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,kBAAkB,EAElB,UAAU,EAGX,MAAM,gBAAgB,CAAC;AAIxB,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AA0B3D,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC;AAClF,OAAO,EAAE,uBAAuB,EAAE,MAAM,iDAAiD,CAAC;AAC1F,OAAO,EAAE,uBAAuB,EAAE,MAAM,gDAAgD,CAAC;AACzF,OAAO,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,MAAM,6CAA6C,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAYjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0CAA0C,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AACzE,OAAO,EAAE,6BAA6B,EAAE,MAAM,uDAAuD,CAAC;AACtG,OAAO,EAAE,kBAAkB,EAAE,MAAM,4CAA4C,CAAC;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,2CAA2C,CAAC;AA8B9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,+CAA+C,CAAC;AAGrF,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../src/auth.module.ts"],"names":[],"mappings":"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"}
|