@idp.global/sdk 19.2.1 → 19.4.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/changelog.md +58 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts_browser/classes.idpclient.js +1 -1
- package/dist_ts_browser/classes.idprequests.js +1 -1
- package/dist_ts_browser/classes.relyingpartybearerclient.d.ts +276 -0
- package/dist_ts_browser/classes.relyingpartybearerclient.js +591 -0
- package/dist_ts_browser/index.d.ts +1 -0
- package/dist_ts_browser/index.js +2 -1
- package/dist_ts_browser/plugins.js +1 -1
- package/dist_ts_browser/secret-delivery.js +1 -1
- package/dist_ts_relyingparty/classes.authorityobserver.d.ts +23 -0
- package/dist_ts_relyingparty/classes.authorityobserver.js +68 -13
- package/dist_ts_relyingparty/classes.errors.d.ts +11 -1
- package/dist_ts_relyingparty/classes.errors.js +6 -1
- package/dist_ts_relyingparty/classes.relyingpartyroutes.d.ts +74 -2
- package/dist_ts_relyingparty/classes.relyingpartyroutes.js +289 -14
- package/dist_ts_relyingparty/classes.relyingpartysessions.d.ts +209 -7
- package/dist_ts_relyingparty/classes.relyingpartysessions.js +734 -87
- package/dist_ts_relyingparty/classes.rpmodels.d.ts +71 -8
- package/dist_ts_relyingparty/classes.rpmodels.js +224 -10
- package/dist_ts_relyingparty/contract.bearer.d.ts +82 -0
- package/dist_ts_relyingparty/contract.bearer.js +9 -0
- package/dist_ts_relyingparty/helpers.bearer.d.ts +48 -0
- package/dist_ts_relyingparty/helpers.bearer.js +199 -0
- package/dist_ts_relyingparty/index.d.ts +1 -0
- package/dist_ts_relyingparty/index.js +2 -1
- package/dist_ts_relyingparty/interfaces.d.ts +122 -2
- package/dist_ts_relyingparty/testing.d.ts +22 -0
- package/dist_ts_relyingparty/testing.js +42 -1
- package/package.json +9 -11
- package/readme.md +353 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_browser/classes.relyingpartybearerclient.ts +821 -0
- package/ts_browser/index.ts +1 -0
- package/ts_relyingparty/classes.authorityobserver.ts +80 -12
- package/ts_relyingparty/classes.errors.ts +16 -1
- package/ts_relyingparty/classes.relyingpartyroutes.ts +340 -16
- package/ts_relyingparty/classes.relyingpartysessions.ts +907 -94
- package/ts_relyingparty/classes.rpmodels.ts +131 -9
- package/ts_relyingparty/contract.bearer.ts +99 -0
- package/ts_relyingparty/helpers.bearer.ts +259 -0
- package/ts_relyingparty/index.ts +1 -0
- package/ts_relyingparty/interfaces.ts +144 -2
- package/ts_relyingparty/testing.ts +60 -0
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import type { IOidcCompletion, TOidcCompletionFailureStage, TOidcPrompt, TOidcTokenHint } from './classes.idpoidcprovider.js';
|
|
2
|
+
import type { TRelyingPartyBearerClientKind } from './contract.bearer.js';
|
|
3
|
+
|
|
4
|
+
export type {
|
|
5
|
+
IRelyingPartyBearerErrorAnswer,
|
|
6
|
+
IRelyingPartyBearerLogoutAnswer,
|
|
7
|
+
IRelyingPartyBearerMessage,
|
|
8
|
+
IRelyingPartyBearerSessionAnswer,
|
|
9
|
+
IRelyingPartyBearerSessionInfo,
|
|
10
|
+
IRelyingPartyBearerTokenAnswer,
|
|
11
|
+
TRelyingPartyBearerClientKind,
|
|
12
|
+
TRelyingPartyBearerDeliveryError,
|
|
13
|
+
TRelyingPartyBearerErrorCode,
|
|
14
|
+
} from './contract.bearer.js';
|
|
2
15
|
|
|
3
16
|
/** A session as the application reads it; no token and no sealed value is part of it. */
|
|
4
17
|
export interface IRelyingPartySession {
|
|
@@ -35,6 +48,21 @@ export interface IRelyingPartySessionOpening {
|
|
|
35
48
|
sessionCookie: string;
|
|
36
49
|
}
|
|
37
50
|
|
|
51
|
+
/** What `sessionsOfPrincipal` reads: how many sessions, and after which page. */
|
|
52
|
+
export interface IRelyingPartySessionsPageOptions {
|
|
53
|
+
/** 1 to `RELYING_PARTY_MAX_SESSIONS_PAGE_LIMIT`; `RELYING_PARTY_DEFAULT_SESSIONS_PAGE_LIMIT` when absent */
|
|
54
|
+
limit?: number;
|
|
55
|
+
/** the `nextCursor` of the page before; absent for the first page */
|
|
56
|
+
cursor?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One page of a principal's live sessions, newest first. */
|
|
60
|
+
export interface IRelyingPartySessionsPage {
|
|
61
|
+
sessions: IRelyingPartySession[];
|
|
62
|
+
/** opaque; reads the next page as `cursor`. Absent on the last page. */
|
|
63
|
+
nextCursor?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
38
66
|
/**
|
|
39
67
|
* What `startLogin` carries besides the return path. `intent` is the
|
|
40
68
|
* application's own text — the invitation a login accepts, the account it
|
|
@@ -62,12 +90,99 @@ export interface IRelyingPartyLoginStart {
|
|
|
62
90
|
intentCookie: string;
|
|
63
91
|
}
|
|
64
92
|
|
|
93
|
+
/**
|
|
94
|
+
* The exact return targets a bearer client may name, per client kind. A kind
|
|
95
|
+
* without targets is not offered. Matching is by equality, never by prefix
|
|
96
|
+
* or pattern; a loopback target matches on any port.
|
|
97
|
+
*/
|
|
98
|
+
export interface IRelyingPartyBearerReturnTargets {
|
|
99
|
+
/** same-origin paths such as `/signed-in`, without query or fragment */
|
|
100
|
+
web?: readonly string[];
|
|
101
|
+
/** `http://127.0.0.1/<path>` or `http://[::1]/<path>` without a port; the client names its own port */
|
|
102
|
+
loopback?: readonly string[];
|
|
103
|
+
/** whole URIs of a private-use scheme with a dot in it, such as `com.example.app:/signed-in`, without query or fragment */
|
|
104
|
+
custom_scheme?: readonly string[];
|
|
105
|
+
/** bare origins such as `https://workspace.example`; the popup posts to exactly the one its client named */
|
|
106
|
+
popup?: readonly string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Bearer hand-off mode: the application's own clients sign in through this
|
|
111
|
+
* relying party and receive its own access and refresh tokens instead of a
|
|
112
|
+
* cookie.
|
|
113
|
+
*/
|
|
114
|
+
export interface IRelyingPartyBearerOptions {
|
|
115
|
+
returnTargets: IRelyingPartyBearerReturnTargets;
|
|
116
|
+
/** how long an access token lives, in seconds; 900 by default, never past its session */
|
|
117
|
+
accessTokenMaxAgeSeconds?: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** What a bearer client sends to start its login. Every field is checked as untrusted input. */
|
|
121
|
+
export interface IRelyingPartyBearerLoginRequest {
|
|
122
|
+
clientKind: TRelyingPartyBearerClientKind;
|
|
123
|
+
/** one of the configured return targets of the kind, exactly; a loopback target with the client's port */
|
|
124
|
+
returnTarget: string;
|
|
125
|
+
/** BASE64URL(SHA-256(code verifier)), 43 characters */
|
|
126
|
+
codeChallenge: string;
|
|
127
|
+
/** only `S256` is accepted */
|
|
128
|
+
codeChallengeMethod: 'S256';
|
|
129
|
+
/** the client's own correlation value, 1 to 256 characters of `A-Z a-z 0-9 . _ ~ -`, handed back with the outcome */
|
|
130
|
+
state?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The redirect that starts a bearer login. It sets no cookie. */
|
|
134
|
+
export interface IRelyingPartyBearerLoginStart {
|
|
135
|
+
authorizationUrl: URL;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Where the outcome of a bearer login goes, as its client named it at the start. */
|
|
139
|
+
export interface IRelyingPartyBearerDelivery {
|
|
140
|
+
clientKind: TRelyingPartyBearerClientKind;
|
|
141
|
+
returnTarget: string;
|
|
142
|
+
/** the client's `state`, when it gave one */
|
|
143
|
+
state?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A bearer login that came back from the provider: its session is open, and
|
|
148
|
+
* its single-use code is what the client claims the tokens with. The code is
|
|
149
|
+
* no token; only its hash is stored.
|
|
150
|
+
*/
|
|
151
|
+
export interface IRelyingPartyBearerHandoff {
|
|
152
|
+
session: IRelyingPartySession;
|
|
153
|
+
delivery: IRelyingPartyBearerDelivery;
|
|
154
|
+
code: string;
|
|
155
|
+
/** epoch milliseconds, at most `BEARER_HANDOFF_MAX_AGE_SECONDS` after the callback */
|
|
156
|
+
expiresAt: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The relying party's own tokens of a bearer session. The refresh token
|
|
161
|
+
* rotates on every use; a rotated one coming back ends the session.
|
|
162
|
+
*/
|
|
163
|
+
export interface IRelyingPartyBearerTokens {
|
|
164
|
+
session: IRelyingPartySession;
|
|
165
|
+
/** epoch milliseconds on the relying party's clock; a client with another clock counts from it */
|
|
166
|
+
issuedAt: number;
|
|
167
|
+
accessToken: string;
|
|
168
|
+
/** epoch milliseconds */
|
|
169
|
+
accessTokenExpiresAt: number;
|
|
170
|
+
refreshToken: string;
|
|
171
|
+
/** epoch milliseconds: the end of the session, which no refresh extends */
|
|
172
|
+
refreshTokenExpiresAt: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
65
175
|
/** A provider access token usable right now, with the subject it belongs to. */
|
|
66
176
|
export interface IProviderToken {
|
|
67
177
|
accessToken: string;
|
|
68
178
|
subject: string;
|
|
69
179
|
/** epoch milliseconds; `0` when the provider named no lifetime */
|
|
70
180
|
expiresAt: number;
|
|
181
|
+
/**
|
|
182
|
+
* `true` only on the answer to `rejectedAccessToken`, and only when a
|
|
183
|
+
* rotation that this call started issued the token.
|
|
184
|
+
*/
|
|
185
|
+
rotatedAfterRejection?: boolean;
|
|
71
186
|
}
|
|
72
187
|
|
|
73
188
|
/** What an authority observer needs from the session owner. `RelyingPartySessions` implements it. */
|
|
@@ -75,8 +190,21 @@ export interface IAuthorityTokenSource {
|
|
|
75
190
|
/**
|
|
76
191
|
* The access token of a login session, rotated first when it is due. The
|
|
77
192
|
* session must belong to `expectedPrincipalIdArg`. Throws `AuthorityTokenError`.
|
|
193
|
+
*
|
|
194
|
+
* `rejectedAccessToken` is a token idp.global answered inactive for. The
|
|
195
|
+
* source must rotate the session's token then, whatever is stored, and set
|
|
196
|
+
* `rotatedAfterRejection` only when a rotation that this call started issued
|
|
197
|
+
* the token. A refused refresh ends the session and throws
|
|
198
|
+
* `authority_login_required`. A rotation that started before the call, one
|
|
199
|
+
* another replica holds, or a session without a refresh token marks
|
|
200
|
+
* nothing. A source that never rotates or never marks its answer can never
|
|
201
|
+
* refuse: the observer answers `authority_unavailable` (fail-closed).
|
|
78
202
|
*/
|
|
79
|
-
liveProviderToken(
|
|
203
|
+
liveProviderToken(
|
|
204
|
+
sessionIdArg: string,
|
|
205
|
+
expectedPrincipalIdArg: string,
|
|
206
|
+
optionsArg?: { rejectedAccessToken?: string },
|
|
207
|
+
): Promise<IProviderToken>;
|
|
80
208
|
}
|
|
81
209
|
|
|
82
210
|
/**
|
|
@@ -123,7 +251,15 @@ export type TRelyingPartySessionEndReason =
|
|
|
123
251
|
/** its access token is dead and there is nothing to rotate it with */
|
|
124
252
|
| 'access_token_unusable'
|
|
125
253
|
/** a rotation found it revoked by someone else */
|
|
126
|
-
| 'revoked_elsewhere'
|
|
254
|
+
| 'revoked_elsewhere'
|
|
255
|
+
/** its bearer hand-off code expired before a client claimed it */
|
|
256
|
+
| 'handoff_unclaimed'
|
|
257
|
+
/** its bearer hand-off code was claimed with a verifier that does not match its challenge */
|
|
258
|
+
| 'handoff_verifier_mismatch'
|
|
259
|
+
/** its bearer hand-off code came back after it was claimed */
|
|
260
|
+
| 'handoff_code_reused'
|
|
261
|
+
/** a bearer refresh token that was already rotated came back: the whole family ends */
|
|
262
|
+
| 'refresh_token_reused';
|
|
127
263
|
|
|
128
264
|
export type TRelyingPartyEventName =
|
|
129
265
|
| 'idp_token_refresh_failed'
|
|
@@ -133,6 +269,12 @@ export type TRelyingPartyEventName =
|
|
|
133
269
|
| 'oidc_token_revocation_failed'
|
|
134
270
|
| 'oidc_token_revocation_timed_out'
|
|
135
271
|
| 'login_attempt_sweep_failed'
|
|
272
|
+
/** expired bearer hand-offs and token generations could not be swept */
|
|
273
|
+
| 'bearer_sweep_failed'
|
|
274
|
+
/** sessions an hour past their end could not be swept */
|
|
275
|
+
| 'session_sweep_failed'
|
|
276
|
+
/** a bearer hand-off code or refresh token came back after its use; `reason` names which, the session ended */
|
|
277
|
+
| 'bearer_credential_reused'
|
|
136
278
|
/** an application hook threw; the step it belonged to ran on regardless */
|
|
137
279
|
| 'hook_failed'
|
|
138
280
|
/** a session whose opening failed could not be revoked; its provider tokens went back anyway */
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
type IOidcRefreshedTokens,
|
|
13
13
|
type TOidcTokenHint,
|
|
14
14
|
} from './classes.idpoidcprovider.js';
|
|
15
|
+
import { pkceChallengeOf, RELYING_PARTY_BEARER_POPUP_DATA_ID } from './helpers.bearer.js';
|
|
16
|
+
import type { IRelyingPartyBearerMessage } from './interfaces.js';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* `@idp.global/sdk/relyingparty/testing`: an in-process idp.global for the
|
|
@@ -350,3 +352,61 @@ export class RelyingPartyProviderDouble implements IOidcProvider {
|
|
|
350
352
|
return !tokenArg.revoked && (tokenArg.expiresAt === 0 || tokenArg.expiresAt > this.#now());
|
|
351
353
|
}
|
|
352
354
|
}
|
|
355
|
+
|
|
356
|
+
// ---- bearer clients ----
|
|
357
|
+
|
|
358
|
+
/** A PKCE pair as a bearer client makes one: the verifier it keeps and the S256 challenge it sends. */
|
|
359
|
+
export const createBearerPkcePair = (): { verifier: string; challenge: string } => {
|
|
360
|
+
const verifier = plugins.crypto.randomBytes(32).toString('base64url');
|
|
361
|
+
return { verifier, challenge: pkceChallengeOf(verifier) };
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
/** What a bearer delivery handed its client. */
|
|
365
|
+
export interface IBearerDeliveryRead {
|
|
366
|
+
/** a redirect: its location without the query; a popup page: the origin it posts to */
|
|
367
|
+
target: string;
|
|
368
|
+
/** the popup page's message as it would be posted; absent for a redirect */
|
|
369
|
+
message?: IRelyingPartyBearerMessage;
|
|
370
|
+
code?: string;
|
|
371
|
+
error?: string;
|
|
372
|
+
state?: string;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const POPUP_DATA = new RegExp(
|
|
376
|
+
`<script type="application/json" id="${RELYING_PARTY_BEARER_POPUP_DATA_ID}">([^<]*)</script>`,
|
|
377
|
+
'u',
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Reads the answer of a bearer login's callback the way its client would:
|
|
382
|
+
* the `code`, `error` and `state` of a redirect's location, or the message
|
|
383
|
+
* and target origin of a popup page. Anything else throws.
|
|
384
|
+
*/
|
|
385
|
+
export const readBearerDelivery = async (responseArg: Response): Promise<IBearerDeliveryRead> => {
|
|
386
|
+
const location = responseArg.headers.get('location');
|
|
387
|
+
if (responseArg.status === 302 && location) {
|
|
388
|
+
const url = new URL(location);
|
|
389
|
+
const read: IBearerDeliveryRead = { target: '' };
|
|
390
|
+
for (const name of ['code', 'error', 'state'] as const) {
|
|
391
|
+
const value = url.searchParams.get(name);
|
|
392
|
+
if (value !== null) {
|
|
393
|
+
read[name] = value;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
url.search = '';
|
|
397
|
+
read.target = url.href;
|
|
398
|
+
return read;
|
|
399
|
+
}
|
|
400
|
+
const data = responseArg.status === 200 ? POPUP_DATA.exec(await responseArg.text())?.[1] : undefined;
|
|
401
|
+
if (data === undefined) {
|
|
402
|
+
throw new Error(`readBearerDelivery: a response with status ${responseArg.status} is no bearer delivery`);
|
|
403
|
+
}
|
|
404
|
+
const delivery = JSON.parse(data) as { targetOrigin: string; message: IRelyingPartyBearerMessage };
|
|
405
|
+
return {
|
|
406
|
+
target: delivery.targetOrigin,
|
|
407
|
+
message: delivery.message,
|
|
408
|
+
...(delivery.message.code !== undefined ? { code: delivery.message.code } : {}),
|
|
409
|
+
...(delivery.message.error !== undefined ? { error: delivery.message.error } : {}),
|
|
410
|
+
...(delivery.message.state !== undefined ? { state: delivery.message.state } : {}),
|
|
411
|
+
};
|
|
412
|
+
};
|