@nestjs/authentication 0.1.2 → 0.1.4
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 +1 -1
- package/dist/context/authentication-scope.service.d.ts +7 -2
- package/dist/context/authentication-scope.service.js +12 -2
- package/dist/testing/index.js +9 -10
- package/dist/utils/auth-state.util.d.ts +5 -2
- package/dist/utils/auth-state.util.js +34 -3
- package/dist/utils/transport-errors.util.d.ts +3 -1
- package/dist/utils/transport-errors.util.js +13 -1
- package/package.json +16 -16
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
|
|
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,13 @@ 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,
|
|
36
|
-
*
|
|
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. An `@nestjs/ai`
|
|
40
|
+
* agent function called in an HTTP request carries it the same way, and has
|
|
41
|
+
* no response either: the route that runs the agent owns it.
|
|
37
42
|
*/
|
|
38
43
|
exchange(): HttpExchange | undefined;
|
|
39
44
|
}
|
|
@@ -39,8 +39,13 @@ 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,
|
|
43
|
-
*
|
|
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. An `@nestjs/ai`
|
|
47
|
+
* agent function called in an HTTP request carries it the same way, and has
|
|
48
|
+
* no response either: the route that runs the agent owns it.
|
|
44
49
|
*/
|
|
45
50
|
exchange() {
|
|
46
51
|
const context = this.current?.context;
|
|
@@ -53,6 +58,11 @@ let AuthenticationScope = class AuthenticationScope {
|
|
|
53
58
|
const gql = context.getArgByIndex(2);
|
|
54
59
|
return gql?.req ? { request: gql.req, response: gql.res } : undefined;
|
|
55
60
|
}
|
|
61
|
+
case 'mcp':
|
|
62
|
+
case 'ai': {
|
|
63
|
+
const request = context.getArgByIndex(1)?.request;
|
|
64
|
+
return request?.headers ? { request } : undefined;
|
|
65
|
+
}
|
|
56
66
|
default:
|
|
57
67
|
return undefined;
|
|
58
68
|
}
|
package/dist/testing/index.js
CHANGED
|
@@ -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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
47
|
-
* it counts as a safe request). `undefined` for RPC
|
|
48
|
+
* and the method for HTTP, GraphQL, MCP and agent functions (a WebSocket
|
|
49
|
+
* handshake has none, so it counts as a safe request). `undefined` for RPC,
|
|
50
|
+
* for MCP over stdio, and for an agent function called in a job.
|
|
48
51
|
*/
|
|
49
52
|
export declare function requestOf(context: ExecutionContext): {
|
|
50
53
|
headers: Headers;
|
|
@@ -28,12 +28,30 @@ 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) or of an
|
|
33
|
+
* `@nestjs/ai` agent function call, read from its arguments, `[input,
|
|
34
|
+
* context]`, so this package needs neither dependency. Its `request` is the
|
|
35
|
+
* HTTP platform's request: the one an MCP call arrived in (a plain
|
|
36
|
+
* `{ method, url, headers }` object for `@nestjs/mcp/testing`'s in-memory
|
|
37
|
+
* client), or the one the agent runs in. Absent over stdio, and in a job.
|
|
38
|
+
*/
|
|
39
|
+
function callContextOf(context) {
|
|
40
|
+
const call = context.getArgByIndex(1);
|
|
41
|
+
return typeof call === 'object' && call !== null ? call : undefined;
|
|
42
|
+
}
|
|
31
43
|
/**
|
|
32
44
|
* The object that carries the result for one call, mirrored as `user` /
|
|
33
45
|
* `session` where Passport-era code and `@nestjs/authorization` look:
|
|
34
46
|
* the request (http), the client socket (ws), the transport context (rpc),
|
|
35
47
|
* `context.req` (graphql; over graphql-ws, the socket's upgrade request, which
|
|
36
|
-
* every operation of the socket shares)
|
|
48
|
+
* every operation of the socket shares), and for an MCP call (`mcp`) the HTTP
|
|
49
|
+
* request it arrived in, else (over stdio) its own context object. Each MCP
|
|
50
|
+
* request travels in an HTTP request of its own, so the request is per call,
|
|
51
|
+
* and it is the one the endpoint's route was authenticated on: a guard there
|
|
52
|
+
* and on the call authenticate once. An agent function call (`ai`) is carried
|
|
53
|
+
* the same way: the HTTP request the agent runs in, which the route that runs
|
|
54
|
+
* it authenticated, else (in a job) its own context object.
|
|
37
55
|
*/
|
|
38
56
|
function carrierOf(context) {
|
|
39
57
|
switch (context.getType()) {
|
|
@@ -47,6 +65,11 @@ function carrierOf(context) {
|
|
|
47
65
|
}
|
|
48
66
|
case 'graphql':
|
|
49
67
|
return context.getArgByIndex(2)?.req;
|
|
68
|
+
case 'mcp':
|
|
69
|
+
case 'ai': {
|
|
70
|
+
const call = callContextOf(context);
|
|
71
|
+
return call?.request ?? call;
|
|
72
|
+
}
|
|
50
73
|
default:
|
|
51
74
|
return undefined;
|
|
52
75
|
}
|
|
@@ -178,6 +201,8 @@ function stateCarrierOf(context) {
|
|
|
178
201
|
* (`WsAuthenticator.authenticateConnection()` stores the upgrade request
|
|
179
202
|
* there for the `ws` library)
|
|
180
203
|
* - `graphql`: `context.req.headers`
|
|
204
|
+
* - `mcp`: the headers of the HTTP request the call arrived in; `undefined`
|
|
205
|
+
* over stdio, where the MCP spec takes credentials from the environment
|
|
181
206
|
* - `rpc`: `undefined`; metadata is transport specific
|
|
182
207
|
*/
|
|
183
208
|
export function getRequestHeaders(context) {
|
|
@@ -185,8 +210,9 @@ export function getRequestHeaders(context) {
|
|
|
185
210
|
}
|
|
186
211
|
/**
|
|
187
212
|
* The request behind a call, as far as authentication needs it: headers,
|
|
188
|
-
* and the method for HTTP and
|
|
189
|
-
* it counts as a safe request). `undefined` for RPC
|
|
213
|
+
* and the method for HTTP, GraphQL, MCP and agent functions (a WebSocket
|
|
214
|
+
* handshake has none, so it counts as a safe request). `undefined` for RPC,
|
|
215
|
+
* for MCP over stdio, and for an agent function called in a job.
|
|
190
216
|
*/
|
|
191
217
|
export function requestOf(context) {
|
|
192
218
|
switch (context.getType()) {
|
|
@@ -199,6 +225,11 @@ export function requestOf(context) {
|
|
|
199
225
|
}
|
|
200
226
|
case 'graphql':
|
|
201
227
|
return context.getArgByIndex(2)?.req;
|
|
228
|
+
case 'mcp':
|
|
229
|
+
case 'ai': {
|
|
230
|
+
const request = callContextOf(context)?.request;
|
|
231
|
+
return request?.headers ? request : undefined;
|
|
232
|
+
}
|
|
202
233
|
default:
|
|
203
234
|
return undefined;
|
|
204
235
|
}
|
|
@@ -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
|
|
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
|
|
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,16 @@ 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;
|
|
77
|
+
case 'ai':
|
|
78
|
+
// Nest's own exception too, which `@nestjs/ai` gives the model as the function's error. The route that runs
|
|
79
|
+
// the agent owns the HTTP response, so no challenge goes with it.
|
|
80
|
+
break;
|
|
69
81
|
}
|
|
70
82
|
const Exception = status === 409 ? ConflictException : status === 403 ? ForbiddenException : UnauthorizedException;
|
|
71
83
|
// 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.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Nest - modern, fast, powerful node.js web framework (@authentication)",
|
|
5
5
|
"author": "Kamil Mysliwiec",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,21 +46,21 @@
|
|
|
46
46
|
"@electric-sql/pglite": "0.5.8",
|
|
47
47
|
"@electric-sql/pglite-socket": "0.2.11",
|
|
48
48
|
"@nestjs/apollo": "14.0.1",
|
|
49
|
-
"@nestjs/authorization": "0.1.
|
|
50
|
-
"@nestjs/common": "12.
|
|
51
|
-
"@nestjs/core": "12.
|
|
49
|
+
"@nestjs/authorization": "0.1.1",
|
|
50
|
+
"@nestjs/common": "12.3.3",
|
|
51
|
+
"@nestjs/core": "12.3.3",
|
|
52
52
|
"@nestjs/drizzle": "0.0.2",
|
|
53
53
|
"@nestjs/graphql": "14.0.1",
|
|
54
|
-
"@nestjs/idempotency": "0.0.
|
|
54
|
+
"@nestjs/idempotency": "0.0.3",
|
|
55
55
|
"@nestjs/mail": "0.0.1",
|
|
56
|
-
"@nestjs/microservices": "12.
|
|
57
|
-
"@nestjs/platform-express": "12.
|
|
58
|
-
"@nestjs/platform-fastify": "12.
|
|
59
|
-
"@nestjs/platform-ws": "12.
|
|
60
|
-
"@nestjs/resilience": "0.0.
|
|
61
|
-
"@nestjs/testing": "12.
|
|
56
|
+
"@nestjs/microservices": "12.3.3",
|
|
57
|
+
"@nestjs/platform-express": "12.3.3",
|
|
58
|
+
"@nestjs/platform-fastify": "12.3.3",
|
|
59
|
+
"@nestjs/platform-ws": "12.3.3",
|
|
60
|
+
"@nestjs/resilience": "0.0.3",
|
|
61
|
+
"@nestjs/testing": "12.3.3",
|
|
62
62
|
"@nestjs/throttler": "6.7.1",
|
|
63
|
-
"@nestjs/websockets": "12.
|
|
63
|
+
"@nestjs/websockets": "12.3.3",
|
|
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.
|
|
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.
|
|
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
|
|
83
|
-
"ws": "8.22.
|
|
82
|
+
"vitest": "5.1.0",
|
|
83
|
+
"ws": "8.22.1"
|
|
84
84
|
},
|
|
85
85
|
"peerDependencies": {
|
|
86
86
|
"@nestjs/common": "^12.0.0",
|