@qauth-labs/resource-guard 0.2.0-rc.1 → 0.2.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/README.md +18 -1
- package/dist/lib/core.d.ts +5 -2
- package/dist/lib/core.js +27 -3
- package/dist/lib/introspection-validator.d.ts +6 -2
- package/dist/lib/introspection-validator.js +14 -2
- package/dist/lib/jwt-validator.d.ts +5 -3
- package/dist/lib/jwt-validator.js +10 -3
- package/dist/lib/sender-constraint.d.ts +47 -0
- package/dist/lib/sender-constraint.js +69 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,8 @@ Drop it into an MCP server and you get spec-correct OAuth in minutes:
|
|
|
19
19
|
is filtered out of both advertisement surfaces — it concerns refresh-token
|
|
20
20
|
issuance at the AS, not access to this resource
|
|
21
21
|
- Bearer-token validation in two modes — local **JWT** verification against the
|
|
22
|
-
QAuth JWKS, or **RFC 7662** introspection
|
|
22
|
+
QAuth JWKS, or **RFC 7662** introspection. Sender-constrained (DPoP-bound or
|
|
23
|
+
certificate-bound) tokens are refused; see [Security model](#security-model)
|
|
23
24
|
- **No token passthrough**: tokens not audience-bound to this resource
|
|
24
25
|
(**RFC 8707**) are rejected, and the inbound token is never forwarded upstream
|
|
25
26
|
- **Step-up** `403 insufficient_scope` scope challenges for incremental consent
|
|
@@ -212,6 +213,22 @@ try {
|
|
|
212
213
|
never returns the raw token to a call site or forwards it to upstream APIs.
|
|
213
214
|
- **Algorithm pinning.** JWT mode accepts `EdDSA` only by default, closing
|
|
214
215
|
algorithm-confusion attacks.
|
|
216
|
+
- **Sender-constrained tokens are refused.** A token bound to a key, DPoP-bound
|
|
217
|
+
(`cnf.jkt`, RFC 9449) or certificate-bound (`cnf["x5t#S256"]`, RFC 8705), is
|
|
218
|
+
refused with `401 invalid_token` in both modes, because this version verifies
|
|
219
|
+
no proof of possession. JWT mode refuses any token that carries `cnf`;
|
|
220
|
+
introspection mode refuses a response that carries `cnf` or whose
|
|
221
|
+
`token_type` is present and is not `Bearer` (compared ignoring case), so a
|
|
222
|
+
bound type another authorization server names differently is refused too. Accepting such a token as a plain bearer would let anyone who copied
|
|
223
|
+
it use it (RFC 9449 §7.2). The `DPoP` authorization scheme is not accepted
|
|
224
|
+
either: it gets the same bare `Bearer` challenge as a request without a token
|
|
225
|
+
(RFC 6750 §3.1), and the thrown `MissingTokenError` names the scheme. A
|
|
226
|
+
client that calls a resource server on this SDK must use unbound tokens: it
|
|
227
|
+
sends no `DPoP` proof to the token endpoint. With QAuth, three kinds of token
|
|
228
|
+
are always bound, so they cannot be used here until this SDK verifies DPoP
|
|
229
|
+
proofs: the tokens of a client marked `dpop_bound_access_tokens`, a token
|
|
230
|
+
exchanged from a DPoP-bound subject token, and an access token issued for a
|
|
231
|
+
`cnf`-bound ID-JAG. DPoP proof support is planned.
|
|
215
232
|
- **Exact scope matching.** Scopes are compared exactly and case-sensitively
|
|
216
233
|
(RFC 6749 §3.3) — no prefix or wildcard semantics. QAuth's scope vocabulary
|
|
217
234
|
declares no scope hierarchy (no scope implies another), so the MCP 2026-07-28
|
package/dist/lib/core.d.ts
CHANGED
|
@@ -58,8 +58,11 @@ export declare class ResourceGuard {
|
|
|
58
58
|
* @param scopes additional per-operation scopes to require on top of the
|
|
59
59
|
* guard's default `requiredScopes` (the step-up surface).
|
|
60
60
|
* @returns the validated, audience-bound token claims.
|
|
61
|
-
* @throws {MissingTokenError} when no bearer is present (→ 401, bare
|
|
62
|
-
*
|
|
61
|
+
* @throws {MissingTokenError} when no bearer is present (→ 401, bare
|
|
62
|
+
* challenge), the `DPoP` scheme included: this version accepts `Bearer`
|
|
63
|
+
* only, and the error's message says so.
|
|
64
|
+
* @throws {InvalidTokenError} when validation fails (→ 401 `invalid_token`),
|
|
65
|
+
* a sender-constrained (`cnf`-carrying) token included.
|
|
63
66
|
* @throws {InsufficientScopeError} when scopes are insufficient
|
|
64
67
|
* (→ 403 `insufficient_scope` step-up challenge).
|
|
65
68
|
*/
|
package/dist/lib/core.js
CHANGED
|
@@ -25,6 +25,17 @@ function stripTrailingSlash(value) {
|
|
|
25
25
|
const BEARER_SCHEME = /^Bearer +/i;
|
|
26
26
|
/** Line terminators, which a credential may not contain (RFC 6750 §2.1 b64token). */
|
|
27
27
|
const LINE_TERMINATOR = /[\n\r\u2028\u2029]/;
|
|
28
|
+
/**
|
|
29
|
+
* The RFC 9449 §7.1 `DPoP` scheme, matched case-insensitively (RFC 9110
|
|
30
|
+
* §11.1), which this version does not accept.
|
|
31
|
+
*/
|
|
32
|
+
const DPOP_SCHEME = /^DPoP +/i;
|
|
33
|
+
/**
|
|
34
|
+
* Why a request under the `DPoP` scheme is refused. This version verifies no
|
|
35
|
+
* DPoP proof, so it accepts only the `Bearer` scheme, and a sender-constrained
|
|
36
|
+
* token is refused under that scheme too.
|
|
37
|
+
*/
|
|
38
|
+
const DPOP_SCHEME_UNSUPPORTED = 'The DPoP authorization scheme is not supported; this resource accepts unbound Bearer tokens only';
|
|
28
39
|
/**
|
|
29
40
|
* Extract a bearer token from an `Authorization` header value (RFC 6750 §2.1).
|
|
30
41
|
* The scheme match is case-insensitive; the credential is returned verbatim.
|
|
@@ -177,8 +188,11 @@ export class ResourceGuard {
|
|
|
177
188
|
* @param scopes additional per-operation scopes to require on top of the
|
|
178
189
|
* guard's default `requiredScopes` (the step-up surface).
|
|
179
190
|
* @returns the validated, audience-bound token claims.
|
|
180
|
-
* @throws {MissingTokenError} when no bearer is present (→ 401, bare
|
|
181
|
-
*
|
|
191
|
+
* @throws {MissingTokenError} when no bearer is present (→ 401, bare
|
|
192
|
+
* challenge), the `DPoP` scheme included: this version accepts `Bearer`
|
|
193
|
+
* only, and the error's message says so.
|
|
194
|
+
* @throws {InvalidTokenError} when validation fails (→ 401 `invalid_token`),
|
|
195
|
+
* a sender-constrained (`cnf`-carrying) token included.
|
|
182
196
|
* @throws {InsufficientScopeError} when scopes are insufficient
|
|
183
197
|
* (→ 403 `insufficient_scope` step-up challenge).
|
|
184
198
|
*/
|
|
@@ -189,7 +203,17 @@ export class ResourceGuard {
|
|
|
189
203
|
// per-call step-up scopes) on the 401 challenge — #284. Same set the 403
|
|
190
204
|
// step-up would advertise, so an unauthenticated client and a
|
|
191
205
|
// under-scoped one are told the same thing.
|
|
192
|
-
|
|
206
|
+
const required = dedupe([...this.requiredScopes, ...scopes]);
|
|
207
|
+
// A `DPoP` credential gets the same bare `Bearer` challenge as no
|
|
208
|
+
// credential at all: RFC 6750 §3.1 gives no error code to a request
|
|
209
|
+
// that "attempted using an unsupported authentication method", and a
|
|
210
|
+
// `DPoP` challenge would advertise support this version lacks. The
|
|
211
|
+
// challenge naming only `Bearer` tells the client which scheme is
|
|
212
|
+
// accepted; the message tells the host's logs why.
|
|
213
|
+
if (authorization && DPOP_SCHEME.test(authorization.trim())) {
|
|
214
|
+
throw new MissingTokenError(required, DPOP_SCHEME_UNSUPPORTED);
|
|
215
|
+
}
|
|
216
|
+
throw new MissingTokenError(required);
|
|
193
217
|
}
|
|
194
218
|
const validated = await this.validator.validate(token);
|
|
195
219
|
this.assertScopes(validated, scopes);
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* and `client_secret_post`. We use Basic auth.
|
|
8
8
|
*
|
|
9
9
|
* QAuth contract (auth-server introspect route): returns
|
|
10
|
-
* `{ active, sub, client_id, exp, iat, iss, aud, scope, token_type }
|
|
10
|
+
* `{ active, sub, client_id, exp, iat, iss, aud, scope, token_type }`, plus
|
|
11
|
+
* `cnf` with `token_type: DPoP` for a DPoP-bound token, and
|
|
11
12
|
* `{ active: false }` for invalid / cross-audience tokens. The AS itself
|
|
12
13
|
* already enforces audience-authoritative introspection; `resource-guard`
|
|
13
14
|
* additionally re-checks `aud` locally so the no-passthrough guarantee does
|
|
@@ -39,7 +40,10 @@ export declare class IntrospectionValidator {
|
|
|
39
40
|
*
|
|
40
41
|
* @throws {InvalidTokenError} when the AS authoritatively says the *token* is
|
|
41
42
|
* unusable — inactive (revoked/expired/unknown) or bound to a different
|
|
42
|
-
* audience
|
|
43
|
+
* audience — or reports it as anything but a plain bearer token (a
|
|
44
|
+
* `token_type` that is present and not `Bearer`, or a `cnf` member): a
|
|
45
|
+
* sender-constrained token needs a proof this SDK does not verify. Maps to a
|
|
46
|
+
* 401 `invalid_token` Bearer challenge.
|
|
43
47
|
* @throws {IntrospectionError} when the *introspection call itself* fails for
|
|
44
48
|
* a reason that is not the token's fault — the AS is unreachable, returns a
|
|
45
49
|
* non-2xx (e.g. THIS RS's introspection credentials are misconfigured → 401
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* and `client_secret_post`. We use Basic auth.
|
|
8
8
|
*
|
|
9
9
|
* QAuth contract (auth-server introspect route): returns
|
|
10
|
-
* `{ active, sub, client_id, exp, iat, iss, aud, scope, token_type }
|
|
10
|
+
* `{ active, sub, client_id, exp, iat, iss, aud, scope, token_type }`, plus
|
|
11
|
+
* `cnf` with `token_type: DPoP` for a DPoP-bound token, and
|
|
11
12
|
* `{ active: false }` for invalid / cross-audience tokens. The AS itself
|
|
12
13
|
* already enforces audience-authoritative introspection; `resource-guard`
|
|
13
14
|
* additionally re-checks `aud` locally so the no-passthrough guarantee does
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
*/
|
|
16
17
|
import { IntrospectionError, InvalidTokenError } from './errors.js';
|
|
17
18
|
import { parseScopes } from './scope.js';
|
|
19
|
+
import { refuseNonBearerToken } from './sender-constraint.js';
|
|
18
20
|
function audienceList(aud) {
|
|
19
21
|
if (aud == null) {
|
|
20
22
|
return [];
|
|
@@ -46,7 +48,10 @@ export class IntrospectionValidator {
|
|
|
46
48
|
*
|
|
47
49
|
* @throws {InvalidTokenError} when the AS authoritatively says the *token* is
|
|
48
50
|
* unusable — inactive (revoked/expired/unknown) or bound to a different
|
|
49
|
-
* audience
|
|
51
|
+
* audience — or reports it as anything but a plain bearer token (a
|
|
52
|
+
* `token_type` that is present and not `Bearer`, or a `cnf` member): a
|
|
53
|
+
* sender-constrained token needs a proof this SDK does not verify. Maps to a
|
|
54
|
+
* 401 `invalid_token` Bearer challenge.
|
|
50
55
|
* @throws {IntrospectionError} when the *introspection call itself* fails for
|
|
51
56
|
* a reason that is not the token's fault — the AS is unreachable, returns a
|
|
52
57
|
* non-2xx (e.g. THIS RS's introspection credentials are misconfigured → 401
|
|
@@ -106,6 +111,13 @@ export class IntrospectionValidator {
|
|
|
106
111
|
if (!auds.includes(this.audience)) {
|
|
107
112
|
throw new InvalidTokenError('token audience does not match this resource');
|
|
108
113
|
}
|
|
114
|
+
// RFC 9449 §6.2 / §7.2: QAuth reports a DPoP-bound token with
|
|
115
|
+
// `token_type: DPoP` and its `cnf`. Such a token needs a proof of
|
|
116
|
+
// possession, and this SDK verifies none, so it is refused rather than
|
|
117
|
+
// accepted as a plain bearer. Either signal alone is enough. `token_type`
|
|
118
|
+
// is an allow-list: absent or `Bearer` only, so a bound type another AS
|
|
119
|
+
// names differently is refused too.
|
|
120
|
+
refuseNonBearerToken(body.cnf, body.token_type);
|
|
109
121
|
return {
|
|
110
122
|
sub: body.sub,
|
|
111
123
|
clientId: body.client_id,
|
|
@@ -48,9 +48,11 @@ export declare class JwtValidator {
|
|
|
48
48
|
* Verify a bearer token. Resolves to the normalised claims on success.
|
|
49
49
|
*
|
|
50
50
|
* @throws {InvalidTokenError} for any verification failure — bad signature,
|
|
51
|
-
* expiry, wrong issuer, wrong/absent audience, disallowed algorithm,
|
|
52
|
-
* `typ` that is not `at+jwt` (#283)
|
|
53
|
-
*
|
|
51
|
+
* expiry, wrong issuer, wrong/absent audience, disallowed algorithm, a
|
|
52
|
+
* `typ` that is not `at+jwt` (#283), or a sender-constrained token (one
|
|
53
|
+
* carrying `cnf`, such as a DPoP-bound token), which needs a proof this SDK
|
|
54
|
+
* does not verify. The reason is short and non-sensitive (safe for
|
|
55
|
+
* `error_description`/logs); the token is never included.
|
|
54
56
|
*/
|
|
55
57
|
validate(token: string): Promise<ValidatedToken>;
|
|
56
58
|
/**
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { createRemoteJWKSet, customFetch, errors as joseErrors, jwtVerify, } from 'jose';
|
|
15
15
|
import { InvalidTokenError } from './errors.js';
|
|
16
16
|
import { parseScopes } from './scope.js';
|
|
17
|
+
import { refuseNonBearerToken } from './sender-constraint.js';
|
|
17
18
|
/**
|
|
18
19
|
* RFC 9068 §2.1 `typ` of a JWT OAuth 2.0 access token.
|
|
19
20
|
*
|
|
@@ -75,9 +76,11 @@ export class JwtValidator {
|
|
|
75
76
|
* Verify a bearer token. Resolves to the normalised claims on success.
|
|
76
77
|
*
|
|
77
78
|
* @throws {InvalidTokenError} for any verification failure — bad signature,
|
|
78
|
-
* expiry, wrong issuer, wrong/absent audience, disallowed algorithm,
|
|
79
|
-
* `typ` that is not `at+jwt` (#283)
|
|
80
|
-
*
|
|
79
|
+
* expiry, wrong issuer, wrong/absent audience, disallowed algorithm, a
|
|
80
|
+
* `typ` that is not `at+jwt` (#283), or a sender-constrained token (one
|
|
81
|
+
* carrying `cnf`, such as a DPoP-bound token), which needs a proof this SDK
|
|
82
|
+
* does not verify. The reason is short and non-sensitive (safe for
|
|
83
|
+
* `error_description`/logs); the token is never included.
|
|
81
84
|
*/
|
|
82
85
|
async validate(token) {
|
|
83
86
|
let payload;
|
|
@@ -102,6 +105,10 @@ export class JwtValidator {
|
|
|
102
105
|
// attacker rewrites at will. Outside the try/catch above so this rejection
|
|
103
106
|
// is not laundered into a generic jose-error string.
|
|
104
107
|
this.assertAccessTokenTyp(protectedHeader['typ']);
|
|
108
|
+
// RFC 9449 §7.2: a token bound to a key (`cnf`) needs a proof of
|
|
109
|
+
// possession, and this SDK verifies none, so it is refused rather than
|
|
110
|
+
// accepted as a plain bearer. Read from the verified payload only.
|
|
111
|
+
refuseNonBearerToken(payload['cnf']);
|
|
105
112
|
return toValidatedToken(payload);
|
|
106
113
|
}
|
|
107
114
|
/**
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refusal of every access token that is not a plain bearer token.
|
|
3
|
+
*
|
|
4
|
+
* A token that carries a `cnf` (confirmation) claim (RFC 7800 §3) is bound to
|
|
5
|
+
* a key. A DPoP-bound token names that key's thumbprint in `cnf.jkt` (RFC 9449
|
|
6
|
+
* §6) and is reported by introspection with `token_type: DPoP` (RFC 9449
|
|
7
|
+
* §6.2). A certificate-bound token names a certificate in `cnf["x5t#S256"]`
|
|
8
|
+
* (RFC 8705 §3.1). Other confirmation methods exist. Whatever the method, the
|
|
9
|
+
* holder must prove possession of the key on every request, and the resource
|
|
10
|
+
* server must check that proof before it grants access (RFC 9449 §7.1, RFC
|
|
11
|
+
* 8705 §3). Accepting such a token as a plain bearer would let anyone who
|
|
12
|
+
* copied it use it, which is exactly the theft binding exists to stop
|
|
13
|
+
* (RFC 9449 §7.2).
|
|
14
|
+
*
|
|
15
|
+
* This version of the SDK verifies no proof of possession of any kind, so it
|
|
16
|
+
* refuses every sender-constrained token outright. QAuth's own protected
|
|
17
|
+
* routes refuse a bound token presented as a Bearer token in the same way.
|
|
18
|
+
*
|
|
19
|
+
* An introspection `token_type` is read as an allow-list, not a deny-list:
|
|
20
|
+
* only an absent one or `Bearer` is a token this SDK can accept. Another
|
|
21
|
+
* authorization server may name a bound token type this SDK has never heard
|
|
22
|
+
* of (`PoP`, a misspelt `DPoP`, a non-string value), and a deny-list would let
|
|
23
|
+
* it through as a bearer token.
|
|
24
|
+
*
|
|
25
|
+
* Internal: not exported from the package entry points.
|
|
26
|
+
*/
|
|
27
|
+
/** The reason given for a token bound to a DPoP key. */
|
|
28
|
+
export declare const DPOP_BOUND_REASON = "sender-constrained (DPoP-bound) token presented without a proof";
|
|
29
|
+
/** The reason given for a token bound by any other confirmation method. */
|
|
30
|
+
export declare const SENDER_CONSTRAINED_REASON = "sender-constrained token presented without a proof";
|
|
31
|
+
/** The reason given for an introspected `token_type` that is neither absent nor `Bearer`. */
|
|
32
|
+
export declare const NOT_BEARER_REASON = "token type is not Bearer";
|
|
33
|
+
/**
|
|
34
|
+
* Refuse a token that is not a plain bearer token. Call it after the token
|
|
35
|
+
* has been verified, with the verified claims only.
|
|
36
|
+
*
|
|
37
|
+
* @param cnf the token's `cnf` claim (JWT payload) or member (introspection
|
|
38
|
+
* response). Any value other than `undefined` refuses the token, a malformed
|
|
39
|
+
* one included, so an unknown confirmation method fails closed.
|
|
40
|
+
* @param tokenType the introspection response's `token_type`, when there is
|
|
41
|
+
* one. Only `undefined` (absent) or `Bearer` in any case is accepted; `DPoP`
|
|
42
|
+
* is refused as DPoP-bound, and any other value as not Bearer. The JWT
|
|
43
|
+
* validator passes nothing, since a JWT carries no `token_type`.
|
|
44
|
+
* @throws {InvalidTokenError} when the token is sender-constrained or its type
|
|
45
|
+
* is not Bearer.
|
|
46
|
+
*/
|
|
47
|
+
export declare function refuseNonBearerToken(cnf: unknown, tokenType?: unknown): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refusal of every access token that is not a plain bearer token.
|
|
3
|
+
*
|
|
4
|
+
* A token that carries a `cnf` (confirmation) claim (RFC 7800 §3) is bound to
|
|
5
|
+
* a key. A DPoP-bound token names that key's thumbprint in `cnf.jkt` (RFC 9449
|
|
6
|
+
* §6) and is reported by introspection with `token_type: DPoP` (RFC 9449
|
|
7
|
+
* §6.2). A certificate-bound token names a certificate in `cnf["x5t#S256"]`
|
|
8
|
+
* (RFC 8705 §3.1). Other confirmation methods exist. Whatever the method, the
|
|
9
|
+
* holder must prove possession of the key on every request, and the resource
|
|
10
|
+
* server must check that proof before it grants access (RFC 9449 §7.1, RFC
|
|
11
|
+
* 8705 §3). Accepting such a token as a plain bearer would let anyone who
|
|
12
|
+
* copied it use it, which is exactly the theft binding exists to stop
|
|
13
|
+
* (RFC 9449 §7.2).
|
|
14
|
+
*
|
|
15
|
+
* This version of the SDK verifies no proof of possession of any kind, so it
|
|
16
|
+
* refuses every sender-constrained token outright. QAuth's own protected
|
|
17
|
+
* routes refuse a bound token presented as a Bearer token in the same way.
|
|
18
|
+
*
|
|
19
|
+
* An introspection `token_type` is read as an allow-list, not a deny-list:
|
|
20
|
+
* only an absent one or `Bearer` is a token this SDK can accept. Another
|
|
21
|
+
* authorization server may name a bound token type this SDK has never heard
|
|
22
|
+
* of (`PoP`, a misspelt `DPoP`, a non-string value), and a deny-list would let
|
|
23
|
+
* it through as a bearer token.
|
|
24
|
+
*
|
|
25
|
+
* Internal: not exported from the package entry points.
|
|
26
|
+
*/
|
|
27
|
+
import { InvalidTokenError } from './errors.js';
|
|
28
|
+
/** The reason given for a token bound to a DPoP key. */
|
|
29
|
+
export const DPOP_BOUND_REASON = 'sender-constrained (DPoP-bound) token presented without a proof';
|
|
30
|
+
/** The reason given for a token bound by any other confirmation method. */
|
|
31
|
+
export const SENDER_CONSTRAINED_REASON = 'sender-constrained token presented without a proof';
|
|
32
|
+
/** The reason given for an introspected `token_type` that is neither absent nor `Bearer`. */
|
|
33
|
+
export const NOT_BEARER_REASON = 'token type is not Bearer';
|
|
34
|
+
/**
|
|
35
|
+
* Whether an introspection `token_type` is `want`, compared ignoring case as
|
|
36
|
+
* RFC 6749 §5.1 says, but otherwise exactly: no trimming, strings only.
|
|
37
|
+
*/
|
|
38
|
+
function isTokenType(tokenType, want) {
|
|
39
|
+
return typeof tokenType === 'string' && tokenType.toLowerCase() === want;
|
|
40
|
+
}
|
|
41
|
+
/** Whether a `cnf` value carries the DPoP member `jkt`. */
|
|
42
|
+
function hasJwkThumbprint(cnf) {
|
|
43
|
+
return typeof cnf === 'object' && cnf !== null && 'jkt' in cnf;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Refuse a token that is not a plain bearer token. Call it after the token
|
|
47
|
+
* has been verified, with the verified claims only.
|
|
48
|
+
*
|
|
49
|
+
* @param cnf the token's `cnf` claim (JWT payload) or member (introspection
|
|
50
|
+
* response). Any value other than `undefined` refuses the token, a malformed
|
|
51
|
+
* one included, so an unknown confirmation method fails closed.
|
|
52
|
+
* @param tokenType the introspection response's `token_type`, when there is
|
|
53
|
+
* one. Only `undefined` (absent) or `Bearer` in any case is accepted; `DPoP`
|
|
54
|
+
* is refused as DPoP-bound, and any other value as not Bearer. The JWT
|
|
55
|
+
* validator passes nothing, since a JWT carries no `token_type`.
|
|
56
|
+
* @throws {InvalidTokenError} when the token is sender-constrained or its type
|
|
57
|
+
* is not Bearer.
|
|
58
|
+
*/
|
|
59
|
+
export function refuseNonBearerToken(cnf, tokenType) {
|
|
60
|
+
if (isTokenType(tokenType, 'dpop') || hasJwkThumbprint(cnf)) {
|
|
61
|
+
throw new InvalidTokenError(DPOP_BOUND_REASON);
|
|
62
|
+
}
|
|
63
|
+
if (cnf !== undefined) {
|
|
64
|
+
throw new InvalidTokenError(SENDER_CONSTRAINED_REASON);
|
|
65
|
+
}
|
|
66
|
+
if (tokenType !== undefined && !isTokenType(tokenType, 'bearer')) {
|
|
67
|
+
throw new InvalidTokenError(NOT_BEARER_REASON);
|
|
68
|
+
}
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qauth-labs/resource-guard",
|
|
3
|
-
"version": "0.2.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Resource-server SDK for QAuth: RFC 9728 protected-resource metadata, RFC 6750 challenges and audience-bound bearer-token validation for MCP servers and other OAuth 2.1 resource servers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"oauth",
|