@idp.global/sdk 19.4.0 → 19.6.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 +60 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts_browser/classes.relyingpartybearerclient.d.ts +40 -3
- package/dist_ts_browser/classes.relyingpartybearerclient.js +67 -12
- package/dist_ts_relyingparty/classes.errors.d.ts +2 -0
- package/dist_ts_relyingparty/classes.errors.js +2 -1
- package/dist_ts_relyingparty/classes.idpoidcprovider.d.ts +69 -11
- package/dist_ts_relyingparty/classes.idpoidcprovider.js +127 -17
- package/dist_ts_relyingparty/classes.relyingpartysessions.d.ts +128 -18
- package/dist_ts_relyingparty/classes.relyingpartysessions.js +334 -69
- package/dist_ts_relyingparty/classes.rpmodels.d.ts +21 -4
- package/dist_ts_relyingparty/classes.rpmodels.js +35 -8
- package/dist_ts_relyingparty/contract.bearer.d.ts +37 -0
- package/dist_ts_relyingparty/contract.bearer.js +5 -1
- package/dist_ts_relyingparty/helpers.bearer.d.ts +2 -3
- package/dist_ts_relyingparty/helpers.bearer.js +4 -6
- package/dist_ts_relyingparty/helpers.cookies.d.ts +6 -0
- package/dist_ts_relyingparty/helpers.cookies.js +8 -1
- package/dist_ts_relyingparty/interfaces.d.ts +73 -3
- package/dist_ts_relyingparty/testing.js +8 -4
- package/package.json +3 -3
- package/readme.md +280 -34
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_browser/classes.relyingpartybearerclient.ts +98 -18
- package/ts_relyingparty/classes.errors.ts +3 -0
- package/ts_relyingparty/classes.idpoidcprovider.ts +183 -17
- package/ts_relyingparty/classes.relyingpartysessions.ts +393 -78
- package/ts_relyingparty/classes.rpmodels.ts +27 -4
- package/ts_relyingparty/contract.bearer.ts +46 -0
- package/ts_relyingparty/helpers.bearer.ts +3 -6
- package/ts_relyingparty/helpers.cookies.ts +13 -0
- package/ts_relyingparty/interfaces.ts +76 -7
- package/ts_relyingparty/testing.ts +7 -3
|
@@ -129,7 +129,9 @@ export class RelyingPartyLoginAttemptDoc extends plugins.nosqldb.SmartDataDbDoc<
|
|
|
129
129
|
/**
|
|
130
130
|
* The single-use code a bearer login hands its client. Only the hash of the
|
|
131
131
|
* code is stored; the code is bound to the client's S256 challenge, lives at
|
|
132
|
-
* most a minute and is consumed by one conditional update.
|
|
132
|
+
* most a minute and is consumed by one conditional update. The `sessionId`
|
|
133
|
+
* index is what the session sweep and the erase delete a session's hand-offs
|
|
134
|
+
* by.
|
|
133
135
|
*/
|
|
134
136
|
export class RelyingPartyBearerHandoffDoc extends plugins.nosqldb.SmartDataDbDoc<RelyingPartyBearerHandoffDoc, RelyingPartyBearerHandoffDoc> {
|
|
135
137
|
@plugins.nosqldb.unI()
|
|
@@ -141,6 +143,7 @@ export class RelyingPartyBearerHandoffDoc extends plugins.nosqldb.SmartDataDbDoc
|
|
|
141
143
|
|
|
142
144
|
/** the session the login opened; the code is its only way out */
|
|
143
145
|
@plugins.nosqldb.svDb()
|
|
146
|
+
@plugins.nosqldb.index()
|
|
144
147
|
public sessionId: string = '';
|
|
145
148
|
|
|
146
149
|
@plugins.nosqldb.svDb()
|
|
@@ -163,8 +166,9 @@ export class RelyingPartyBearerHandoffDoc extends plugins.nosqldb.SmartDataDbDoc
|
|
|
163
166
|
* One generation of a bearer session's tokens: an access token and the
|
|
164
167
|
* refresh token that replaces both. Only hashes are stored. Exactly one
|
|
165
168
|
* generation of a session is current (`rotatedAt` `null`); a rotated one is
|
|
166
|
-
* kept until
|
|
167
|
-
* recognized as reuse.
|
|
169
|
+
* kept until its session is swept, so its refresh token coming back is
|
|
170
|
+
* recognized as reuse. The `sessionId` index is what the session sweep and
|
|
171
|
+
* the erase delete a session's generations by.
|
|
168
172
|
*/
|
|
169
173
|
export class RelyingPartyBearerTokenDoc extends plugins.nosqldb.SmartDataDbDoc<RelyingPartyBearerTokenDoc, RelyingPartyBearerTokenDoc> {
|
|
170
174
|
@plugins.nosqldb.unI()
|
|
@@ -177,6 +181,7 @@ export class RelyingPartyBearerTokenDoc extends plugins.nosqldb.SmartDataDbDoc<R
|
|
|
177
181
|
public refreshTokenHash: string = '';
|
|
178
182
|
|
|
179
183
|
@plugins.nosqldb.svDb()
|
|
184
|
+
@plugins.nosqldb.index()
|
|
180
185
|
public sessionId: string = '';
|
|
181
186
|
|
|
182
187
|
@plugins.nosqldb.svDb()
|
|
@@ -190,9 +195,27 @@ export class RelyingPartyBearerTokenDoc extends plugins.nosqldb.SmartDataDbDoc<R
|
|
|
190
195
|
@plugins.nosqldb.index()
|
|
191
196
|
public expiresAt: number = 0;
|
|
192
197
|
|
|
193
|
-
/** `null` while this generation is current; when its refresh token was spent otherwise */
|
|
198
|
+
/** `null` while this generation is current; when its refresh token was spent, or the generation revoked, otherwise */
|
|
194
199
|
@plugins.nosqldb.svDb()
|
|
195
200
|
public rotatedAt: number | null = null;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* When its access token or its refresh token was first presented; `null`
|
|
204
|
+
* until then (and on a generation stored before 19.6.0). A rotated
|
|
205
|
+
* generation whose successor was never presented may come back within the
|
|
206
|
+
* reuse grace window as a lost answer.
|
|
207
|
+
*/
|
|
208
|
+
@plugins.nosqldb.svDb()
|
|
209
|
+
public firstUsedAt: number | null = null;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The generation that replaced this one: set by its rotation and moved to
|
|
213
|
+
* the new one when a lost answer is issued again. `null` on a current
|
|
214
|
+
* generation, on one that was revoked unpresented, and on one stored before
|
|
215
|
+
* 19.6.0.
|
|
216
|
+
*/
|
|
217
|
+
@plugins.nosqldb.svDb()
|
|
218
|
+
public successorId: string | null = null;
|
|
196
219
|
}
|
|
197
220
|
|
|
198
221
|
export const RELYING_PARTY_DEFAULT_COLLECTION_NAMES = {
|
|
@@ -41,6 +41,52 @@ export interface IRelyingPartyBearerMessage {
|
|
|
41
41
|
*/
|
|
42
42
|
export type TRelyingPartyBearerDeliveryError = 'access_denied' | 'authentication_failed' | (string & {});
|
|
43
43
|
|
|
44
|
+
/** RFC 7636 §4.1: a code verifier is 43 to 128 characters of `A-Z a-z 0-9 . _ ~ -`. */
|
|
45
|
+
const PKCE_VERIFIER = /^[A-Za-z0-9._~-]{43,128}$/u;
|
|
46
|
+
|
|
47
|
+
/** Whether a value is a well-formed PKCE code verifier (RFC 7636 §4.1). */
|
|
48
|
+
export const isPkceVerifier = (valueArg: unknown): valueArg is string =>
|
|
49
|
+
typeof valueArg === 'string' && PKCE_VERIFIER.test(valueArg);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The query of `GET <prefix>bearer/login`, by its parameter names on the
|
|
53
|
+
* wire. The routes check every parameter as untrusted input.
|
|
54
|
+
*/
|
|
55
|
+
export interface IRelyingPartyBearerLoginQuery {
|
|
56
|
+
client_kind: TRelyingPartyBearerClientKind;
|
|
57
|
+
/** one of the configured return targets of the kind, exactly; a loopback target with the client's port */
|
|
58
|
+
return_target: string;
|
|
59
|
+
/** BASE64URL(SHA-256(code verifier)), 43 characters */
|
|
60
|
+
code_challenge: string;
|
|
61
|
+
/** only `S256` is accepted */
|
|
62
|
+
code_challenge_method: 'S256';
|
|
63
|
+
/** the client's own correlation value, 1 to 256 characters of `A-Z a-z 0-9 . _ ~ -`, handed back with the outcome */
|
|
64
|
+
state?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The JSON body of `POST <prefix>bearer/claim`. */
|
|
68
|
+
export interface IRelyingPartyBearerClaimRequest {
|
|
69
|
+
/** the hand-off code, claimed once */
|
|
70
|
+
code: string;
|
|
71
|
+
/** the verifier of the challenge the code is bound to (`isPkceVerifier`) */
|
|
72
|
+
codeVerifier: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The JSON body of `POST <prefix>bearer/refresh`. */
|
|
76
|
+
export interface IRelyingPartyBearerRefreshRequest {
|
|
77
|
+
/** the current refresh token; this call spends it */
|
|
78
|
+
refreshToken: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The JSON body of `POST <prefix>bearer/logout`. The session is named by
|
|
83
|
+
* `Authorization: Bearer <accessToken>`, or by `refreshToken` once that access
|
|
84
|
+
* token has expired.
|
|
85
|
+
*/
|
|
86
|
+
export interface IRelyingPartyBearerLogoutRequest {
|
|
87
|
+
refreshToken?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
44
90
|
/** The session a bearer answer names. */
|
|
45
91
|
export interface IRelyingPartyBearerSessionInfo {
|
|
46
92
|
sessionId: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
isPkceVerifier,
|
|
2
3
|
RELYING_PARTY_BEARER_MESSAGE_TYPE,
|
|
3
4
|
type IRelyingPartyBearerMessage,
|
|
4
5
|
type TRelyingPartyBearerClientKind,
|
|
@@ -7,7 +8,8 @@ import { sha256, timingSafeEqualString } from './helpers.crypto.js';
|
|
|
7
8
|
import { sanitizeReturnPath } from './helpers.returnpath.js';
|
|
8
9
|
import type { IRelyingPartyBearerDelivery, IRelyingPartyBearerReturnTargets } from './interfaces.js';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
// the verifier check lives in the import-free contract, which the browser client shares
|
|
12
|
+
export { isPkceVerifier, RELYING_PARTY_BEARER_MESSAGE_TYPE };
|
|
11
13
|
|
|
12
14
|
export const RELYING_PARTY_BEARER_CLIENT_KINDS: readonly TRelyingPartyBearerClientKind[] = Object.freeze([
|
|
13
15
|
'web',
|
|
@@ -20,8 +22,6 @@ export const RELYING_PARTY_BEARER_CLIENT_KINDS: readonly TRelyingPartyBearerClie
|
|
|
20
22
|
const MAX_RETURN_TARGET_LENGTH = 2_048;
|
|
21
23
|
/** BASE64URL of a SHA-256 digest, without padding (RFC 7636 §4.2). */
|
|
22
24
|
const PKCE_CHALLENGE = /^[A-Za-z0-9_-]{43}$/u;
|
|
23
|
-
/** RFC 7636 §4.1: 43 to 128 unreserved characters. */
|
|
24
|
-
const PKCE_VERIFIER = /^[A-Za-z0-9._~-]{43,128}$/u;
|
|
25
25
|
const CLIENT_STATE = /^[A-Za-z0-9._~-]{1,256}$/u;
|
|
26
26
|
const LOOPBACK_HOSTS: readonly string[] = ['127.0.0.1', '[::1]'];
|
|
27
27
|
/** A private-use scheme is a reverse domain name: it has a dot, and no web scheme has one (RFC 8252 §7.1). */
|
|
@@ -33,9 +33,6 @@ export const isBearerClientKind = (valueArg: unknown): valueArg is TRelyingParty
|
|
|
33
33
|
export const isPkceChallenge = (valueArg: unknown): valueArg is string =>
|
|
34
34
|
typeof valueArg === 'string' && PKCE_CHALLENGE.test(valueArg);
|
|
35
35
|
|
|
36
|
-
export const isPkceVerifier = (valueArg: unknown): valueArg is string =>
|
|
37
|
-
typeof valueArg === 'string' && PKCE_VERIFIER.test(valueArg);
|
|
38
|
-
|
|
39
36
|
export const isBearerClientState = (valueArg: unknown): valueArg is string =>
|
|
40
37
|
typeof valueArg === 'string' && CLIENT_STATE.test(valueArg);
|
|
41
38
|
|
|
@@ -3,6 +3,12 @@ export interface ICookieOptions {
|
|
|
3
3
|
maxAgeSeconds?: number;
|
|
4
4
|
sameSite?: 'Lax' | 'Strict' | 'None';
|
|
5
5
|
secure?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* `Partitioned` (CHIPS): the browser keeps the cookie per top-level site, so
|
|
8
|
+
* a page framed on another site gets a cookie jar of its own there. Needs
|
|
9
|
+
* `secure`; a cookie set partitioned is also cleared partitioned.
|
|
10
|
+
*/
|
|
11
|
+
partitioned?: boolean;
|
|
6
12
|
}
|
|
7
13
|
|
|
8
14
|
// Control characters, space, DEL and the cookie delimiters are never valid.
|
|
@@ -60,6 +66,13 @@ export const serializeCookie = (
|
|
|
60
66
|
if (optionsArg.sameSite) {
|
|
61
67
|
parts.push('SameSite=' + optionsArg.sameSite);
|
|
62
68
|
}
|
|
69
|
+
if (optionsArg.partitioned) {
|
|
70
|
+
if (!optionsArg.secure) {
|
|
71
|
+
// browsers drop a partitioned cookie that is not set Secure
|
|
72
|
+
throw new Error('A Partitioned cookie needs Secure');
|
|
73
|
+
}
|
|
74
|
+
parts.push('Partitioned');
|
|
75
|
+
}
|
|
63
76
|
if (optionsArg.maxAgeSeconds !== undefined) {
|
|
64
77
|
if (!Number.isSafeInteger(optionsArg.maxAgeSeconds) || optionsArg.maxAgeSeconds < 0) {
|
|
65
78
|
throw new Error('Invalid cookie max age');
|
|
@@ -2,9 +2,13 @@ import type { IOidcCompletion, TOidcCompletionFailureStage, TOidcPrompt, TOidcTo
|
|
|
2
2
|
import type { TRelyingPartyBearerClientKind } from './contract.bearer.js';
|
|
3
3
|
|
|
4
4
|
export type {
|
|
5
|
+
IRelyingPartyBearerClaimRequest,
|
|
5
6
|
IRelyingPartyBearerErrorAnswer,
|
|
7
|
+
IRelyingPartyBearerLoginQuery,
|
|
6
8
|
IRelyingPartyBearerLogoutAnswer,
|
|
9
|
+
IRelyingPartyBearerLogoutRequest,
|
|
7
10
|
IRelyingPartyBearerMessage,
|
|
11
|
+
IRelyingPartyBearerRefreshRequest,
|
|
8
12
|
IRelyingPartyBearerSessionAnswer,
|
|
9
13
|
IRelyingPartyBearerSessionInfo,
|
|
10
14
|
IRelyingPartyBearerTokenAnswer,
|
|
@@ -41,6 +45,27 @@ export interface IResolvedRelyingPartySession<TPrincipal extends object | undefi
|
|
|
41
45
|
csrfToken: string;
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
/**
|
|
49
|
+
* How the session cookie and the login's intent cookie are sent across
|
|
50
|
+
* sites; one policy for both, on every cookie they are set and cleared with.
|
|
51
|
+
*/
|
|
52
|
+
export interface IRelyingPartyCookiePolicy {
|
|
53
|
+
/**
|
|
54
|
+
* `Lax`, the default: the browser sends the cookies with this origin's own
|
|
55
|
+
* requests and with a top-level navigation to it, never into a frame on
|
|
56
|
+
* another site. `None`: also for a page of this origin framed on another
|
|
57
|
+
* site; it needs an https `publicOrigin`, because a `SameSite=None` cookie
|
|
58
|
+
* must be `Secure`.
|
|
59
|
+
*/
|
|
60
|
+
sameSite?: 'Lax' | 'None';
|
|
61
|
+
/**
|
|
62
|
+
* `Partitioned` (CHIPS): the browser keeps the cookies per top-level site,
|
|
63
|
+
* so the framed application has a jar of its own under each site that frames
|
|
64
|
+
* it. Only with `sameSite: 'None'`. `false` by default.
|
|
65
|
+
*/
|
|
66
|
+
partitioned?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
44
69
|
/** A session just opened: the answer, the CSRF token and the `Set-Cookie` value that carries it. */
|
|
45
70
|
export interface IRelyingPartySessionOpening {
|
|
46
71
|
session: IRelyingPartySession;
|
|
@@ -115,6 +140,15 @@ export interface IRelyingPartyBearerOptions {
|
|
|
115
140
|
returnTargets: IRelyingPartyBearerReturnTargets;
|
|
116
141
|
/** how long an access token lives, in seconds; 900 by default, never past its session */
|
|
117
142
|
accessTokenMaxAgeSeconds?: number;
|
|
143
|
+
/**
|
|
144
|
+
* How long after a rotation its spent refresh token may come back as a lost
|
|
145
|
+
* answer instead of reuse, in whole seconds: 60 by default, 0 turns it off,
|
|
146
|
+
* at most 300. Within it, a spent refresh token whose successor was never
|
|
147
|
+
* presented (neither its access token nor its refresh token) revokes that
|
|
148
|
+
* successor and answers a new pair; once the successor was presented, or
|
|
149
|
+
* after the window, it is reuse and ends the session.
|
|
150
|
+
*/
|
|
151
|
+
refreshReuseGraceSeconds?: number;
|
|
118
152
|
}
|
|
119
153
|
|
|
120
154
|
/** What a bearer client sends to start its login. Every field is checked as untrusted input. */
|
|
@@ -156,6 +190,37 @@ export interface IRelyingPartyBearerHandoff {
|
|
|
156
190
|
expiresAt: number;
|
|
157
191
|
}
|
|
158
192
|
|
|
193
|
+
/**
|
|
194
|
+
* A bearer session the application opened for a device it paired
|
|
195
|
+
* (`openBearerHandoff`): the session, which has no provider login of its own
|
|
196
|
+
* (`idpSubject` and `loginAttemptId` are absent), and the single-use code the
|
|
197
|
+
* device claims at `bearer/claim` with the verifier of its challenge. The
|
|
198
|
+
* code is no token; only its hash is stored.
|
|
199
|
+
*/
|
|
200
|
+
export interface IRelyingPartyBearerHandoffOpening {
|
|
201
|
+
session: IRelyingPartySession;
|
|
202
|
+
code: string;
|
|
203
|
+
/** epoch milliseconds, at most `BEARER_HANDOFF_MAX_AGE_SECONDS` after the opening and never past the session */
|
|
204
|
+
expiresAt: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* What `eraseSessionsOfPrincipal` removed of one principal: the sessions it
|
|
209
|
+
* ended first, and the rows it deleted.
|
|
210
|
+
*/
|
|
211
|
+
export interface IRelyingPartySessionsErasure {
|
|
212
|
+
/** sessions not revoked yet (an expired one too) that it ended, each with `onSessionEnded` and its provider tokens handed back */
|
|
213
|
+
endedSessions: number;
|
|
214
|
+
/** session rows, live or ended */
|
|
215
|
+
deletedSessions: number;
|
|
216
|
+
/** the sign-in transactions the provider sessions came out of */
|
|
217
|
+
deletedLoginAttempts: number;
|
|
218
|
+
/** bearer hand-offs, claimed or not */
|
|
219
|
+
deletedBearerHandoffs: number;
|
|
220
|
+
/** bearer token generations, current or rotated */
|
|
221
|
+
deletedBearerTokens: number;
|
|
222
|
+
}
|
|
223
|
+
|
|
159
224
|
/**
|
|
160
225
|
* The relying party's own tokens of a bearer session. The refresh token
|
|
161
226
|
* rotates on every use; a rotated one coming back ends the session.
|
|
@@ -275,6 +340,8 @@ export type TRelyingPartyEventName =
|
|
|
275
340
|
| 'session_sweep_failed'
|
|
276
341
|
/** a bearer hand-off code or refresh token came back after its use; `reason` names which, the session ended */
|
|
277
342
|
| 'bearer_credential_reused'
|
|
343
|
+
/** a spent refresh token came back within `refreshReuseGraceSeconds` before its successor was presented: the successor was revoked and a new pair issued */
|
|
344
|
+
| 'bearer_refresh_reissued'
|
|
278
345
|
/** an application hook threw; the step it belonged to ran on regardless */
|
|
279
346
|
| 'hook_failed'
|
|
280
347
|
/** a session whose opening failed could not be revoked; its provider tokens went back anyway */
|
|
@@ -298,20 +365,22 @@ export interface IRelyingPartyHooks {
|
|
|
298
365
|
* A provider login opened a session, linked to its attempt. Runs before the
|
|
299
366
|
* login answers; a throw fails the login: the session is revoked, the
|
|
300
367
|
* provider tokens are handed back and the original error is rethrown.
|
|
368
|
+
* `delivery` is where a bearer login's outcome goes (its client kind, return
|
|
369
|
+
* target and the client's `state`); a cookie login has none.
|
|
301
370
|
*/
|
|
302
|
-
onProviderSessionOpened?(eventArg: { session: IRelyingPartySession }): Promise<void>;
|
|
371
|
+
onProviderSessionOpened?(eventArg: { session: IRelyingPartySession; delivery?: IRelyingPartyBearerDelivery }): Promise<void>;
|
|
303
372
|
/**
|
|
304
373
|
* A session ended, or a caller tried to end one that was already over
|
|
305
374
|
* (`revokedHere: false`). Close its live connections when `revokedHere`,
|
|
306
375
|
* drop cached authority either way. Must not wait. A throw (or a rejected
|
|
307
376
|
* promise) is reported as a `hook_failed` event and never stops the end:
|
|
308
|
-
* the provider tokens go back regardless.
|
|
377
|
+
* the provider tokens go back regardless. An end that revoked the session
|
|
378
|
+
* here names its principal (`principalId`); one that found it over already
|
|
379
|
+
* does not.
|
|
309
380
|
*/
|
|
310
|
-
onSessionEnded?(eventArg:
|
|
311
|
-
sessionId: string;
|
|
312
|
-
reason: TRelyingPartySessionEndReason;
|
|
313
|
-
revokedHere: boolean;
|
|
314
|
-
}): void;
|
|
381
|
+
onSessionEnded?(eventArg:
|
|
382
|
+
| { sessionId: string; reason: TRelyingPartySessionEndReason; revokedHere: true; principalId: string }
|
|
383
|
+
| { sessionId: string; reason: TRelyingPartySessionEndReason; revokedHere: false }): void;
|
|
315
384
|
/**
|
|
316
385
|
* Structured diagnostics; the default writes one JSON line to
|
|
317
386
|
* `console.error`. A throwing sink never interrupts the step that reported:
|
|
@@ -231,16 +231,20 @@ export class RelyingPartyProviderDouble implements IOidcProvider {
|
|
|
231
231
|
|
|
232
232
|
public async completeAuthorization(optionsArg: IOidcCompletionOptions): Promise<IOidcCompletion> {
|
|
233
233
|
if (!this.#discovered) {
|
|
234
|
-
throw new OidcCompletionError('token_exchange', notDiscovered());
|
|
234
|
+
throw new OidcCompletionError('token_exchange', notDiscovered(), { code: 'provider_unreachable' });
|
|
235
235
|
}
|
|
236
236
|
const code = optionsArg.callbackUrl.searchParams.get('code') ?? '';
|
|
237
237
|
const issued = this.#codes.get(code);
|
|
238
238
|
this.#codes.delete(code);
|
|
239
239
|
if (!issued || issued.state !== optionsArg.state) {
|
|
240
|
-
|
|
240
|
+
// what idp.global's token endpoint answers for such a code
|
|
241
|
+
throw new OidcCompletionError('token_exchange', new Error('unknown, used or foreign code'), {
|
|
242
|
+
code: 'token_exchange_refused',
|
|
243
|
+
oauthError: 'invalid_grant',
|
|
244
|
+
});
|
|
241
245
|
}
|
|
242
246
|
if (issued.nonce !== optionsArg.nonce) {
|
|
243
|
-
throw new OidcCompletionError('token_validation', new Error('nonce mismatch'));
|
|
247
|
+
throw new OidcCompletionError('token_validation', new Error('nonce mismatch'), { code: 'login_mismatch' });
|
|
244
248
|
}
|
|
245
249
|
this.completions.push({ subject: issued.subject, state: issued.state });
|
|
246
250
|
if (this.completionFailure) {
|