@idpartners/client-attestation-sdk 0.2.0 → 1.0.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 +35 -4
- package/dist/attester.d.ts +93 -0
- package/dist/attester.js +139 -0
- package/dist/builders.d.ts +4 -8
- package/dist/builders.js +18 -10
- package/dist/evidence.d.ts +4 -19
- package/dist/evidence.js +38 -20
- package/dist/http.d.ts +1 -41
- package/dist/http.js +30 -5
- package/dist/index.d.ts +11 -3
- package/dist/index.js +8 -2
- package/dist/issuance.d.ts +22 -3
- package/dist/issuance.js +37 -30
- package/dist/logging.d.ts +12 -0
- package/dist/logging.js +77 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +64 -0
- package/dist/signer.d.ts +2 -3
- package/dist/signer.js +9 -4
- package/dist/signing-key-pair.d.ts +8 -14
- package/dist/signing-key-pair.js +20 -5
- package/dist/svid-source.d.ts +15 -1
- package/dist/svid-source.js +20 -7
- package/dist/token-source.d.ts +42 -16
- package/dist/token-source.js +160 -74
- package/dist/validator/config.d.ts +5 -8
- package/dist/validator/config.js +22 -5
- package/dist/validator/index.d.ts +4 -3
- package/dist/validator/index.js +3 -3
- package/dist/validator/jwks.d.ts +0 -37
- package/dist/validator/jwks.js +17 -2
- package/dist/validator/resource.d.ts +0 -5
- package/dist/validator/resource.js +2 -0
- package/dist/validator/result.d.ts +0 -2
- package/dist/validator/result.js +2 -0
- package/dist/validator/strict.d.ts +1 -0
- package/dist/validator/strict.js +202 -0
- package/dist/validator/validator.d.ts +8 -0
- package/dist/validator/validator.js +120 -77
- package/package.json +9 -6
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
* cross-language error-precedence order, plus optional RFC 7662 introspection.
|
|
4
4
|
*
|
|
5
5
|
* This is the validator counterpart to the client-side builder SDK, and mirrors the Python
|
|
6
|
-
* `token_validator` package so every port reports the same verdict for the shared vectors.
|
|
6
|
+
* `client_attestation_sdk.token_validator` package so every port reports the same verdict for the shared vectors.
|
|
7
7
|
*/
|
|
8
8
|
export { AccessTokenValidator } from "./validator.js";
|
|
9
9
|
export type { HttpPost, Claims, ValidatorOptions } from "./validator.js";
|
|
10
10
|
export type { HttpGet } from "./jwks.js";
|
|
11
|
-
export {
|
|
11
|
+
export type { Logger, LogAttributes, LogLevel } from "../logging.js";
|
|
12
|
+
export { DEFAULT_ALGORITHMS, DEFAULT_TOKEN_TYPES } from "./config.js";
|
|
12
13
|
export type { ValidatorConfig, ResolvedConfig, IntrospectionConfig, IntrospectionAuthMethod, } from "./config.js";
|
|
13
14
|
export type { ValidationResult } from "./result.js";
|
|
14
15
|
export type { ValidationError } from "./errors.js";
|
|
15
16
|
export * as errors from "./errors.js";
|
|
16
17
|
export { ProtectedResource, bearerToken } from "./resource.js";
|
|
17
18
|
export type { ProtectedResourceMetadata, AuthDecision } from "./resource.js";
|
|
18
|
-
export declare const VALIDATOR_VERSION = "0.
|
|
19
|
+
export declare const VALIDATOR_VERSION = "1.0.0";
|
package/dist/validator/index.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* cross-language error-precedence order, plus optional RFC 7662 introspection.
|
|
4
4
|
*
|
|
5
5
|
* This is the validator counterpart to the client-side builder SDK, and mirrors the Python
|
|
6
|
-
* `token_validator` package so every port reports the same verdict for the shared vectors.
|
|
6
|
+
* `client_attestation_sdk.token_validator` package so every port reports the same verdict for the shared vectors.
|
|
7
7
|
*/
|
|
8
8
|
export { AccessTokenValidator } from "./validator.js";
|
|
9
|
-
export {
|
|
9
|
+
export { DEFAULT_ALGORITHMS, DEFAULT_TOKEN_TYPES } from "./config.js";
|
|
10
10
|
export * as errors from "./errors.js";
|
|
11
11
|
export { ProtectedResource, bearerToken } from "./resource.js";
|
|
12
|
-
export const VALIDATOR_VERSION = "0.
|
|
12
|
+
export const VALIDATOR_VERSION = "1.0.0";
|
package/dist/validator/jwks.d.ts
CHANGED
|
@@ -1,39 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolves issuer signing keys — from a static JWKS or a `jwksUri` (fetched and cached, refreshed on an
|
|
3
|
-
* unknown `kid` and when the cached set grows old).
|
|
4
|
-
*
|
|
5
|
-
* A `jwksUri` provider never fetches more than once per `minRefreshSeconds`, however many unknown `kid`
|
|
6
|
-
* values it is shown, and concurrent misses share one fetch. Without that, a stream of tokens with made-up
|
|
7
|
-
* `kid` values turns every request into a request to the issuer.
|
|
8
|
-
*/
|
|
9
|
-
import { type CryptoKey, type JSONWebKeySet } from "jose";
|
|
10
|
-
/** An imported verification key, as returned by jose's `importJWK`. */
|
|
11
|
-
export type VerificationKey = CryptoKey | Uint8Array;
|
|
12
1
|
/** Fetch a URL and return its body text. Injectable so tests can avoid real network I/O. */
|
|
13
2
|
export type HttpGet = (url: string) => Promise<string>;
|
|
14
|
-
export declare const MAX_JWKS_BYTES: number;
|
|
15
|
-
export declare const DEFAULT_MIN_REFRESH_SECONDS = 30;
|
|
16
|
-
export declare const DEFAULT_MAX_AGE_SECONDS = 3600;
|
|
17
|
-
export interface JwksProviderOptions {
|
|
18
|
-
jwks?: JSONWebKeySet;
|
|
19
|
-
jwksUri?: string;
|
|
20
|
-
httpGet?: HttpGet;
|
|
21
|
-
/** Minimum time between fetches. Defaults to {@link DEFAULT_MIN_REFRESH_SECONDS}. */
|
|
22
|
-
minRefreshSeconds?: number;
|
|
23
|
-
/** Refresh a fetched set once it is this old. Defaults to {@link DEFAULT_MAX_AGE_SECONDS}. */
|
|
24
|
-
maxAgeSeconds?: number;
|
|
25
|
-
/** Milliseconds clock, injectable for tests. Defaults to `Date.now`. */
|
|
26
|
-
clock?: () => number;
|
|
27
|
-
}
|
|
28
|
-
export declare class JwksProvider {
|
|
29
|
-
#private;
|
|
30
|
-
/** Why the last fetch failed, or `null` after a successful one. */
|
|
31
|
-
lastError: string | null;
|
|
32
|
-
constructor(opts: JwksProviderOptions);
|
|
33
|
-
/**
|
|
34
|
-
* Resolve the verification key for a header `kid` (and `alg`). Falls back to the single configured key
|
|
35
|
-
* when the header carries no `kid`. Returns `undefined` when no key can be found; never rejects — a
|
|
36
|
-
* failed fetch leaves its reason in {@link lastError}.
|
|
37
|
-
*/
|
|
38
|
-
resolve(kid: string | undefined, alg?: string): Promise<VerificationKey | undefined>;
|
|
39
|
-
}
|
package/dist/validator/jwks.js
CHANGED
|
@@ -8,16 +8,21 @@ var _a;
|
|
|
8
8
|
* `kid` values turns every request into a request to the issuer.
|
|
9
9
|
*/
|
|
10
10
|
import { importJWK } from "jose";
|
|
11
|
+
import { TransportError } from "../errors.js";
|
|
11
12
|
import { request } from "../http.js";
|
|
13
|
+
import { emit, failureReason, hostOf, NOOP_LOGGER } from "../logging.js";
|
|
14
|
+
/** @internal */
|
|
12
15
|
export const MAX_JWKS_BYTES = 512 * 1024;
|
|
16
|
+
/** @internal */
|
|
13
17
|
export const DEFAULT_MIN_REFRESH_SECONDS = 30;
|
|
18
|
+
/** @internal */
|
|
14
19
|
export const DEFAULT_MAX_AGE_SECONDS = 3600;
|
|
15
20
|
const FETCH_TIMEOUT_MS = 10_000;
|
|
16
21
|
const defaultGet = async (url) => {
|
|
17
22
|
// the shared transport refuses redirects and caps the body at MAX_JWKS_BYTES while reading it
|
|
18
23
|
const resp = await request(url, { method: "GET", timeoutMs: FETCH_TIMEOUT_MS, maxBytes: MAX_JWKS_BYTES });
|
|
19
24
|
if (!resp.ok) {
|
|
20
|
-
throw new
|
|
25
|
+
throw new TransportError(`GET ${url} failed: ${resp.status}`, "status", { status: resp.status });
|
|
21
26
|
}
|
|
22
27
|
return resp.body;
|
|
23
28
|
};
|
|
@@ -34,6 +39,7 @@ function algorithmFor(jwk) {
|
|
|
34
39
|
}
|
|
35
40
|
return "RS256";
|
|
36
41
|
}
|
|
42
|
+
/** @internal */
|
|
37
43
|
export class JwksProvider {
|
|
38
44
|
#static;
|
|
39
45
|
#jwksUri;
|
|
@@ -41,6 +47,7 @@ export class JwksProvider {
|
|
|
41
47
|
#minRefreshMs;
|
|
42
48
|
#maxAgeMs;
|
|
43
49
|
#clock;
|
|
50
|
+
#logger;
|
|
44
51
|
/** kid -> key, plus the JWK's declared `alg` (a JWK that declares one is only used for that alg). */
|
|
45
52
|
#keys = new Map();
|
|
46
53
|
#loaded = null;
|
|
@@ -56,6 +63,7 @@ export class JwksProvider {
|
|
|
56
63
|
this.#minRefreshMs = (opts.minRefreshSeconds ?? DEFAULT_MIN_REFRESH_SECONDS) * 1000;
|
|
57
64
|
this.#maxAgeMs = (opts.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS) * 1000;
|
|
58
65
|
this.#clock = opts.clock ?? Date.now;
|
|
66
|
+
this.#logger = opts.logger ?? NOOP_LOGGER;
|
|
59
67
|
if (opts.jwks != null) {
|
|
60
68
|
this.#loaded = _a.#parse(opts.jwks).then((keys) => {
|
|
61
69
|
this.#keys = keys;
|
|
@@ -108,6 +116,7 @@ export class JwksProvider {
|
|
|
108
116
|
}
|
|
109
117
|
const now = this.#clock();
|
|
110
118
|
if (this.#lastAttempt !== null && now - this.#lastAttempt < this.#minRefreshMs) {
|
|
119
|
+
emit(this.#logger, "debug", "jwks.refetch_throttled", { host: hostOf(this.#jwksUri) });
|
|
111
120
|
return Promise.resolve();
|
|
112
121
|
}
|
|
113
122
|
this.#lastAttempt = now;
|
|
@@ -115,13 +124,19 @@ export class JwksProvider {
|
|
|
115
124
|
try {
|
|
116
125
|
const body = await this.#httpGet(this.#jwksUri);
|
|
117
126
|
if (body.length > MAX_JWKS_BYTES) {
|
|
118
|
-
throw new
|
|
127
|
+
throw new TransportError(`JWKS is larger than ${MAX_JWKS_BYTES} bytes`, "too_large");
|
|
119
128
|
}
|
|
120
129
|
this.#keys = await _a.#parse(JSON.parse(body));
|
|
121
130
|
this.#loadedAt = this.#clock();
|
|
122
131
|
this.lastError = null;
|
|
132
|
+
emit(this.#logger, "debug", "jwks.fetched", { host: hostOf(this.#jwksUri), keys: this.#keys.size });
|
|
123
133
|
}
|
|
124
134
|
catch (exc) {
|
|
135
|
+
emit(this.#logger, "warn", "jwks.fetch_failed", {
|
|
136
|
+
host: hostOf(this.#jwksUri),
|
|
137
|
+
reason: failureReason(exc),
|
|
138
|
+
status: exc instanceof TransportError ? exc.status : undefined,
|
|
139
|
+
});
|
|
125
140
|
// keep serving the keys already held
|
|
126
141
|
this.lastError = `JWKS fetch failed: ${exc instanceof Error ? exc.message : String(exc)}`;
|
|
127
142
|
}
|
|
@@ -35,8 +35,3 @@ export declare class ProtectedResource {
|
|
|
35
35
|
challenge(error?: string, errorDescription?: string | null): string;
|
|
36
36
|
authenticate(authorizationHeader?: string | null, requiredScopes?: string[]): Promise<AuthDecision>;
|
|
37
37
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Make a value safe inside a quoted-string header parameter: drop CR, LF and every other control
|
|
40
|
-
* character (0x00-0x1F, 0x7F) so nothing can split or forge a header, then escape backslash and quote.
|
|
41
|
-
*/
|
|
42
|
-
export declare function quote(value: string): string;
|
|
@@ -89,6 +89,8 @@ function httpError(error) {
|
|
|
89
89
|
/**
|
|
90
90
|
* Make a value safe inside a quoted-string header parameter: drop CR, LF and every other control
|
|
91
91
|
* character (0x00-0x1F, 0x7F) so nothing can split or forge a header, then escape backslash and quote.
|
|
92
|
+
*
|
|
93
|
+
* @internal
|
|
92
94
|
*/
|
|
93
95
|
export function quote(value) {
|
|
94
96
|
// eslint-disable-next-line no-control-regex
|
|
@@ -15,5 +15,3 @@ export interface ValidationResult {
|
|
|
15
15
|
/** The `exp` claim (integer epoch seconds) when present. */
|
|
16
16
|
expiresAt: number | null;
|
|
17
17
|
}
|
|
18
|
-
export declare function success(claims: Record<string, unknown>, scopes: string[], audience: string[]): ValidationResult;
|
|
19
|
-
export declare function failure(error: ValidationError, description?: string): ValidationResult;
|
package/dist/validator/result.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** @internal */
|
|
1
2
|
export function success(claims, scopes, audience) {
|
|
2
3
|
const sub = claims.sub;
|
|
3
4
|
const exp = claims.exp;
|
|
@@ -12,6 +13,7 @@ export function success(claims, scopes, audience) {
|
|
|
12
13
|
expiresAt: exp == null ? null : Number(exp),
|
|
13
14
|
};
|
|
14
15
|
}
|
|
16
|
+
/** @internal */
|
|
15
17
|
export function failure(error, description) {
|
|
16
18
|
return {
|
|
17
19
|
valid: false,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The strict parsing every port applies to a token before trusting any of it (validation/SEMANTICS.md):
|
|
3
|
+
* compact serialisation, strict JSON objects, NumericDates and claim types. A failure throws
|
|
4
|
+
* {@link Malformed}; its message is generic and never quotes token bytes.
|
|
5
|
+
*/
|
|
6
|
+
import { base64url } from "jose";
|
|
7
|
+
/**
|
|
8
|
+
* Deepest object/array nesting a header, payload or introspection response may have.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export const MAX_DEPTH = 64;
|
|
13
|
+
const MAX_SAFE = Number.MAX_SAFE_INTEGER;
|
|
14
|
+
const SEGMENT = /^[A-Za-z0-9_-]*$/;
|
|
15
|
+
/**
|
|
16
|
+
* The input is not well-formed; the token is invalid.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export class Malformed extends Error {
|
|
21
|
+
}
|
|
22
|
+
function decodeSegment(segment) {
|
|
23
|
+
if (!SEGMENT.test(segment) || segment.length % 4 === 1) {
|
|
24
|
+
throw new Malformed("malformed token");
|
|
25
|
+
}
|
|
26
|
+
const data = base64url.decode(segment);
|
|
27
|
+
if (base64url.encode(data) !== segment) {
|
|
28
|
+
throw new Malformed("malformed token"); // non-zero padding bits: not the canonical encoding
|
|
29
|
+
}
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The header object and payload bytes of a compact JWS. The payload is returned undecoded: it is only
|
|
34
|
+
* parsed once the signature over it verifies.
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export function splitCompact(token) {
|
|
39
|
+
if (typeof token !== "string") {
|
|
40
|
+
throw new Malformed("malformed token");
|
|
41
|
+
}
|
|
42
|
+
const parts = token.split(".");
|
|
43
|
+
if (parts.length !== 3) {
|
|
44
|
+
throw new Malformed("malformed token");
|
|
45
|
+
}
|
|
46
|
+
const [headerBytes, payload] = parts.map(decodeSegment);
|
|
47
|
+
const header = parseObject(headerBytes, "header");
|
|
48
|
+
if ("crit" in header) {
|
|
49
|
+
// we implement no JWS extension, so RFC 7515 section 4.1.11 says reject any crit
|
|
50
|
+
throw new Malformed("header has crit");
|
|
51
|
+
}
|
|
52
|
+
return { header, payload };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* `data` as a strict RFC 8259 JSON object: UTF-8 without a BOM, no duplicate member names at any depth,
|
|
56
|
+
* at most {@link MAX_DEPTH} levels deep. (`JSON.parse` already refuses NaN, trailing commas and the like.)
|
|
57
|
+
*
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
export function parseObject(data, what = "payload") {
|
|
61
|
+
let text;
|
|
62
|
+
try {
|
|
63
|
+
// fatal: invalid UTF-8 is an error, not U+FFFD; ignoreBOM: keep a BOM so JSON.parse refuses it
|
|
64
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(data);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new Malformed(`${what} is not UTF-8`);
|
|
68
|
+
}
|
|
69
|
+
let value;
|
|
70
|
+
try {
|
|
71
|
+
value = JSON.parse(text);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new Malformed(`${what} is not JSON`);
|
|
75
|
+
}
|
|
76
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
77
|
+
throw new Malformed(`${what} is not a JSON object`);
|
|
78
|
+
}
|
|
79
|
+
checkStructure(text, what);
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Walk JSON text that `JSON.parse` has already accepted, refusing a duplicate member name in any object
|
|
84
|
+
* (names compared after unescaping) and nesting deeper than {@link MAX_DEPTH}. Iterative, so depth can't
|
|
85
|
+
* exhaust the stack.
|
|
86
|
+
*/
|
|
87
|
+
function checkStructure(text, what) {
|
|
88
|
+
// one frame per open container: a Set of member names for an object, null for an array
|
|
89
|
+
const stack = [];
|
|
90
|
+
let expectName = false;
|
|
91
|
+
for (let i = 0; i < text.length; i++) {
|
|
92
|
+
const ch = text[i];
|
|
93
|
+
if (ch === '"') {
|
|
94
|
+
let j = i + 1;
|
|
95
|
+
while (text[j] !== '"') {
|
|
96
|
+
j += text[j] === "\\" ? 2 : 1;
|
|
97
|
+
}
|
|
98
|
+
const top = stack[stack.length - 1];
|
|
99
|
+
if (expectName && top) {
|
|
100
|
+
const name = JSON.parse(text.slice(i, j + 1));
|
|
101
|
+
if (top.has(name)) {
|
|
102
|
+
throw new Malformed(`${what} is not a strict JSON object (duplicate member name)`);
|
|
103
|
+
}
|
|
104
|
+
top.add(name);
|
|
105
|
+
expectName = false;
|
|
106
|
+
}
|
|
107
|
+
i = j;
|
|
108
|
+
}
|
|
109
|
+
else if (ch === "{" || ch === "[") {
|
|
110
|
+
stack.push(ch === "{" ? new Set() : null);
|
|
111
|
+
if (stack.length > MAX_DEPTH) {
|
|
112
|
+
throw new Malformed(`${what} is nested too deeply`);
|
|
113
|
+
}
|
|
114
|
+
expectName = ch === "{";
|
|
115
|
+
}
|
|
116
|
+
else if (ch === "}" || ch === "]") {
|
|
117
|
+
stack.pop();
|
|
118
|
+
}
|
|
119
|
+
else if (ch === ",") {
|
|
120
|
+
expectName = stack[stack.length - 1] != null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* A NumericDate claim as an integer (fractions truncate toward zero), `undefined` when absent or null.
|
|
126
|
+
* Anything but a finite JSON number within ±(2^53 − 1) is malformed.
|
|
127
|
+
*
|
|
128
|
+
* @internal
|
|
129
|
+
*/
|
|
130
|
+
export function numericDate(claims, name) {
|
|
131
|
+
const value = claims[name];
|
|
132
|
+
if (value == null) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
136
|
+
throw new Malformed(`${name} is not a NumericDate`);
|
|
137
|
+
}
|
|
138
|
+
if (Math.abs(value) > MAX_SAFE) {
|
|
139
|
+
throw new Malformed(`${name} is out of range`);
|
|
140
|
+
}
|
|
141
|
+
return Math.trunc(value);
|
|
142
|
+
}
|
|
143
|
+
function isStringArray(value) {
|
|
144
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* `aud` as a list: absent or null is empty, a string is one entry, an array must hold only strings.
|
|
148
|
+
*
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
export function audience(claims) {
|
|
152
|
+
const value = claims.aud;
|
|
153
|
+
if (value == null) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
if (typeof value === "string") {
|
|
157
|
+
return [value];
|
|
158
|
+
}
|
|
159
|
+
if (isStringArray(value)) {
|
|
160
|
+
return [...value];
|
|
161
|
+
}
|
|
162
|
+
throw new Malformed("aud is not a string or an array of strings");
|
|
163
|
+
}
|
|
164
|
+
/** Split on U+0020 only, as RFC 6749 section 3.3 says; runs of spaces make no empty scopes. */
|
|
165
|
+
function split(text) {
|
|
166
|
+
return text.split(" ").filter((s) => s.length > 0);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Granted scopes: from `scope` (a space-delimited string) when present, else `scp` (a space-delimited string,
|
|
170
|
+
* as Microsoft Entra ID issues it, or an array of strings). Either one present with any other shape is
|
|
171
|
+
* malformed.
|
|
172
|
+
*
|
|
173
|
+
* @internal
|
|
174
|
+
*/
|
|
175
|
+
export function scopes(claims) {
|
|
176
|
+
const { scope, scp } = claims;
|
|
177
|
+
if (scope != null && typeof scope !== "string") {
|
|
178
|
+
throw new Malformed("scope is not a string");
|
|
179
|
+
}
|
|
180
|
+
if (scp != null && typeof scp !== "string" && !isStringArray(scp)) {
|
|
181
|
+
throw new Malformed("scp is not a string or an array of strings");
|
|
182
|
+
}
|
|
183
|
+
if (scope != null) {
|
|
184
|
+
return split(scope);
|
|
185
|
+
}
|
|
186
|
+
if (typeof scp === "string") {
|
|
187
|
+
return split(scp);
|
|
188
|
+
}
|
|
189
|
+
return scp != null ? [...scp] : [];
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* `sub`, `undefined` when absent or null; any other non-string is malformed.
|
|
193
|
+
*
|
|
194
|
+
* @internal
|
|
195
|
+
*/
|
|
196
|
+
export function subject(claims) {
|
|
197
|
+
const value = claims.sub;
|
|
198
|
+
if (value != null && typeof value !== "string") {
|
|
199
|
+
throw new Malformed("sub is not a string");
|
|
200
|
+
}
|
|
201
|
+
return value ?? undefined;
|
|
202
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ResolvedConfig, type ValidatorConfig } from "./config.js";
|
|
2
|
+
import { type Logger } from "../logging.js";
|
|
2
3
|
import { type HttpGet } from "./jwks.js";
|
|
3
4
|
import { type ValidationResult } from "./result.js";
|
|
4
5
|
/** Claims as decoded from a JWT payload (or an introspection response). */
|
|
@@ -11,6 +12,13 @@ export type HttpPost = (url: string, body: string, headers: Record<string, strin
|
|
|
11
12
|
export interface ValidatorOptions {
|
|
12
13
|
httpPost?: HttpPost;
|
|
13
14
|
httpGet?: HttpGet;
|
|
15
|
+
/**
|
|
16
|
+
* Where to send log events (`token.validated`, `jwks.*`, `http.failed`; see docs/logging.md). Unset,
|
|
17
|
+
* nothing is logged. Tokens and client secrets are never logged.
|
|
18
|
+
*/
|
|
19
|
+
logger?: Logger;
|
|
20
|
+
/** Epoch-millisecond clock that `exp` / `nbf` / `iat` are checked against. Defaults to `Date.now`. */
|
|
21
|
+
clock?: () => number;
|
|
14
22
|
}
|
|
15
23
|
/** Validates access tokens for a resource server. */
|
|
16
24
|
export declare class AccessTokenValidator {
|