@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 +1 -1
- package/dist/context/authentication-scope.service.d.ts +5 -2
- package/dist/context/authentication-scope.service.js +9 -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 +29 -3
- package/dist/utils/transport-errors.util.d.ts +3 -1
- package/dist/utils/transport-errors.util.js +9 -1
- package/package.json +15 -15
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,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,
|
|
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.
|
|
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,
|
|
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.
|
|
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
|
}
|
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 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
|
|
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
|
|
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,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.
|
|
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
|
|
51
|
-
"@nestjs/core": "12.2
|
|
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.
|
|
54
|
+
"@nestjs/idempotency": "0.0.3",
|
|
55
55
|
"@nestjs/mail": "0.0.1",
|
|
56
|
-
"@nestjs/microservices": "12.2
|
|
57
|
-
"@nestjs/platform-express": "12.2
|
|
58
|
-
"@nestjs/platform-fastify": "12.2
|
|
59
|
-
"@nestjs/platform-ws": "12.2
|
|
60
|
-
"@nestjs/resilience": "0.0.
|
|
61
|
-
"@nestjs/testing": "12.2
|
|
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
|
|
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.
|
|
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",
|