@nestjs/authentication 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  ## Description
20
20
 
21
- Authentication module for [Nest](https://github.com/nestjs/nest): a global guard with `@Public()` and `@Authenticate()`, credential providers as ordinary injectable classes (bearer JWT, cookie sessions, your own), server-side sessions with rotation, refresh tokens, TOTP second factor, magic links, OIDC sign-in, email verification and password reset, across HTTP, GraphQL, WebSockets and microservices, with no third-party dependencies.
21
+ Authentication module for [Nest](https://github.com/nestjs/nest): a global guard with `@Public()` and `@Authenticate()`, credential providers as ordinary injectable classes (bearer JWT, cookie sessions, your own), server-side sessions with rotation, refresh tokens, TOTP second factor, magic links, OIDC sign-in, email verification and password reset, across HTTP, GraphQL, WebSockets, microservices and MCP tool calls (`@nestjs/mcp`), with no third-party dependencies.
22
22
 
23
23
  ## Installation
24
24
 
@@ -32,8 +32,11 @@ export declare class AuthenticationScope {
32
32
  get current(): Scope | undefined;
33
33
  run<R>(scope: Scope, fn: () => R): R;
34
34
  /**
35
- * The HTTP request and response of the current call: an HTTP handler, or
36
- * a GraphQL resolver whose context carries `req` (and `res`).
35
+ * The HTTP request and response of the current call: an HTTP handler, a
36
+ * GraphQL resolver whose context carries `req` (and `res`), or an
37
+ * `@nestjs/mcp` call that arrived over HTTP (its context, the second of
38
+ * its arguments, carries `request`). An MCP call has no response of its own
39
+ * to set cookies on: the MCP SDK writes the HTTP response.
37
40
  */
38
41
  exchange(): HttpExchange | undefined;
39
42
  }
@@ -39,8 +39,11 @@ let AuthenticationScope = class AuthenticationScope {
39
39
  return this.storage.run(scope, fn);
40
40
  }
41
41
  /**
42
- * The HTTP request and response of the current call: an HTTP handler, or
43
- * a GraphQL resolver whose context carries `req` (and `res`).
42
+ * The HTTP request and response of the current call: an HTTP handler, a
43
+ * GraphQL resolver whose context carries `req` (and `res`), or an
44
+ * `@nestjs/mcp` call that arrived over HTTP (its context, the second of
45
+ * its arguments, carries `request`). An MCP call has no response of its own
46
+ * to set cookies on: the MCP SDK writes the HTTP response.
44
47
  */
45
48
  exchange() {
46
49
  const context = this.current?.context;
@@ -53,6 +56,10 @@ let AuthenticationScope = class AuthenticationScope {
53
56
  const gql = context.getArgByIndex(2);
54
57
  return gql?.req ? { request: gql.req, response: gql.res } : undefined;
55
58
  }
59
+ case 'mcp': {
60
+ const request = context.getArgByIndex(1)?.request;
61
+ return request?.headers ? { request } : undefined;
62
+ }
56
63
  default:
57
64
  return undefined;
58
65
  }
@@ -113,16 +113,15 @@ export function authenticationStoreContract(create, options = {}) {
113
113
  // Two instances: one clock a little behind the other.
114
114
  const behind = new SessionService(storage, { session: { ...options, now: () => clock - 2_000 } });
115
115
  const ahead = new SessionService(storage, { session: { ...options, now: () => clock } });
116
- for (let round = 0; round < 3; round++) {
117
- const { token } = await ahead.create(uid());
118
- clock += 61_000; // idle for 61 s on `ahead`, 59 s on `behind`
119
- const [seenIdle, touched] = await Promise.all([ahead.validate(token), behind.validate(token)]);
120
- same(seenIdle, null, 'the request that saw the session idle is refused');
121
- assert.ok(touched, 'the other instance, within the idle timeout, touches the session');
122
- clock += 1_000;
123
- assert.ok(await ahead.validate(token), 'the touched session is still there');
124
- }
125
- }, concurrent);
116
+ const { token } = await ahead.create(uid());
117
+ clock += 61_000; // idle for 61 s on `ahead`, 59 s on `behind`
118
+ // One after the other: run together, `behind` may read and touch first, and then `ahead` rightly
119
+ // finds the session alive. In this order, a `validate()` that deleted what it saw idle fails below.
120
+ same(await ahead.validate(token), null, 'the request that saw the session idle is refused');
121
+ assert.ok(await behind.validate(token), 'the other instance, within the idle timeout, touches the session');
122
+ clock += 1_000;
123
+ assert.ok(await ahead.validate(token), 'the touched session is still there');
124
+ });
126
125
  add('sessions', 'a request racing a sign-out or a rotation never brings the session back (SessionService)', async (sources) => {
127
126
  let clock = T0;
128
127
  const sessions = new SessionService(storageOf(sources, 'sessions'), {
@@ -38,13 +38,16 @@ export declare function getRawResult(context: ExecutionContext): State | undefin
38
38
  * (`WsAuthenticator.authenticateConnection()` stores the upgrade request
39
39
  * there for the `ws` library)
40
40
  * - `graphql`: `context.req.headers`
41
+ * - `mcp`: the headers of the HTTP request the call arrived in; `undefined`
42
+ * over stdio, where the MCP spec takes credentials from the environment
41
43
  * - `rpc`: `undefined`; metadata is transport specific
42
44
  */
43
45
  export declare function getRequestHeaders(context: ExecutionContext): Headers | undefined;
44
46
  /**
45
47
  * The request behind a call, as far as authentication needs it: headers,
46
- * and the method for HTTP and GraphQL (a WebSocket handshake has none, so
47
- * it counts as a safe request). `undefined` for RPC.
48
+ * and the method for HTTP, GraphQL and MCP (a WebSocket handshake has none,
49
+ * so it counts as a safe request). `undefined` for RPC, and for MCP over
50
+ * stdio.
48
51
  */
49
52
  export declare function requestOf(context: ExecutionContext): {
50
53
  headers: Headers;
@@ -28,12 +28,27 @@ function callKeys(context) {
28
28
  }
29
29
  return keys;
30
30
  }
31
+ /**
32
+ * The context of an `@nestjs/mcp` call (a tool, resource or prompt), read from
33
+ * its arguments, `[input, context]`, so this package needs no `@nestjs/mcp`
34
+ * dependency. Its `request` is the HTTP platform's request (a plain
35
+ * `{ method, url, headers }` object for `@nestjs/mcp/testing`'s in-memory
36
+ * client), absent over stdio.
37
+ */
38
+ function mcpCallOf(context) {
39
+ const call = context.getArgByIndex(1);
40
+ return typeof call === 'object' && call !== null ? call : undefined;
41
+ }
31
42
  /**
32
43
  * The object that carries the result for one call, mirrored as `user` /
33
44
  * `session` where Passport-era code and `@nestjs/authorization` look:
34
45
  * the request (http), the client socket (ws), the transport context (rpc),
35
46
  * `context.req` (graphql; over graphql-ws, the socket's upgrade request, which
36
- * every operation of the socket shares).
47
+ * every operation of the socket shares), and for an MCP call (`mcp`) the HTTP
48
+ * request it arrived in, else (over stdio) its own context object. Each MCP
49
+ * request travels in an HTTP request of its own, so the request is per call,
50
+ * and it is the one the endpoint's route was authenticated on: a guard there
51
+ * and on the call authenticate once.
37
52
  */
38
53
  function carrierOf(context) {
39
54
  switch (context.getType()) {
@@ -47,6 +62,10 @@ function carrierOf(context) {
47
62
  }
48
63
  case 'graphql':
49
64
  return context.getArgByIndex(2)?.req;
65
+ case 'mcp': {
66
+ const call = mcpCallOf(context);
67
+ return call?.request ?? call;
68
+ }
50
69
  default:
51
70
  return undefined;
52
71
  }
@@ -178,6 +197,8 @@ function stateCarrierOf(context) {
178
197
  * (`WsAuthenticator.authenticateConnection()` stores the upgrade request
179
198
  * there for the `ws` library)
180
199
  * - `graphql`: `context.req.headers`
200
+ * - `mcp`: the headers of the HTTP request the call arrived in; `undefined`
201
+ * over stdio, where the MCP spec takes credentials from the environment
181
202
  * - `rpc`: `undefined`; metadata is transport specific
182
203
  */
183
204
  export function getRequestHeaders(context) {
@@ -185,8 +206,9 @@ export function getRequestHeaders(context) {
185
206
  }
186
207
  /**
187
208
  * The request behind a call, as far as authentication needs it: headers,
188
- * and the method for HTTP and GraphQL (a WebSocket handshake has none, so
189
- * it counts as a safe request). `undefined` for RPC.
209
+ * and the method for HTTP, GraphQL and MCP (a WebSocket handshake has none,
210
+ * so it counts as a safe request). `undefined` for RPC, and for MCP over
211
+ * stdio.
190
212
  */
191
213
  export function requestOf(context) {
192
214
  switch (context.getType()) {
@@ -199,6 +221,10 @@ export function requestOf(context) {
199
221
  }
200
222
  case 'graphql':
201
223
  return context.getArgByIndex(2)?.req;
224
+ case 'mcp': {
225
+ const request = mcpCallOf(context)?.request;
226
+ return request?.headers ? request : undefined;
227
+ }
202
228
  default:
203
229
  return undefined;
204
230
  }
@@ -23,13 +23,15 @@ export declare function refusalOf(error: AuthenticationError): Refusal;
23
23
  * for `AuthenticationError`s thrown by handlers: the error each transport's
24
24
  * exception filter understands, for the refusal's status.
25
25
  *
26
- * - HTTP and GraphQL: Nest's own `UnauthorizedException` (or
26
+ * - HTTP, GraphQL and MCP: Nest's own `UnauthorizedException` (or
27
27
  * `ForbiddenException` for a 403, `ConflictException` for a 409), with
28
28
  * Nest's own body: `{"message":"Unauthorized","statusCode":401}`, or with a
29
29
  * message, `{"message":"Refresh token reused","error":"Unauthorized","statusCode":401}`.
30
30
  * A `code` is Nest's `errorCode` (on the exception, and in the body), and
31
31
  * the description in `error` when there is a message; `details` follow.
32
32
  * Over HTTP, `challenge` becomes the `WWW-Authenticate` header of a 401.
33
+ * `@nestjs/mcp` answers a tool call's exception with a tool error carrying
34
+ * its message, and a resource's or prompt's with a JSON-RPC error.
33
35
  * - ws and rpc: a `WsException` / `RpcException` carrying
34
36
  * `{ statusCode, message }` (plus `status: 'error'` for ws, and `error`,
35
37
  * `errorCode` and `details` as above). Those filters report
@@ -21,13 +21,15 @@ export function refusalOf(error) {
21
21
  * for `AuthenticationError`s thrown by handlers: the error each transport's
22
22
  * exception filter understands, for the refusal's status.
23
23
  *
24
- * - HTTP and GraphQL: Nest's own `UnauthorizedException` (or
24
+ * - HTTP, GraphQL and MCP: Nest's own `UnauthorizedException` (or
25
25
  * `ForbiddenException` for a 403, `ConflictException` for a 409), with
26
26
  * Nest's own body: `{"message":"Unauthorized","statusCode":401}`, or with a
27
27
  * message, `{"message":"Refresh token reused","error":"Unauthorized","statusCode":401}`.
28
28
  * A `code` is Nest's `errorCode` (on the exception, and in the body), and
29
29
  * the description in `error` when there is a message; `details` follow.
30
30
  * Over HTTP, `challenge` becomes the `WWW-Authenticate` header of a 401.
31
+ * `@nestjs/mcp` answers a tool call's exception with a tool error carrying
32
+ * its message, and a resource's or prompt's with a JSON-RPC error.
31
33
  * - ws and rpc: a `WsException` / `RpcException` carrying
32
34
  * `{ statusCode, message }` (plus `status: 'error'` for ws, and `error`,
33
35
  * `errorCode` and `details` as above). Those filters report
@@ -66,6 +68,12 @@ export async function refusal(context, { status = 401, message, code, details, c
66
68
  adapterHost?.httpAdapter?.setHeader(context.switchToHttp().getResponse(), 'WWW-Authenticate', challenge);
67
69
  }
68
70
  break;
71
+ case 'mcp':
72
+ // Nest's own exception, as for HTTP, which `@nestjs/mcp` answers with: a tool result with `isError: true` and
73
+ // the message, for a tool; a JSON-RPC error with the message and the status, for a resource or a prompt. The
74
+ // MCP SDK writes the HTTP response, so no challenge goes with it: an MCP client learns where to sign in from
75
+ // the endpoint's own 401 (`@nestjs/mcp`'s `auth` option).
76
+ break;
69
77
  }
70
78
  const Exception = status === 409 ? ConflictException : status === 403 ? ForbiddenException : UnauthorizedException;
71
79
  // Nest builds the body: `error` is the code (the description) or the status phrase, then `errorCode`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/authentication",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@authentication)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -47,20 +47,20 @@
47
47
  "@electric-sql/pglite-socket": "0.2.11",
48
48
  "@nestjs/apollo": "14.0.1",
49
49
  "@nestjs/authorization": "0.1.0",
50
- "@nestjs/common": "12.2.0",
51
- "@nestjs/core": "12.2.0",
50
+ "@nestjs/common": "12.3.2",
51
+ "@nestjs/core": "12.3.2",
52
52
  "@nestjs/drizzle": "0.0.2",
53
53
  "@nestjs/graphql": "14.0.1",
54
- "@nestjs/idempotency": "0.0.2",
54
+ "@nestjs/idempotency": "0.0.3",
55
55
  "@nestjs/mail": "0.0.1",
56
- "@nestjs/microservices": "12.2.0",
57
- "@nestjs/platform-express": "12.2.0",
58
- "@nestjs/platform-fastify": "12.2.0",
59
- "@nestjs/platform-ws": "12.2.0",
60
- "@nestjs/resilience": "0.0.2",
61
- "@nestjs/testing": "12.2.0",
56
+ "@nestjs/microservices": "12.3.2",
57
+ "@nestjs/platform-express": "12.3.2",
58
+ "@nestjs/platform-fastify": "12.3.2",
59
+ "@nestjs/platform-ws": "12.3.2",
60
+ "@nestjs/resilience": "0.0.3",
61
+ "@nestjs/testing": "12.3.2",
62
62
  "@nestjs/throttler": "6.7.1",
63
- "@nestjs/websockets": "12.2.0",
63
+ "@nestjs/websockets": "12.3.2",
64
64
  "@types/node": "24.19.2",
65
65
  "@types/pg": "8.23.1",
66
66
  "@types/supertest": "7.2.1",
@@ -69,18 +69,18 @@
69
69
  "graphql": "16.14.2",
70
70
  "husky": "9.1.7",
71
71
  "lint-staged": "17.6.0",
72
- "oxlint": "1.89.0",
72
+ "oxlint": "1.91.0",
73
73
  "pg": "8.23.1",
74
74
  "prettier": "3.9.10",
75
75
  "reflect-metadata": "0.2.2",
76
- "release-it": "21.1.1",
76
+ "release-it": "21.1.2",
77
77
  "rxjs": "7.8.2",
78
78
  "supertest": "7.3.1",
79
79
  "typeorm": "1.1.2",
80
80
  "typescript": "7.0.2",
81
81
  "vite": "8.3.5",
82
- "vitest": "5.0.5",
83
- "ws": "8.22.0"
82
+ "vitest": "5.1.0",
83
+ "ws": "8.22.1"
84
84
  },
85
85
  "peerDependencies": {
86
86
  "@nestjs/common": "^12.0.0",