@lenne.tech/nest-server 11.42.13 → 11.42.14

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.
@@ -781,6 +781,14 @@ their public-route shortcut, so the policy holds whichever guard runs first or a
781
781
  4. A request that carries a token credential but reaches a guard without a token context (the
782
782
  middleware did not run) is refused with 401 rather than treated as anonymous.
783
783
 
784
+ A route-level `@UseGuards(AuthGuard(AuthGuardStrategy.JWT | BETTER_AUTH))` (redundant next to
785
+ `@Roles()`, but common — nest-server-starter's `UserController` carries it on six routes) accepts the
786
+ token `CoreApiTokenMiddleware` authenticated instead of re-verifying the `ltt_…` credential, which
787
+ neither Passport nor `BetterAuthTokenService` knows; before 11.42.14 that re-verification answered
788
+ 401 on every released route. It calls `enforceApiTokenRoute()` itself, so it never opens a route the
789
+ role guards would refuse, even where it runs without one. `JWT_REFRESH` and project-defined strategies
790
+ never accept a token.
791
+
784
792
  #### System Roles (S_ prefix)
785
793
 
786
794
  System roles are evaluated at runtime and must **never** be stored in `user.roles`.
@@ -0,0 +1,74 @@
1
+ # Migration Guide: 11.42.13 → 11.42.14
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ | -------------------- | -------------------------------------------------------------------------------------------------------- |
7
+ | **Bugfixes** | A route-level `@UseGuards(AuthGuard(...))` no longer refuses every API token with 401 on a route released via `@ApiTokenScopes()` (§1) |
8
+ | **New Features** | None |
9
+ | **Framework API** | Unchanged — no new exports, no changed signatures |
10
+ | **Dependencies** | None for the package (development tooling only) |
11
+ | **Breaking Changes** | None. Tokens on routes that do not release them are refused exactly as before |
12
+ | **Migration Effort** | None. Projects that removed an `AuthGuard` to make tokens work may keep it removed (§1) |
13
+
14
+ ## Quick Migration
15
+
16
+ ```bash
17
+ pnpm update @lenne.tech/nest-server
18
+ ```
19
+
20
+ Vendor-mode projects pick it up with the core update. No code or configuration change is required.
21
+
22
+ ## 1. An explicit `AuthGuard` no longer closes a released route to API tokens
23
+
24
+ Many projects put `@UseGuards(AuthGuard(AuthGuardStrategy.BETTER_AUTH))` (or `AuthGuard(AuthGuardStrategy.JWT)`) on
25
+ routes next to `@Roles()`. nest-server-starter's `UserController` does so on six routes. The guard is redundant
26
+ there, because `@Roles()` already authenticates, but it was not harmless: it re-verified every request unless the
27
+ user came from Better-Auth, and a user authenticated by an API token does not. So it read the `ltt_…` credential as a
28
+ session or a JWT, failed, and answered **401** — on a route the role guard had just admitted via
29
+ `@ApiTokenScopes()`. Opening such a route for tokens therefore never worked while the guard was on it.
30
+
31
+ The guard now accepts the token `CoreApiTokenMiddleware` already authenticated, for the `JWT` and `BETTER_AUTH`
32
+ strategies. It applies the same route policy as the role guards (`enforceApiTokenRoute()`: deny by default, scopes,
33
+ tenant binding) itself, so it never admits a token on a route that does not release it — also in a setup where it
34
+ runs without a framework role guard in front of it.
35
+
36
+ ```typescript
37
+ @ApiTokenScopes('read')
38
+ @Get(':id')
39
+ @Roles(RoleEnum.S_USER)
40
+ @UseGuards(AuthGuard(AuthGuardStrategy.BETTER_AUTH)) // 11.42.13: 401 for every token; 11.42.14: works
41
+ async getUser(@CurrentUser() currentUser: User, @Param('id') id: string): Promise<User> { … }
42
+ ```
43
+
44
+ `AuthGuard(AuthGuardStrategy.JWT_REFRESH)`, `RefreshTokenGuard` and project-defined strategies never accept a token.
45
+
46
+ **What to check:**
47
+
48
+ - **You removed an `AuthGuard` from a route to make tokens work:** nothing to do. The guard stays redundant next to
49
+ `@Roles()`; restoring it is optional.
50
+ - **A test expected 401 for a token on a released route that carries an `AuthGuard`:** it now gets the route's
51
+ answer. A token on a route WITHOUT `@ApiTokenScopes()` is still refused with 403, as before.
52
+
53
+ ## Compatibility Notes
54
+
55
+ | Pattern | Status |
56
+ | ------- | ------ |
57
+ | Routes with `@Roles()` only | Unchanged |
58
+ | Routes with `@Roles()` + `@UseGuards(AuthGuard(BETTER_AUTH \| JWT))` + `@ApiTokenScopes()` | Tokens now pass (§1) |
59
+ | Routes with `@UseGuards(AuthGuard(...))` but no `@ApiTokenScopes()` | Tokens refused with 403, as before |
60
+ | `refreshToken` / `AuthGuard(JWT_REFRESH)` / custom Passport strategies | Never accept a token, as before |
61
+ | A project guard that extends `AuthGuard(...)` and overrides `handleRequest()` | Compatible; the override also runs for token users |
62
+
63
+ ## Troubleshooting
64
+
65
+ **A token still gets 401 on a route with an `AuthGuard`**
66
+ Check that the route (or its class) declares `@ApiTokenScopes()` with a scope the token holds — without it, the role
67
+ guard refuses the token with 403 before the `AuthGuard` runs. A 401 means the token itself did not authenticate
68
+ (revoked, expired, wrong prefix); the token middleware answers that before any guard.
69
+
70
+ ## Module Documentation
71
+
72
+ - [API token module README](../src/core/modules/api-token/README.md) — "Opening a route"
73
+ - [API token integration checklist](../src/core/modules/api-token/INTEGRATION-CHECKLIST.md)
74
+ - [Request lifecycle](../docs/REQUEST-LIFECYCLE.md) — "API tokens (`@ApiTokenScopes`)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.42.13",
3
+ "version": "11.42.14",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -192,7 +192,7 @@
192
192
  "@nestjs/schematics": "11.1.0",
193
193
  "@nestjs/testing": "11.2.7",
194
194
  "@swc/cli": "0.8.1",
195
- "@swc/core": "1.16.13",
195
+ "@swc/core": "1.16.18",
196
196
  "@tus/s3-store": "2.0.7",
197
197
  "@types/compression": "1.8.1",
198
198
  "@types/cookie-parser": "1.4.10",
@@ -60,6 +60,11 @@ What an opened route means per kind:
60
60
  membership role. Losing a membership removes the token's access at once — rights are read per
61
61
  request, never copied into the token.
62
62
 
63
+ An explicit `@UseGuards(AuthGuard(AuthGuardStrategy.BETTER_AUTH))` or `AuthGuard(AuthGuardStrategy.JWT)`
64
+ on the route does not close it to tokens (since 11.42.14; before, it answered 401): it accepts the token
65
+ the middleware authenticated and applies the same route policy. `AuthGuard(JWT_REFRESH)` and custom
66
+ strategies never accept a token.
67
+
63
68
  `request.user`:
64
69
 
65
70
  - TENANT token: `{ id: <token id>, name, scopes, tenantId, roles: [], hasRole: () => false }`
@@ -6,6 +6,7 @@ import { defaultOptions } from '@nestjs/passport/dist/options.js';
6
6
  import { memoize } from '@nestjs/passport/dist/utils/memoize.util.js';
7
7
  import passport = require('passport');
8
8
 
9
+ import { enforceApiTokenRoute, getApiTokenContext } from '../../api-token/core-api-token.helpers';
9
10
  import { BetterAuthTokenService } from '../../better-auth/better-auth-token.service';
10
11
  import { BetterAuthenticatedUser } from '../../better-auth/better-auth.types';
11
12
  import { CoreBetterAuthService } from '../../better-auth/core-better-auth.service';
@@ -21,6 +22,21 @@ const NO_STRATEGY_ERROR =
21
22
  'In order to use "defaultStrategy", please, ensure to import PassportModule in each ' +
22
23
  "place where AuthGuard() is being used. Otherwise, passport won't work correctly.";
23
24
 
25
+ /**
26
+ * Strategies that authenticate a person by an access credential. An API token authenticated by
27
+ * `CoreApiTokenMiddleware` satisfies them; `JWT_REFRESH` and project-defined strategies authenticate
28
+ * something else (a refresh token, a custom credential), so a token never stands in for those.
29
+ */
30
+ const API_TOKEN_SATISFIED_STRATEGIES: ReadonlySet<string> = new Set([
31
+ AuthGuardStrategy.BETTER_AUTH,
32
+ AuthGuardStrategy.JWT,
33
+ ]);
34
+
35
+ function isSatisfiedByApiToken(strategy: string | string[] | undefined): boolean {
36
+ const strategies = Array.isArray(strategy) ? strategy : [strategy];
37
+ return strategies.some((value) => !!value && API_TOKEN_SATISFIED_STRATEGIES.has(value));
38
+ }
39
+
24
40
  /**
25
41
  * Interface for auth guard
26
42
  */
@@ -61,6 +77,9 @@ const createPassportContext =
61
77
  * - If not, validates the token via BetterAuthTokenService (JWT or session token)
62
78
  * - Loads the full user from MongoDB with hasRole() capability
63
79
  *
80
+ * API tokens (JWT and BETTER_AUTH only): a token authenticated by CoreApiTokenMiddleware is accepted as is
81
+ * and the route is decided by enforceApiTokenRoute(), exactly as in the role guards.
82
+ *
64
83
  * Can be removed when pull request is merged:
65
84
  * https://github.com/nestjs/passport/pull/66
66
85
  */
@@ -118,11 +137,22 @@ function createAuthGuard(type?: AuthGuardStrategy | string | string[]): Type<IAu
118
137
 
119
138
  const options = { ...defaultOptions, ...this.options };
120
139
  const request = this.getRequest(context);
140
+ const existingUser = request?.[options.property || defaultOptions.property];
141
+
142
+ // An API token was already authenticated by CoreApiTokenMiddleware. Neither Passport nor
143
+ // BetterAuthTokenService knows its credential, so verifying it again here refused every token with
144
+ // 401 — also on a route that released it via @ApiTokenScopes(). The route is decided here with the
145
+ // same function the role guards use (deny by default, scopes, tenant binding), so the policy holds
146
+ // even where this guard runs without a framework role guard in front of it.
147
+ if (getApiTokenContext(existingUser) && isSatisfiedByApiToken(type || this.options?.defaultStrategy)) {
148
+ enforceApiTokenRoute({ controllerClass: context.getClass(), handler: context.getHandler(), request });
149
+ request[options.property || defaultOptions.property] = this.handleRequest(null, existingUser, null, context);
150
+ return true;
151
+ }
121
152
 
122
153
  // Check if user is already authenticated via Better-Auth middleware
123
154
  // Only skip Passport for Better-Auth users (marked with _authenticatedViaBetterAuth)
124
155
  // This ensures JWT_REFRESH guard still validates refresh tokens properly
125
- const existingUser = request?.[options.property || defaultOptions.property];
126
156
  if (existingUser && existingUser._authenticatedViaBetterAuth === true) {
127
157
  // User is authenticated via Better-Auth - skip Passport authentication
128
158
  // Validate through handleRequest to ensure role checks work