@broberg/sso 0.15.0 → 0.17.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 +54 -1
- package/dist/{chunk-ZPRWYUFG.js → chunk-AVXQLEYK.js} +88 -3
- package/dist/chunk-AVXQLEYK.js.map +1 -0
- package/dist/{chunk-JGIO7XPR.cjs → chunk-QYMEISTU.cjs} +89 -2
- package/dist/chunk-QYMEISTU.cjs.map +1 -0
- package/dist/hono.cjs +37 -37
- package/dist/hono.d.cts +1 -1
- package/dist/hono.d.ts +1 -1
- package/dist/hono.js +1 -1
- package/dist/index.cjs +170 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -6
- package/dist/index.d.ts +112 -6
- package/dist/index.js +114 -11
- package/dist/index.js.map +1 -1
- package/dist/{session-DsjOyCJb.d.cts → session-CIm4_raQ.d.cts} +47 -1
- package/dist/{session-DsjOyCJb.d.ts → session-CIm4_raQ.d.ts} +47 -1
- package/package.json +1 -1
- package/dist/chunk-JGIO7XPR.cjs.map +0 -1
- package/dist/chunk-ZPRWYUFG.js.map +0 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AVATAR_TYPES, c as AddressOwnership, B as BeginLoginOptions, d as BidProfile, C as CreateSsoClientOptions, D as DEFAULT_SESSION_MAX_AGE, e as Discovery, I as
|
|
2
|
-
import 'jose';
|
|
1
|
+
export { A as AVATAR_TYPES, c as AddressOwnership, B as BeginLoginOptions, d as BidProfile, C as CreateSsoClientOptions, D as DEFAULT_SESSION_MAX_AGE, e as Discovery, I as IntrospectOptions, f as IntrospectedToken, g as InvitationOutcome, h as InvitationResult, i as InviteUsersInput, J as JwksCache, j as JwksCacheOptions, k as JwksError, l as JwksUnavailableError, m as JwksUnknownKeyError, L as LoginResult, n as LoginStart, o as LogoutToken, M as MAX_AVATAR_BYTES, p as MAX_INVITATIONS, q as MigrationState, r as MigrationStatus, b as SessionPayload, s as SsoAppApiError, t as SsoAvatarRejectedError, u as SsoClaims, S as SsoClient, a as SsoConfig, v as SsoConfigError, w as SsoError, x as SsoInsufficientScopeError, y as SsoIntrospectionUnavailableError, z as SsoReauthError, E as SsoTokenInactiveError, F as TokenSet, T as TokenStore, G as cookieHeader, H as createJwksCache, K as createSsoClient, N as loadSsoConfig, O as memoryTokenStore, P as readCookie, Q as signSession, R as signValue, U as verifySession, V as verifyValue } from './session-CIm4_raQ.cjs';
|
|
2
|
+
import { JWK } from 'jose';
|
|
3
3
|
|
|
4
4
|
declare const DEFAULT_ISSUER = "https://id.broberg.ai";
|
|
5
5
|
/** No platform identity to exchange: not on Fly, not in a GitHub Actions job with id-token: write. */
|
|
@@ -32,6 +32,17 @@ interface TicketClientOptions {
|
|
|
32
32
|
flyOidc?: (aud: string) => Promise<string>;
|
|
33
33
|
/** Whether the Fly socket is present. Default: checks /.fly/api. */
|
|
34
34
|
onFly?: () => boolean;
|
|
35
|
+
/**
|
|
36
|
+
* F084.157 — ask BID for DPoP-BOUND tickets. One ES256 key per client
|
|
37
|
+
* (non-extractable, never stored); every call then needs a proof, which
|
|
38
|
+
* `authHeaders` makes. Default false: bearer tickets, exactly as before.
|
|
39
|
+
*/
|
|
40
|
+
dpop?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** Headers for one call to a receiver. With `dpop`, `authorization` is `DPoP <ticket>` and `dpop` is the proof. */
|
|
43
|
+
interface TicketAuthHeaders {
|
|
44
|
+
authorization: string;
|
|
45
|
+
dpop?: string;
|
|
35
46
|
}
|
|
36
47
|
interface TicketClient {
|
|
37
48
|
/** A ticket for `audience` (BID's short name, e.g. "discovery"), from cache while it is fresh. */
|
|
@@ -39,6 +50,17 @@ interface TicketClient {
|
|
|
39
50
|
audience: string;
|
|
40
51
|
scope?: string;
|
|
41
52
|
}): Promise<string>;
|
|
53
|
+
/**
|
|
54
|
+
* F084.157 — the headers for ONE request to a receiver: a ticket plus, with
|
|
55
|
+
* `dpop`, a fresh proof bound to this method and URL (use it once). Without
|
|
56
|
+
* `dpop`: `{ authorization: "Bearer <ticket>" }`.
|
|
57
|
+
*/
|
|
58
|
+
authHeaders(opts: {
|
|
59
|
+
audience: string;
|
|
60
|
+
scope?: string;
|
|
61
|
+
method: string;
|
|
62
|
+
url: string;
|
|
63
|
+
}): Promise<TicketAuthHeaders>;
|
|
42
64
|
}
|
|
43
65
|
declare function createTicketClient(options?: TicketClientOptions): TicketClient;
|
|
44
66
|
/** A ticket from the process-wide client (one cache per process). */
|
|
@@ -47,6 +69,68 @@ declare function fetchTicket(opts: {
|
|
|
47
69
|
scope?: string;
|
|
48
70
|
}): Promise<string>;
|
|
49
71
|
|
|
72
|
+
/**
|
|
73
|
+
* F084.157 — DPoP (RFC 9449): a bound ticket works only for whoever holds the key
|
|
74
|
+
* (broberg-id-F087.16, joint design #2572/#2574).
|
|
75
|
+
*
|
|
76
|
+
* A DPoP-bound ticket carries `cnf.jkt`, the thumbprint of the caller's public
|
|
77
|
+
* key. With every request the caller signs a short PROOF (method, URL, time, a
|
|
78
|
+
* hash of the ticket) with the matching private key. A ticket copied out of a log
|
|
79
|
+
* is useless on another machine: it has the ticket, not the key.
|
|
80
|
+
*
|
|
81
|
+
* THE RULE THAT MAKES IT PROTECTION (agreed with broberg-id, (A) in #2574): once a
|
|
82
|
+
* receiver verifies with this version, a ticket WITH cnf.jkt needs a valid proof —
|
|
83
|
+
* also while DPoP is otherwise optional. `requireDpop` only decides whether
|
|
84
|
+
* UNBOUND tickets are still accepted. Were bound tickets accepted without a proof
|
|
85
|
+
* during the rollout, a stolen bound ticket would work as bearer the whole time.
|
|
86
|
+
*
|
|
87
|
+
* Not here yet: server nonces (RFC 9449 §8). The jti replay store covers replay
|
|
88
|
+
* inside the iat window.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/** One process's DPoP key. The private half cannot be exported. */
|
|
92
|
+
interface DpopKey {
|
|
93
|
+
privateKey: CryptoKey;
|
|
94
|
+
publicJwk: JWK;
|
|
95
|
+
/** RFC 7638 thumbprint — what BID puts in the ticket's cnf.jkt. */
|
|
96
|
+
jkt: string;
|
|
97
|
+
}
|
|
98
|
+
/** A fresh ES256 key pair, private key non-extractable, never written anywhere. */
|
|
99
|
+
declare function createDpopKey(): Promise<DpopKey>;
|
|
100
|
+
/** `ath`: base64url(SHA-256(ticket)) — ties a proof to one ticket. */
|
|
101
|
+
declare function accessTokenHash(ticket: string): Promise<string>;
|
|
102
|
+
/** RFC 9449 §4.3: htu is compared without query and fragment. */
|
|
103
|
+
declare function normalizeHtu(url: string): string;
|
|
104
|
+
/** Sign one proof. Pass `ticket` when calling a resource server (adds ath); omit it for BID's token endpoint. */
|
|
105
|
+
declare function createDpopProof(key: DpopKey, opts: {
|
|
106
|
+
method: string;
|
|
107
|
+
url: string;
|
|
108
|
+
ticket?: string;
|
|
109
|
+
now?: () => number;
|
|
110
|
+
}): Promise<string>;
|
|
111
|
+
/**
|
|
112
|
+
* Remembers which proofs were already used. The default is in-memory, which
|
|
113
|
+
* holds on ONE machine: a receiver running on two machines must pass a shared
|
|
114
|
+
* store, or a proof replayed against the other machine inside the window passes.
|
|
115
|
+
*/
|
|
116
|
+
interface ReplayStore {
|
|
117
|
+
/** Record `key`; resolve true if it was already recorded within `ttlMs`. */
|
|
118
|
+
seen(key: string, ttlMs: number): Promise<boolean>;
|
|
119
|
+
}
|
|
120
|
+
declare function createMemoryReplayStore(now?: () => number): ReplayStore;
|
|
121
|
+
interface VerifyProofOptions {
|
|
122
|
+
method: string;
|
|
123
|
+
/** The receiver's PUBLIC URL for this request (behind a proxy the process sees an internal one). */
|
|
124
|
+
url: string;
|
|
125
|
+
ticket: string;
|
|
126
|
+
jkt: string;
|
|
127
|
+
replay: ReplayStore;
|
|
128
|
+
iatWindowSec: number;
|
|
129
|
+
now: () => number;
|
|
130
|
+
}
|
|
131
|
+
/** Throws SsoError naming the first thing that is wrong. */
|
|
132
|
+
declare function verifyDpopProof(proof: string, o: VerifyProofOptions): Promise<void>;
|
|
133
|
+
|
|
50
134
|
type PrincipalType = "human" | "service" | "agent";
|
|
51
135
|
/**
|
|
52
136
|
* F084.156 — one link of an RFC 8693 delegation chain. `act` is the next link
|
|
@@ -76,8 +160,8 @@ interface TicketPrincipal {
|
|
|
76
160
|
version: number;
|
|
77
161
|
/**
|
|
78
162
|
* F084.156 — the DPoP key thumbprint when the ticket is sender-constrained.
|
|
79
|
-
*
|
|
80
|
-
*
|
|
163
|
+
* Since 0.16.0 (F084.157) a ticket with cnf.jkt is only accepted together with
|
|
164
|
+
* a valid DPoP proof for that key — see `verify(token, { dpop })`.
|
|
81
165
|
*/
|
|
82
166
|
cnf: {
|
|
83
167
|
jkt: string;
|
|
@@ -106,13 +190,35 @@ interface TicketVerifierOptions {
|
|
|
106
190
|
warmUp?: boolean;
|
|
107
191
|
fetchImpl?: typeof fetch;
|
|
108
192
|
minRefetchIntervalMs?: number;
|
|
193
|
+
/**
|
|
194
|
+
* F084.157 — refuse UNBOUND tickets (bearer). Off by default during the
|
|
195
|
+
* rollout. A BOUND ticket (cnf.jkt) needs a valid proof either way.
|
|
196
|
+
*/
|
|
197
|
+
requireDpop?: boolean;
|
|
198
|
+
/** Where used proof ids are remembered. Default in-memory — ONE machine only; share one across machines. */
|
|
199
|
+
replayStore?: ReplayStore;
|
|
200
|
+
/** How far a proof's iat may be from now. Default 60 s. */
|
|
201
|
+
dpopIatWindowSec?: number;
|
|
202
|
+
/** Injectable clock for the DPoP checks (ms). */
|
|
203
|
+
now?: () => number;
|
|
204
|
+
}
|
|
205
|
+
/** The DPoP half of a request: the `DPoP` header, and the method + PUBLIC URL the receiver was called on. */
|
|
206
|
+
interface DpopRequest {
|
|
207
|
+
proof: string | null | undefined;
|
|
208
|
+
method: string;
|
|
209
|
+
url: string;
|
|
109
210
|
}
|
|
110
211
|
interface TicketVerifier {
|
|
111
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* Verify a ticket; with `scope`, also require that scope to be granted. Pass
|
|
214
|
+
* `dpop` with the request's DPoP header, method and public URL — a bound ticket
|
|
215
|
+
* is refused without it.
|
|
216
|
+
*/
|
|
112
217
|
verify(token: string, options?: {
|
|
113
218
|
scope?: string | string[];
|
|
219
|
+
dpop?: DpopRequest;
|
|
114
220
|
}): Promise<TicketPrincipal>;
|
|
115
221
|
}
|
|
116
222
|
declare function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier;
|
|
117
223
|
|
|
118
|
-
export { DEFAULT_ISSUER, MAX_ACT_DEPTH, NoWorkloadIdentityError, type PrincipalType, TICKET_FORMAT_VERSION, type TicketActor, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, createTicketClient, createTicketVerifier, fetchTicket };
|
|
224
|
+
export { DEFAULT_ISSUER, type DpopKey, type DpopRequest, MAX_ACT_DEPTH, NoWorkloadIdentityError, type PrincipalType, type ReplayStore, TICKET_FORMAT_VERSION, type TicketActor, type TicketAuthHeaders, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, accessTokenHash, createDpopKey, createDpopProof, createMemoryReplayStore, createTicketClient, createTicketVerifier, fetchTicket, normalizeHtu, verifyDpopProof };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AVATAR_TYPES, c as AddressOwnership, B as BeginLoginOptions, d as BidProfile, C as CreateSsoClientOptions, D as DEFAULT_SESSION_MAX_AGE, e as Discovery, I as
|
|
2
|
-
import 'jose';
|
|
1
|
+
export { A as AVATAR_TYPES, c as AddressOwnership, B as BeginLoginOptions, d as BidProfile, C as CreateSsoClientOptions, D as DEFAULT_SESSION_MAX_AGE, e as Discovery, I as IntrospectOptions, f as IntrospectedToken, g as InvitationOutcome, h as InvitationResult, i as InviteUsersInput, J as JwksCache, j as JwksCacheOptions, k as JwksError, l as JwksUnavailableError, m as JwksUnknownKeyError, L as LoginResult, n as LoginStart, o as LogoutToken, M as MAX_AVATAR_BYTES, p as MAX_INVITATIONS, q as MigrationState, r as MigrationStatus, b as SessionPayload, s as SsoAppApiError, t as SsoAvatarRejectedError, u as SsoClaims, S as SsoClient, a as SsoConfig, v as SsoConfigError, w as SsoError, x as SsoInsufficientScopeError, y as SsoIntrospectionUnavailableError, z as SsoReauthError, E as SsoTokenInactiveError, F as TokenSet, T as TokenStore, G as cookieHeader, H as createJwksCache, K as createSsoClient, N as loadSsoConfig, O as memoryTokenStore, P as readCookie, Q as signSession, R as signValue, U as verifySession, V as verifyValue } from './session-CIm4_raQ.js';
|
|
2
|
+
import { JWK } from 'jose';
|
|
3
3
|
|
|
4
4
|
declare const DEFAULT_ISSUER = "https://id.broberg.ai";
|
|
5
5
|
/** No platform identity to exchange: not on Fly, not in a GitHub Actions job with id-token: write. */
|
|
@@ -32,6 +32,17 @@ interface TicketClientOptions {
|
|
|
32
32
|
flyOidc?: (aud: string) => Promise<string>;
|
|
33
33
|
/** Whether the Fly socket is present. Default: checks /.fly/api. */
|
|
34
34
|
onFly?: () => boolean;
|
|
35
|
+
/**
|
|
36
|
+
* F084.157 — ask BID for DPoP-BOUND tickets. One ES256 key per client
|
|
37
|
+
* (non-extractable, never stored); every call then needs a proof, which
|
|
38
|
+
* `authHeaders` makes. Default false: bearer tickets, exactly as before.
|
|
39
|
+
*/
|
|
40
|
+
dpop?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** Headers for one call to a receiver. With `dpop`, `authorization` is `DPoP <ticket>` and `dpop` is the proof. */
|
|
43
|
+
interface TicketAuthHeaders {
|
|
44
|
+
authorization: string;
|
|
45
|
+
dpop?: string;
|
|
35
46
|
}
|
|
36
47
|
interface TicketClient {
|
|
37
48
|
/** A ticket for `audience` (BID's short name, e.g. "discovery"), from cache while it is fresh. */
|
|
@@ -39,6 +50,17 @@ interface TicketClient {
|
|
|
39
50
|
audience: string;
|
|
40
51
|
scope?: string;
|
|
41
52
|
}): Promise<string>;
|
|
53
|
+
/**
|
|
54
|
+
* F084.157 — the headers for ONE request to a receiver: a ticket plus, with
|
|
55
|
+
* `dpop`, a fresh proof bound to this method and URL (use it once). Without
|
|
56
|
+
* `dpop`: `{ authorization: "Bearer <ticket>" }`.
|
|
57
|
+
*/
|
|
58
|
+
authHeaders(opts: {
|
|
59
|
+
audience: string;
|
|
60
|
+
scope?: string;
|
|
61
|
+
method: string;
|
|
62
|
+
url: string;
|
|
63
|
+
}): Promise<TicketAuthHeaders>;
|
|
42
64
|
}
|
|
43
65
|
declare function createTicketClient(options?: TicketClientOptions): TicketClient;
|
|
44
66
|
/** A ticket from the process-wide client (one cache per process). */
|
|
@@ -47,6 +69,68 @@ declare function fetchTicket(opts: {
|
|
|
47
69
|
scope?: string;
|
|
48
70
|
}): Promise<string>;
|
|
49
71
|
|
|
72
|
+
/**
|
|
73
|
+
* F084.157 — DPoP (RFC 9449): a bound ticket works only for whoever holds the key
|
|
74
|
+
* (broberg-id-F087.16, joint design #2572/#2574).
|
|
75
|
+
*
|
|
76
|
+
* A DPoP-bound ticket carries `cnf.jkt`, the thumbprint of the caller's public
|
|
77
|
+
* key. With every request the caller signs a short PROOF (method, URL, time, a
|
|
78
|
+
* hash of the ticket) with the matching private key. A ticket copied out of a log
|
|
79
|
+
* is useless on another machine: it has the ticket, not the key.
|
|
80
|
+
*
|
|
81
|
+
* THE RULE THAT MAKES IT PROTECTION (agreed with broberg-id, (A) in #2574): once a
|
|
82
|
+
* receiver verifies with this version, a ticket WITH cnf.jkt needs a valid proof —
|
|
83
|
+
* also while DPoP is otherwise optional. `requireDpop` only decides whether
|
|
84
|
+
* UNBOUND tickets are still accepted. Were bound tickets accepted without a proof
|
|
85
|
+
* during the rollout, a stolen bound ticket would work as bearer the whole time.
|
|
86
|
+
*
|
|
87
|
+
* Not here yet: server nonces (RFC 9449 §8). The jti replay store covers replay
|
|
88
|
+
* inside the iat window.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/** One process's DPoP key. The private half cannot be exported. */
|
|
92
|
+
interface DpopKey {
|
|
93
|
+
privateKey: CryptoKey;
|
|
94
|
+
publicJwk: JWK;
|
|
95
|
+
/** RFC 7638 thumbprint — what BID puts in the ticket's cnf.jkt. */
|
|
96
|
+
jkt: string;
|
|
97
|
+
}
|
|
98
|
+
/** A fresh ES256 key pair, private key non-extractable, never written anywhere. */
|
|
99
|
+
declare function createDpopKey(): Promise<DpopKey>;
|
|
100
|
+
/** `ath`: base64url(SHA-256(ticket)) — ties a proof to one ticket. */
|
|
101
|
+
declare function accessTokenHash(ticket: string): Promise<string>;
|
|
102
|
+
/** RFC 9449 §4.3: htu is compared without query and fragment. */
|
|
103
|
+
declare function normalizeHtu(url: string): string;
|
|
104
|
+
/** Sign one proof. Pass `ticket` when calling a resource server (adds ath); omit it for BID's token endpoint. */
|
|
105
|
+
declare function createDpopProof(key: DpopKey, opts: {
|
|
106
|
+
method: string;
|
|
107
|
+
url: string;
|
|
108
|
+
ticket?: string;
|
|
109
|
+
now?: () => number;
|
|
110
|
+
}): Promise<string>;
|
|
111
|
+
/**
|
|
112
|
+
* Remembers which proofs were already used. The default is in-memory, which
|
|
113
|
+
* holds on ONE machine: a receiver running on two machines must pass a shared
|
|
114
|
+
* store, or a proof replayed against the other machine inside the window passes.
|
|
115
|
+
*/
|
|
116
|
+
interface ReplayStore {
|
|
117
|
+
/** Record `key`; resolve true if it was already recorded within `ttlMs`. */
|
|
118
|
+
seen(key: string, ttlMs: number): Promise<boolean>;
|
|
119
|
+
}
|
|
120
|
+
declare function createMemoryReplayStore(now?: () => number): ReplayStore;
|
|
121
|
+
interface VerifyProofOptions {
|
|
122
|
+
method: string;
|
|
123
|
+
/** The receiver's PUBLIC URL for this request (behind a proxy the process sees an internal one). */
|
|
124
|
+
url: string;
|
|
125
|
+
ticket: string;
|
|
126
|
+
jkt: string;
|
|
127
|
+
replay: ReplayStore;
|
|
128
|
+
iatWindowSec: number;
|
|
129
|
+
now: () => number;
|
|
130
|
+
}
|
|
131
|
+
/** Throws SsoError naming the first thing that is wrong. */
|
|
132
|
+
declare function verifyDpopProof(proof: string, o: VerifyProofOptions): Promise<void>;
|
|
133
|
+
|
|
50
134
|
type PrincipalType = "human" | "service" | "agent";
|
|
51
135
|
/**
|
|
52
136
|
* F084.156 — one link of an RFC 8693 delegation chain. `act` is the next link
|
|
@@ -76,8 +160,8 @@ interface TicketPrincipal {
|
|
|
76
160
|
version: number;
|
|
77
161
|
/**
|
|
78
162
|
* F084.156 — the DPoP key thumbprint when the ticket is sender-constrained.
|
|
79
|
-
*
|
|
80
|
-
*
|
|
163
|
+
* Since 0.16.0 (F084.157) a ticket with cnf.jkt is only accepted together with
|
|
164
|
+
* a valid DPoP proof for that key — see `verify(token, { dpop })`.
|
|
81
165
|
*/
|
|
82
166
|
cnf: {
|
|
83
167
|
jkt: string;
|
|
@@ -106,13 +190,35 @@ interface TicketVerifierOptions {
|
|
|
106
190
|
warmUp?: boolean;
|
|
107
191
|
fetchImpl?: typeof fetch;
|
|
108
192
|
minRefetchIntervalMs?: number;
|
|
193
|
+
/**
|
|
194
|
+
* F084.157 — refuse UNBOUND tickets (bearer). Off by default during the
|
|
195
|
+
* rollout. A BOUND ticket (cnf.jkt) needs a valid proof either way.
|
|
196
|
+
*/
|
|
197
|
+
requireDpop?: boolean;
|
|
198
|
+
/** Where used proof ids are remembered. Default in-memory — ONE machine only; share one across machines. */
|
|
199
|
+
replayStore?: ReplayStore;
|
|
200
|
+
/** How far a proof's iat may be from now. Default 60 s. */
|
|
201
|
+
dpopIatWindowSec?: number;
|
|
202
|
+
/** Injectable clock for the DPoP checks (ms). */
|
|
203
|
+
now?: () => number;
|
|
204
|
+
}
|
|
205
|
+
/** The DPoP half of a request: the `DPoP` header, and the method + PUBLIC URL the receiver was called on. */
|
|
206
|
+
interface DpopRequest {
|
|
207
|
+
proof: string | null | undefined;
|
|
208
|
+
method: string;
|
|
209
|
+
url: string;
|
|
109
210
|
}
|
|
110
211
|
interface TicketVerifier {
|
|
111
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* Verify a ticket; with `scope`, also require that scope to be granted. Pass
|
|
214
|
+
* `dpop` with the request's DPoP header, method and public URL — a bound ticket
|
|
215
|
+
* is refused without it.
|
|
216
|
+
*/
|
|
112
217
|
verify(token: string, options?: {
|
|
113
218
|
scope?: string | string[];
|
|
219
|
+
dpop?: DpopRequest;
|
|
114
220
|
}): Promise<TicketPrincipal>;
|
|
115
221
|
}
|
|
116
222
|
declare function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier;
|
|
117
223
|
|
|
118
|
-
export { DEFAULT_ISSUER, MAX_ACT_DEPTH, NoWorkloadIdentityError, type PrincipalType, TICKET_FORMAT_VERSION, type TicketActor, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, createTicketClient, createTicketVerifier, fetchTicket };
|
|
224
|
+
export { DEFAULT_ISSUER, type DpopKey, type DpopRequest, MAX_ACT_DEPTH, NoWorkloadIdentityError, type PrincipalType, type ReplayStore, TICKET_FORMAT_VERSION, type TicketActor, type TicketAuthHeaders, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, accessTokenHash, createDpopKey, createDpopProof, createMemoryReplayStore, createTicketClient, createTicketVerifier, fetchTicket, normalizeHtu, verifyDpopProof };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,87 @@
|
|
|
1
|
-
import { SsoError, ALLOWED_ALGS, JwksUnavailableError, createJwksCache } from './chunk-
|
|
2
|
-
export { AVATAR_TYPES, DEFAULT_SESSION_MAX_AGE, JwksError, JwksUnavailableError, JwksUnknownKeyError, MAX_AVATAR_BYTES, MAX_INVITATIONS, SsoAppApiError, SsoAvatarRejectedError, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, cookieHeader, createJwksCache, createSsoClient, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue } from './chunk-
|
|
1
|
+
import { SsoError, ALLOWED_ALGS, JwksUnavailableError, createJwksCache } from './chunk-AVXQLEYK.js';
|
|
2
|
+
export { AVATAR_TYPES, DEFAULT_SESSION_MAX_AGE, JwksError, JwksUnavailableError, JwksUnknownKeyError, MAX_AVATAR_BYTES, MAX_INVITATIONS, SsoAppApiError, SsoAvatarRejectedError, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoIntrospectionUnavailableError, SsoReauthError, SsoTokenInactiveError, cookieHeader, createJwksCache, createSsoClient, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue } from './chunk-AVXQLEYK.js';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { request } from 'http';
|
|
5
|
-
import { decodeProtectedHeader, jwtVerify } from 'jose';
|
|
5
|
+
import { generateKeyPair, exportJWK, calculateJwkThumbprint, SignJWT, decodeProtectedHeader, importJWK, jwtVerify } from 'jose';
|
|
6
6
|
|
|
7
|
+
async function createDpopKey() {
|
|
8
|
+
const { privateKey, publicKey } = await generateKeyPair("ES256", { extractable: false });
|
|
9
|
+
const jwk = await exportJWK(publicKey);
|
|
10
|
+
const publicJwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
|
|
11
|
+
return { privateKey, publicJwk, jkt: await calculateJwkThumbprint(publicJwk) };
|
|
12
|
+
}
|
|
13
|
+
var enc = new TextEncoder();
|
|
14
|
+
var b64url = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
15
|
+
async function accessTokenHash(ticket) {
|
|
16
|
+
return b64url(await crypto.subtle.digest("SHA-256", enc.encode(ticket)));
|
|
17
|
+
}
|
|
18
|
+
function normalizeHtu(url) {
|
|
19
|
+
const u = new URL(url);
|
|
20
|
+
return `${u.protocol}//${u.host}${u.pathname}`;
|
|
21
|
+
}
|
|
22
|
+
async function createDpopProof(key, opts) {
|
|
23
|
+
const payload = {
|
|
24
|
+
jti: crypto.randomUUID(),
|
|
25
|
+
htm: opts.method.toUpperCase(),
|
|
26
|
+
htu: normalizeHtu(opts.url),
|
|
27
|
+
iat: Math.floor((opts.now?.() ?? Date.now()) / 1e3)
|
|
28
|
+
};
|
|
29
|
+
if (opts.ticket !== void 0) payload.ath = await accessTokenHash(opts.ticket);
|
|
30
|
+
return new SignJWT(payload).setProtectedHeader({ typ: "dpop+jwt", alg: "ES256", jwk: key.publicJwk }).sign(key.privateKey);
|
|
31
|
+
}
|
|
32
|
+
function createMemoryReplayStore(now = () => Date.now()) {
|
|
33
|
+
const entries = /* @__PURE__ */ new Map();
|
|
34
|
+
return {
|
|
35
|
+
async seen(key, ttlMs) {
|
|
36
|
+
const t = now();
|
|
37
|
+
for (const [k, until] of entries) if (until <= t) entries.delete(k);
|
|
38
|
+
if (entries.has(key)) return true;
|
|
39
|
+
entries.set(key, t + ttlMs);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
var ASYMMETRIC = /* @__PURE__ */ new Set(["ES256", "ES384", "ES512", "EdDSA", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512"]);
|
|
45
|
+
async function verifyDpopProof(proof, o) {
|
|
46
|
+
let header;
|
|
47
|
+
try {
|
|
48
|
+
header = decodeProtectedHeader(proof);
|
|
49
|
+
} catch {
|
|
50
|
+
throw new SsoError("DPoP proof is not a JWS");
|
|
51
|
+
}
|
|
52
|
+
if (header.typ !== "dpop+jwt") throw new SsoError(`DPoP proof has typ ${JSON.stringify(header.typ)}, not "dpop+jwt"`);
|
|
53
|
+
if (!header.alg || !ASYMMETRIC.has(header.alg)) throw new SsoError(`DPoP proof alg ${JSON.stringify(header.alg)} is not an accepted asymmetric algorithm`);
|
|
54
|
+
const jwk = header.jwk;
|
|
55
|
+
if (!jwk || typeof jwk !== "object") throw new SsoError("DPoP proof carries no jwk");
|
|
56
|
+
if ("d" in jwk) throw new SsoError("DPoP proof jwk contains a private key");
|
|
57
|
+
let payload;
|
|
58
|
+
try {
|
|
59
|
+
const key = await importJWK(jwk, header.alg);
|
|
60
|
+
({ payload } = await jwtVerify(proof, key, { typ: "dpop+jwt", algorithms: [header.alg] }));
|
|
61
|
+
} catch (e) {
|
|
62
|
+
throw new SsoError(`DPoP proof signature rejected: ${e instanceof Error ? e.message : String(e)}`);
|
|
63
|
+
}
|
|
64
|
+
if (await calculateJwkThumbprint(jwk) !== o.jkt) throw new SsoError("DPoP proof key does not match the ticket's cnf.jkt");
|
|
65
|
+
if (payload.htm !== o.method.toUpperCase()) throw new SsoError(`DPoP proof is for ${JSON.stringify(payload.htm)}, the request is ${o.method.toUpperCase()}`);
|
|
66
|
+
let htu;
|
|
67
|
+
try {
|
|
68
|
+
htu = normalizeHtu(String(payload.htu));
|
|
69
|
+
} catch {
|
|
70
|
+
throw new SsoError("DPoP proof htu is not a URL");
|
|
71
|
+
}
|
|
72
|
+
if (htu !== normalizeHtu(o.url)) throw new SsoError(`DPoP proof is for ${htu}, the request is ${normalizeHtu(o.url)}`);
|
|
73
|
+
const nowSec = Math.floor(o.now() / 1e3);
|
|
74
|
+
if (typeof payload.iat !== "number" || Math.abs(nowSec - payload.iat) > o.iatWindowSec) {
|
|
75
|
+
throw new SsoError(`DPoP proof iat is outside \xB1${o.iatWindowSec}s`);
|
|
76
|
+
}
|
|
77
|
+
if (payload.ath !== await accessTokenHash(o.ticket)) throw new SsoError("DPoP proof ath does not match this ticket");
|
|
78
|
+
if (typeof payload.jti !== "string" || payload.jti === "") throw new SsoError("DPoP proof has no jti");
|
|
79
|
+
if (await o.replay.seen(`${o.jkt}:${payload.jti}`, o.iatWindowSec * 2 * 1e3)) {
|
|
80
|
+
throw new SsoError("DPoP proof was already used (jti replay)");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/fetch-ticket.ts
|
|
7
85
|
var DEFAULT_ISSUER = "https://id.broberg.ai";
|
|
8
86
|
var FLY_SOCKET = "/.fly/api";
|
|
9
87
|
var NoWorkloadIdentityError = class extends Error {
|
|
@@ -65,6 +143,8 @@ function createTicketClient(options = {}) {
|
|
|
65
143
|
const onFly = options.onFly ?? (() => !!env.FLY_APP_NAME && existsSync(FLY_SOCKET));
|
|
66
144
|
const cache = /* @__PURE__ */ new Map();
|
|
67
145
|
const inFlight = /* @__PURE__ */ new Map();
|
|
146
|
+
let dpopKey = null;
|
|
147
|
+
const ownKey = () => dpopKey ??= createDpopKey();
|
|
68
148
|
async function platformToken() {
|
|
69
149
|
if (onFly()) return flyOidc(issuer);
|
|
70
150
|
const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
@@ -96,11 +176,10 @@ function createTicketClient(options = {}) {
|
|
|
96
176
|
if (scope) form.set("scope", scope);
|
|
97
177
|
let res;
|
|
98
178
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
179
|
+
const tokenUrl = `${issuer}/oauth2/token`;
|
|
180
|
+
const headers = { "content-type": "application/x-www-form-urlencoded" };
|
|
181
|
+
if (options.dpop) headers.dpop = await createDpopProof(await ownKey(), { method: "POST", url: tokenUrl, now });
|
|
182
|
+
res = await fetchImpl(tokenUrl, { method: "POST", headers, body: form });
|
|
104
183
|
} catch (e) {
|
|
105
184
|
throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
106
185
|
}
|
|
@@ -119,7 +198,12 @@ function createTicketClient(options = {}) {
|
|
|
119
198
|
const exp = jwtExp(body.access_token) ?? nowSec + (body.expires_in ?? 300);
|
|
120
199
|
return { token: body.access_token, exp };
|
|
121
200
|
}
|
|
122
|
-
|
|
201
|
+
const client = {
|
|
202
|
+
async authHeaders({ audience, scope, method, url }) {
|
|
203
|
+
const ticket = await client.get({ audience, scope });
|
|
204
|
+
if (!options.dpop) return { authorization: `Bearer ${ticket}` };
|
|
205
|
+
return { authorization: `DPoP ${ticket}`, dpop: await createDpopProof(await ownKey(), { method, url, ticket, now }) };
|
|
206
|
+
},
|
|
123
207
|
async get({ audience, scope }) {
|
|
124
208
|
const key = `${audience} ${scope ?? ""}`;
|
|
125
209
|
const hit = cache.get(key);
|
|
@@ -134,6 +218,7 @@ function createTicketClient(options = {}) {
|
|
|
134
218
|
return p;
|
|
135
219
|
}
|
|
136
220
|
};
|
|
221
|
+
return client;
|
|
137
222
|
}
|
|
138
223
|
var shared = null;
|
|
139
224
|
function fetchTicket(opts) {
|
|
@@ -149,8 +234,12 @@ function createTicketVerifier(options) {
|
|
|
149
234
|
audience,
|
|
150
235
|
maxLifetimeSec = 15 * 60,
|
|
151
236
|
clockToleranceSec = 30,
|
|
152
|
-
fetchImpl = fetch
|
|
237
|
+
fetchImpl = fetch,
|
|
238
|
+
requireDpop = false,
|
|
239
|
+
dpopIatWindowSec = 60,
|
|
240
|
+
now = () => Date.now()
|
|
153
241
|
} = options;
|
|
242
|
+
const replayStore = options.replayStore ?? createMemoryReplayStore(now);
|
|
154
243
|
if (!issuer) throw new SsoError("createTicketVerifier needs an issuer");
|
|
155
244
|
if (!audience) throw new SsoError("createTicketVerifier needs this receiver's audience");
|
|
156
245
|
let cachePromise = null;
|
|
@@ -234,6 +323,20 @@ function createTicketVerifier(options) {
|
|
|
234
323
|
const act = payload.act === void 0 ? null : readActor(payload.act, 1);
|
|
235
324
|
const cnfRaw = payload.cnf;
|
|
236
325
|
const cnf = cnfRaw && typeof cnfRaw.jkt === "string" && cnfRaw.jkt ? { jkt: cnfRaw.jkt } : null;
|
|
326
|
+
if (cnf) {
|
|
327
|
+
if (!opts.dpop?.proof) throw new SsoError("ticket is DPoP-bound (cnf.jkt) but no DPoP proof was presented");
|
|
328
|
+
await verifyDpopProof(opts.dpop.proof, {
|
|
329
|
+
method: opts.dpop.method,
|
|
330
|
+
url: opts.dpop.url,
|
|
331
|
+
ticket: token,
|
|
332
|
+
jkt: cnf.jkt,
|
|
333
|
+
replay: replayStore,
|
|
334
|
+
iatWindowSec: dpopIatWindowSec,
|
|
335
|
+
now
|
|
336
|
+
});
|
|
337
|
+
} else if (requireDpop) {
|
|
338
|
+
throw new SsoError(cnfRaw !== void 0 ? "ticket has a cnf without a string jkt \u2014 DPoP is required here" : "this receiver requires DPoP; the ticket is not bound (no cnf.jkt)");
|
|
339
|
+
}
|
|
237
340
|
return {
|
|
238
341
|
principal: payload.sub,
|
|
239
342
|
type,
|
|
@@ -258,6 +361,6 @@ function readActor(raw, depth) {
|
|
|
258
361
|
return link.act === void 0 ? { sub: link.sub } : { sub: link.sub, act: readActor(link.act, depth + 1) };
|
|
259
362
|
}
|
|
260
363
|
|
|
261
|
-
export { DEFAULT_ISSUER, MAX_ACT_DEPTH, NoWorkloadIdentityError, TICKET_FORMAT_VERSION, TicketExchangeError, TicketUnavailableError, createTicketClient, createTicketVerifier, fetchTicket };
|
|
364
|
+
export { DEFAULT_ISSUER, MAX_ACT_DEPTH, NoWorkloadIdentityError, TICKET_FORMAT_VERSION, TicketExchangeError, TicketUnavailableError, accessTokenHash, createDpopKey, createDpopProof, createMemoryReplayStore, createTicketClient, createTicketVerifier, fetchTicket, normalizeHtu, verifyDpopProof };
|
|
262
365
|
//# sourceMappingURL=index.js.map
|
|
263
366
|
//# sourceMappingURL=index.js.map
|