@idp.global/sdk 18.0.0 → 18.1.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 +28 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts_relyingparty/classes.actorscope.d.ts +23 -0
- package/dist_ts_relyingparty/classes.actorscope.js +22 -0
- package/dist_ts_relyingparty/classes.authorityobserver.d.ts +81 -0
- package/dist_ts_relyingparty/classes.authorityobserver.js +185 -0
- package/dist_ts_relyingparty/classes.errors.d.ts +33 -0
- package/dist_ts_relyingparty/classes.errors.js +53 -0
- package/dist_ts_relyingparty/classes.idpoidcprovider.d.ts +142 -0
- package/dist_ts_relyingparty/classes.idpoidcprovider.js +255 -0
- package/dist_ts_relyingparty/classes.relyingpartyroutes.d.ts +84 -0
- package/dist_ts_relyingparty/classes.relyingpartyroutes.js +232 -0
- package/dist_ts_relyingparty/classes.relyingpartysessions.d.ts +210 -0
- package/dist_ts_relyingparty/classes.relyingpartysessions.js +786 -0
- package/dist_ts_relyingparty/classes.rpmodels.d.ts +79 -0
- package/dist_ts_relyingparty/classes.rpmodels.js +283 -0
- package/dist_ts_relyingparty/classes.secretbox.d.ts +14 -0
- package/dist_ts_relyingparty/classes.secretbox.js +45 -0
- package/dist_ts_relyingparty/classes.socketbinding.d.ts +75 -0
- package/dist_ts_relyingparty/classes.socketbinding.js +94 -0
- package/dist_ts_relyingparty/helpers.cookies.d.ts +11 -0
- package/dist_ts_relyingparty/helpers.cookies.js +62 -0
- package/dist_ts_relyingparty/helpers.crypto.d.ts +12 -0
- package/dist_ts_relyingparty/helpers.crypto.js +40 -0
- package/dist_ts_relyingparty/helpers.returnpath.d.ts +9 -0
- package/dist_ts_relyingparty/helpers.returnpath.js +39 -0
- package/dist_ts_relyingparty/index.d.ts +13 -0
- package/dist_ts_relyingparty/index.js +14 -0
- package/dist_ts_relyingparty/interfaces.d.ts +113 -0
- package/dist_ts_relyingparty/interfaces.js +2 -0
- package/dist_ts_relyingparty/plugins.d.ts +10 -0
- package/dist_ts_relyingparty/plugins.js +11 -0
- package/package.json +14 -11
- package/readme.md +158 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_relyingparty/classes.actorscope.ts +39 -0
- package/ts_relyingparty/classes.authorityobserver.ts +253 -0
- package/ts_relyingparty/classes.errors.ts +69 -0
- package/ts_relyingparty/classes.idpoidcprovider.ts +386 -0
- package/ts_relyingparty/classes.relyingpartyroutes.ts +286 -0
- package/ts_relyingparty/classes.relyingpartysessions.ts +970 -0
- package/ts_relyingparty/classes.rpmodels.ts +146 -0
- package/ts_relyingparty/classes.secretbox.ts +49 -0
- package/ts_relyingparty/classes.socketbinding.ts +142 -0
- package/ts_relyingparty/helpers.cookies.ts +79 -0
- package/ts_relyingparty/helpers.crypto.ts +49 -0
- package/ts_relyingparty/helpers.returnpath.ts +44 -0
- package/ts_relyingparty/index.ts +13 -0
- package/ts_relyingparty/interfaces.ts +133 -0
- package/ts_relyingparty/plugins.ts +17 -0
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
import { SecretBox } from './classes.secretbox.js';
|
|
2
|
+
import { AuthorityTokenError, LoginCallbackError, RelyingPartyError, RelyingPartyRefusal, } from './classes.errors.js';
|
|
3
|
+
import { OidcCompletionError, OidcRefreshRefusedError, } from './classes.idpoidcprovider.js';
|
|
4
|
+
import { parseCookies, serializeClearedCookie, serializeCookie } from './helpers.cookies.js';
|
|
5
|
+
import { deriveCsrfToken, randomToken, sha256, timingSafeEqualString } from './helpers.crypto.js';
|
|
6
|
+
import { sanitizeReturnPath } from './helpers.returnpath.js';
|
|
7
|
+
export const RELYING_PARTY_DEFAULT_SESSION_MAX_AGE_SECONDS = 12 * 60 * 60;
|
|
8
|
+
export const RELYING_PARTY_DEFAULT_LOGIN_PATH_PREFIX = '/auth/';
|
|
9
|
+
/** The label the CSRF HMAC starts with unless the application names its own. */
|
|
10
|
+
export const RELYING_PARTY_DEFAULT_CSRF_LABEL = 'idp.global/sdk relyingparty csrf' + String.fromCharCode(0);
|
|
11
|
+
const MAX_COOKIE_TOKEN_LENGTH = 256;
|
|
12
|
+
const SESSION_TOUCH_INTERVAL_MS = 60_000;
|
|
13
|
+
/** A sign-in transaction has to survive an interactive login at the provider, not a working day. */
|
|
14
|
+
export const LOGIN_ATTEMPT_MAX_AGE_SECONDS = 10 * 60;
|
|
15
|
+
/** Attempts are swept once they are this much older than their expiry. */
|
|
16
|
+
const LOGIN_ATTEMPT_SWEEP_GRACE_MS = 60 * 60 * 1_000;
|
|
17
|
+
/** How many of them one sign-in removes; the next sign-in takes the following page. */
|
|
18
|
+
const LOGIN_ATTEMPT_SWEEP_BATCH = 100;
|
|
19
|
+
/**
|
|
20
|
+
* How long ending a session waits for the provider to take its tokens back.
|
|
21
|
+
* The local session is already revoked when that call runs.
|
|
22
|
+
*/
|
|
23
|
+
export const TOKEN_REVOCATION_BUDGET_MS = 3_000;
|
|
24
|
+
/** How long before its expiry the provider access token is rotated. */
|
|
25
|
+
export const ACCESS_TOKEN_REFRESH_SKEW_MS = 60_000;
|
|
26
|
+
/**
|
|
27
|
+
* What the refresh lock adds on top of the provider request it has to cover:
|
|
28
|
+
* the claim and the store around it, and clock disagreement between replicas.
|
|
29
|
+
*/
|
|
30
|
+
export const REFRESH_LOCK_MARGIN_MS = 20_000;
|
|
31
|
+
const REVOKED_TOKEN_FIELDS = { sealedAccessToken: '', sealedRefreshToken: '', refreshLockUntil: 0 };
|
|
32
|
+
const errorMessage = (errorArg) => (errorArg instanceof Error ? errorArg.message : String(errorArg));
|
|
33
|
+
/** Browsers store a cookie with one of these prefixes only when it is set `Secure`; they match it case-insensitively. */
|
|
34
|
+
const SECURE_COOKIE_NAME_PREFIX = /^__(host|secure)-/i;
|
|
35
|
+
const cookieNameFor = (configuredArg, defaultArg, secureArg, labelArg) => {
|
|
36
|
+
if (configuredArg === undefined) {
|
|
37
|
+
return defaultArg;
|
|
38
|
+
}
|
|
39
|
+
if (!secureArg && SECURE_COOKIE_NAME_PREFIX.test(configuredArg)) {
|
|
40
|
+
throw new Error(`cookieNames.${labelArg} "${configuredArg}" needs an https publicOrigin: `
|
|
41
|
+
+ 'browsers drop a __Host- or __Secure- cookie that is not set Secure');
|
|
42
|
+
}
|
|
43
|
+
return configuredArg;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* The relying-party session layer: provider logins through the authorization
|
|
47
|
+
* code flow with PKCE, HttpOnly cookie sessions with a session-bound CSRF
|
|
48
|
+
* token, provider tokens sealed at rest and rotated under a cross-replica
|
|
49
|
+
* lock, and session ends that hand the tokens back.
|
|
50
|
+
*
|
|
51
|
+
* Nothing here runs on a timer: expired login attempts are swept when the next
|
|
52
|
+
* login starts, and the only timer — the revocation budget — is unreferenced
|
|
53
|
+
* and cleared on every path.
|
|
54
|
+
*/
|
|
55
|
+
export class RelyingPartySessions {
|
|
56
|
+
publicOrigin;
|
|
57
|
+
secureCookies;
|
|
58
|
+
sessionCookieName;
|
|
59
|
+
intentCookieName;
|
|
60
|
+
loginPathPrefix;
|
|
61
|
+
sessionMaxAgeSeconds;
|
|
62
|
+
#models;
|
|
63
|
+
#principals;
|
|
64
|
+
#provider;
|
|
65
|
+
#secretBox;
|
|
66
|
+
#csrfKey;
|
|
67
|
+
#csrfLabel;
|
|
68
|
+
#hooks;
|
|
69
|
+
#now;
|
|
70
|
+
/** one rotation per session in this process, however many requests need it at once */
|
|
71
|
+
#refreshesInFlight = new Map();
|
|
72
|
+
constructor(optionsArg) {
|
|
73
|
+
const origin = new URL(optionsArg.publicOrigin);
|
|
74
|
+
if (origin.origin !== optionsArg.publicOrigin) {
|
|
75
|
+
throw new Error('publicOrigin must be a bare origin such as https://app.example');
|
|
76
|
+
}
|
|
77
|
+
if (!Buffer.isBuffer(optionsArg.csrfKey) || optionsArg.csrfKey.length !== 32) {
|
|
78
|
+
throw new Error('csrfKey must be 32 bytes');
|
|
79
|
+
}
|
|
80
|
+
const maxAge = optionsArg.sessionMaxAgeSeconds ?? RELYING_PARTY_DEFAULT_SESSION_MAX_AGE_SECONDS;
|
|
81
|
+
if (!Number.isSafeInteger(maxAge) || maxAge < 1) {
|
|
82
|
+
throw new Error('sessionMaxAgeSeconds must be a positive integer');
|
|
83
|
+
}
|
|
84
|
+
this.publicOrigin = origin.origin;
|
|
85
|
+
this.secureCookies = origin.protocol === 'https:';
|
|
86
|
+
const prefix = this.secureCookies ? '__Host-' : '';
|
|
87
|
+
this.sessionCookieName = cookieNameFor(optionsArg.cookieNames?.session, prefix + 'idp_rp_session', this.secureCookies, 'session');
|
|
88
|
+
this.intentCookieName = cookieNameFor(optionsArg.cookieNames?.intent, prefix + 'idp_rp_intent', this.secureCookies, 'intent');
|
|
89
|
+
this.loginPathPrefix = optionsArg.loginPathPrefix ?? RELYING_PARTY_DEFAULT_LOGIN_PATH_PREFIX;
|
|
90
|
+
if (!this.loginPathPrefix.startsWith('/') || !this.loginPathPrefix.endsWith('/')) {
|
|
91
|
+
throw new Error('loginPathPrefix must start and end with a slash');
|
|
92
|
+
}
|
|
93
|
+
this.sessionMaxAgeSeconds = maxAge;
|
|
94
|
+
this.#models = optionsArg.models;
|
|
95
|
+
this.#principals = optionsArg.principals;
|
|
96
|
+
this.#provider = optionsArg.provider;
|
|
97
|
+
this.#secretBox = new SecretBox(optionsArg.sessionEncryptionKey);
|
|
98
|
+
this.#csrfKey = Buffer.from(optionsArg.csrfKey);
|
|
99
|
+
this.#csrfLabel = optionsArg.csrfLabel ?? RELYING_PARTY_DEFAULT_CSRF_LABEL;
|
|
100
|
+
this.#hooks = optionsArg.hooks ?? {};
|
|
101
|
+
this.#now = optionsArg.now ?? (() => Date.now());
|
|
102
|
+
}
|
|
103
|
+
/** Whether provider logins are offered at all. */
|
|
104
|
+
get providerLoginEnabled() {
|
|
105
|
+
return this.#provider !== undefined;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* How long one replica holds the right to rotate a session's tokens. The
|
|
109
|
+
* provider detects reuse of a superseded refresh token and kills the family,
|
|
110
|
+
* so the lock strictly outlives its longest possible holder: one provider
|
|
111
|
+
* request at its full deadline plus the margin.
|
|
112
|
+
*/
|
|
113
|
+
get refreshLockMs() {
|
|
114
|
+
return (this.#provider?.requestTimeoutSeconds ?? 0) * 1_000 + REFRESH_LOCK_MARGIN_MS;
|
|
115
|
+
}
|
|
116
|
+
// ==========================================================================
|
|
117
|
+
// sessions
|
|
118
|
+
// ==========================================================================
|
|
119
|
+
/**
|
|
120
|
+
* Opens a session the application authenticated itself (a local password
|
|
121
|
+
* login). It carries no provider tokens.
|
|
122
|
+
*/
|
|
123
|
+
async openSession(optionsArg) {
|
|
124
|
+
if (typeof optionsArg?.principalId !== 'string' || optionsArg.principalId.length === 0) {
|
|
125
|
+
throw new Error('openSession needs a principal id');
|
|
126
|
+
}
|
|
127
|
+
const token = randomToken(32);
|
|
128
|
+
const session = this.newSessionDoc(token, optionsArg.principalId);
|
|
129
|
+
await session.save();
|
|
130
|
+
return this.opening(session, token);
|
|
131
|
+
}
|
|
132
|
+
/** The live session behind the session cookie, or `null`. A read touches it at most once a minute. */
|
|
133
|
+
async resolveCookieSession(cookieHeaderArg) {
|
|
134
|
+
const token = parseCookies(cookieHeaderArg).get(this.sessionCookieName);
|
|
135
|
+
if (!token || token.length > MAX_COOKIE_TOKEN_LENGTH) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const session = await this.#models.Session.getInstance({ tokenHash: sha256(token) });
|
|
139
|
+
const live = await this.liveSession(session);
|
|
140
|
+
if (!live) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return { session: this.snapshot(live), csrfToken: this.csrfTokenOf(token) };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Re-checks a session by its id with the same strength as a cookie request:
|
|
147
|
+
* live, its principal active, and touched under the liveness it was read
|
|
148
|
+
* with. What a pinned socket connection runs per request.
|
|
149
|
+
*/
|
|
150
|
+
async revalidateSession(sessionIdArg) {
|
|
151
|
+
if (typeof sessionIdArg !== 'string' || sessionIdArg.length === 0) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const live = await this.liveSession(await this.#models.Session.getInstance({ id: sessionIdArg }));
|
|
155
|
+
return live ? this.snapshot(live) : null;
|
|
156
|
+
}
|
|
157
|
+
/** Whether a session is live right now, without touching it; what a sweep asks. */
|
|
158
|
+
async isSessionLive(sessionIdArg) {
|
|
159
|
+
if (typeof sessionIdArg !== 'string' || sessionIdArg.length === 0) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const session = await this.#models.Session.getInstance({ id: sessionIdArg });
|
|
163
|
+
if (!session || session.revokedAt !== undefined || session.expiresAt <= this.#now()) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return this.#principals.isActive(session.userId);
|
|
167
|
+
}
|
|
168
|
+
validateCsrf(resolvedArg, candidateArg) {
|
|
169
|
+
return typeof candidateArg === 'string' && timingSafeEqualString(resolvedArg.csrfToken, candidateArg);
|
|
170
|
+
}
|
|
171
|
+
/** Ends one session; see `endSession`. */
|
|
172
|
+
async logout(sessionIdArg) {
|
|
173
|
+
return this.endSession(sessionIdArg);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Revokes a session once, with one conditional update: a touch running
|
|
177
|
+
* beside it cannot write the revocation away, and a second call finds
|
|
178
|
+
* nothing. Its provider tokens go back within `TOKEN_REVOCATION_BUDGET_MS`,
|
|
179
|
+
* after the local revocation, which never depends on the provider. Answers
|
|
180
|
+
* whether this call ended it.
|
|
181
|
+
*/
|
|
182
|
+
async endSession(sessionIdArg, reasonArg = 'logout') {
|
|
183
|
+
const ended = await this.revokeSession(sessionIdArg, reasonArg);
|
|
184
|
+
if (ended) {
|
|
185
|
+
await this.revokeSealedTokens(ended);
|
|
186
|
+
}
|
|
187
|
+
return ended !== null;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Ends every live session of a principal but `keepSessionId` (a password
|
|
191
|
+
* change keeps the session that asked for it) and answers how many ended.
|
|
192
|
+
*/
|
|
193
|
+
async endSessionsOfPrincipal(principalIdArg, optionsArg = {}) {
|
|
194
|
+
let ended = 0;
|
|
195
|
+
const sessions = await this.#models.Session.getInstances({
|
|
196
|
+
userId: principalIdArg,
|
|
197
|
+
revokedAt: { $exists: false },
|
|
198
|
+
});
|
|
199
|
+
for (const session of sessions) {
|
|
200
|
+
if (session.id === optionsArg.keepSessionId) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (await this.endSession(session.id)) {
|
|
204
|
+
ended += 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return ended;
|
|
208
|
+
}
|
|
209
|
+
clearSessionCookie() {
|
|
210
|
+
return serializeClearedCookie(this.sessionCookieName, {
|
|
211
|
+
httpOnly: true,
|
|
212
|
+
secure: this.secureCookies,
|
|
213
|
+
sameSite: 'Lax',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
clearIntentCookie() {
|
|
217
|
+
return serializeClearedCookie(this.intentCookieName, {
|
|
218
|
+
httpOnly: true,
|
|
219
|
+
secure: this.secureCookies,
|
|
220
|
+
sameSite: 'Lax',
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
// ==========================================================================
|
|
224
|
+
// provider login
|
|
225
|
+
// ==========================================================================
|
|
226
|
+
/**
|
|
227
|
+
* Starts a sign-in transaction: one attempt document with the PKCE verifier
|
|
228
|
+
* sealed next to it, and an intent cookie that binds the callback to this
|
|
229
|
+
* browser.
|
|
230
|
+
*/
|
|
231
|
+
async startLogin(returnPathArg) {
|
|
232
|
+
const provider = this.requireProvider();
|
|
233
|
+
await this.sweepExpiredAttempts();
|
|
234
|
+
const state = randomToken(32);
|
|
235
|
+
const nonce = randomToken(32);
|
|
236
|
+
const pkceVerifier = randomToken(32);
|
|
237
|
+
const bindingToken = randomToken(32);
|
|
238
|
+
const now = this.#now();
|
|
239
|
+
const attempt = new this.#models.LoginAttempt();
|
|
240
|
+
attempt.id = 'lga_' + randomToken(18);
|
|
241
|
+
attempt.stateHash = sha256(state);
|
|
242
|
+
attempt.bindingTokenHash = sha256(bindingToken);
|
|
243
|
+
attempt.sealedPkceVerifier = this.#secretBox.seal(pkceVerifier);
|
|
244
|
+
attempt.nonce = nonce;
|
|
245
|
+
attempt.returnPath = sanitizeReturnPath(returnPathArg, this.publicOrigin, this.loginPathPrefix);
|
|
246
|
+
attempt.createdAt = now;
|
|
247
|
+
attempt.expiresAt = now + LOGIN_ATTEMPT_MAX_AGE_SECONDS * 1_000;
|
|
248
|
+
await attempt.save();
|
|
249
|
+
return {
|
|
250
|
+
authorizationUrl: await provider.buildAuthorizationUrl({ state, nonce, pkceVerifier }),
|
|
251
|
+
intentCookie: serializeCookie(this.intentCookieName, bindingToken, {
|
|
252
|
+
httpOnly: true,
|
|
253
|
+
secure: this.secureCookies,
|
|
254
|
+
sameSite: 'Lax',
|
|
255
|
+
maxAgeSeconds: LOGIN_ATTEMPT_MAX_AGE_SECONDS,
|
|
256
|
+
}),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Consumes the attempt exactly once, exchanges the code, admits the
|
|
261
|
+
* principal and opens a session. A replayed callback, a foreign intent
|
|
262
|
+
* cookie and an expired attempt all find nothing to consume. Nothing of a
|
|
263
|
+
* failed login survives: its tokens are handed back before the failure
|
|
264
|
+
* leaves.
|
|
265
|
+
*/
|
|
266
|
+
async completeLogin(callbackUrlArg, cookieHeaderArg) {
|
|
267
|
+
const provider = this.requireProvider();
|
|
268
|
+
const states = callbackUrlArg.searchParams.getAll('state');
|
|
269
|
+
if (states.length !== 1 || !states[0]) {
|
|
270
|
+
throw new RelyingPartyError('invalid_login_callback');
|
|
271
|
+
}
|
|
272
|
+
const bindingToken = parseCookies(cookieHeaderArg).get(this.intentCookieName);
|
|
273
|
+
if (!bindingToken || bindingToken.length > MAX_COOKIE_TOKEN_LENGTH) {
|
|
274
|
+
throw new RelyingPartyError('missing_login_intent');
|
|
275
|
+
}
|
|
276
|
+
const now = this.#now();
|
|
277
|
+
const claimed = await this.#models.LoginAttempt.atomicFindOneAndUpdate({
|
|
278
|
+
stateHash: sha256(states[0]),
|
|
279
|
+
bindingTokenHash: sha256(bindingToken),
|
|
280
|
+
consumedAt: null,
|
|
281
|
+
expiresAt: { $gt: now },
|
|
282
|
+
}, { $set: { consumedAt: now } }, { returnDocument: 'after' });
|
|
283
|
+
if (claimed.status !== 'matched' || !claimed.document) {
|
|
284
|
+
throw new RelyingPartyError('invalid_or_replayed_login');
|
|
285
|
+
}
|
|
286
|
+
const attempt = claimed.document;
|
|
287
|
+
let completion;
|
|
288
|
+
let stage = 'token_exchange';
|
|
289
|
+
try {
|
|
290
|
+
completion = await provider.completeAuthorization({
|
|
291
|
+
callbackUrl: callbackUrlArg,
|
|
292
|
+
state: states[0],
|
|
293
|
+
nonce: attempt.nonce,
|
|
294
|
+
pkceVerifier: this.#secretBox.open(attempt.sealedPkceVerifier),
|
|
295
|
+
});
|
|
296
|
+
stage = 'principal_admission';
|
|
297
|
+
const { principalId } = await this.#principals.admit(completion);
|
|
298
|
+
stage = 'session_persistence';
|
|
299
|
+
return await this.createProviderSession(attempt, principalId, completion);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (completion) {
|
|
303
|
+
await this.revokeCompletionTokens(completion);
|
|
304
|
+
}
|
|
305
|
+
if (error instanceof RelyingPartyError || error instanceof RelyingPartyRefusal) {
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
if (error instanceof OidcCompletionError) {
|
|
309
|
+
throw new LoginCallbackError(error.stage, error);
|
|
310
|
+
}
|
|
311
|
+
throw new LoginCallbackError(stage, error);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The provider sent the browser back without a code. The attempt is
|
|
316
|
+
* consumed so its state can never complete afterwards; a callback that
|
|
317
|
+
* matches no open attempt leaves nothing to do.
|
|
318
|
+
*/
|
|
319
|
+
async abandonLogin(callbackUrlArg, cookieHeaderArg) {
|
|
320
|
+
const states = callbackUrlArg.searchParams.getAll('state');
|
|
321
|
+
const bindingToken = parseCookies(cookieHeaderArg).get(this.intentCookieName);
|
|
322
|
+
if (states.length !== 1
|
|
323
|
+
|| !states[0]
|
|
324
|
+
|| !bindingToken
|
|
325
|
+
|| bindingToken.length > MAX_COOKIE_TOKEN_LENGTH) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
await this.#models.LoginAttempt.atomicUpdate({ stateHash: sha256(states[0]), bindingTokenHash: sha256(bindingToken), consumedAt: null }, { $set: { consumedAt: this.#now() } });
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* The last step of a login: the browser arrives with both cookies, and the
|
|
332
|
+
* return path is released only when the session cookie belongs to the
|
|
333
|
+
* session this very attempt created.
|
|
334
|
+
*/
|
|
335
|
+
async finalizeLogin(cookieHeaderArg) {
|
|
336
|
+
this.requireProvider();
|
|
337
|
+
const bindingToken = parseCookies(cookieHeaderArg).get(this.intentCookieName);
|
|
338
|
+
if (!bindingToken || bindingToken.length > MAX_COOKIE_TOKEN_LENGTH) {
|
|
339
|
+
throw new RelyingPartyError('invalid_login_finalize');
|
|
340
|
+
}
|
|
341
|
+
const attempt = await this.#models.LoginAttempt.getInstance({ bindingTokenHash: sha256(bindingToken) });
|
|
342
|
+
if (!attempt
|
|
343
|
+
|| attempt.consumedAt === undefined
|
|
344
|
+
|| attempt.consumedAt === null
|
|
345
|
+
|| !attempt.createdSessionId
|
|
346
|
+
|| attempt.expiresAt <= this.#now()) {
|
|
347
|
+
throw new RelyingPartyError('invalid_login_finalize');
|
|
348
|
+
}
|
|
349
|
+
const resolved = await this.resolveCookieSession(cookieHeaderArg);
|
|
350
|
+
if (!resolved
|
|
351
|
+
|| resolved.session.id !== attempt.createdSessionId
|
|
352
|
+
|| resolved.session.loginAttemptId !== attempt.id) {
|
|
353
|
+
throw new RelyingPartyError('invalid_login_finalize');
|
|
354
|
+
}
|
|
355
|
+
return sanitizeReturnPath(attempt.returnPath, this.publicOrigin, this.loginPathPrefix);
|
|
356
|
+
}
|
|
357
|
+
// ==========================================================================
|
|
358
|
+
// provider tokens
|
|
359
|
+
// ==========================================================================
|
|
360
|
+
/**
|
|
361
|
+
* An access token of this login session that is usable right now. A token
|
|
362
|
+
* within `ACCESS_TOKEN_REFRESH_SKEW_MS` of its expiry — or one whose expiry
|
|
363
|
+
* is unknown — is rotated first. The session has to belong to
|
|
364
|
+
* `expectedPrincipalIdArg`, so a credential is never evaluated with another
|
|
365
|
+
* person's tokens.
|
|
366
|
+
*/
|
|
367
|
+
async liveProviderToken(sessionIdArg, expectedPrincipalIdArg) {
|
|
368
|
+
const session = await this.#models.Session.getInstance({ id: sessionIdArg });
|
|
369
|
+
const now = this.#now();
|
|
370
|
+
if (!session
|
|
371
|
+
|| session.revokedAt !== undefined
|
|
372
|
+
|| session.expiresAt <= now
|
|
373
|
+
|| !session.idpSubject
|
|
374
|
+
|| session.userId !== expectedPrincipalIdArg) {
|
|
375
|
+
throw new AuthorityTokenError('authority_login_required');
|
|
376
|
+
}
|
|
377
|
+
const subject = session.idpSubject;
|
|
378
|
+
const expiresAt = typeof session.accessTokenExpiresAt === 'number' ? session.accessTokenExpiresAt : 0;
|
|
379
|
+
const accessToken = this.openSealed(session.sealedAccessToken);
|
|
380
|
+
// an unknown expiry is rotated too: afterwards the lifetime is known
|
|
381
|
+
const due = accessToken === undefined || expiresAt === 0 || expiresAt <= now + ACCESS_TOKEN_REFRESH_SKEW_MS;
|
|
382
|
+
if (!due && accessToken !== undefined) {
|
|
383
|
+
return { accessToken, subject, expiresAt };
|
|
384
|
+
}
|
|
385
|
+
if (!session.sealedRefreshToken) {
|
|
386
|
+
if (accessToken !== undefined && (expiresAt === 0 || expiresAt > now)) {
|
|
387
|
+
// nothing to rotate with, and the token is not known to be dead
|
|
388
|
+
return { accessToken, subject, expiresAt };
|
|
389
|
+
}
|
|
390
|
+
await this.endProviderSession(sessionIdArg, 'access_token_unusable');
|
|
391
|
+
throw new AuthorityTokenError('authority_login_required');
|
|
392
|
+
}
|
|
393
|
+
return this.refreshProviderToken(sessionIdArg, subject);
|
|
394
|
+
}
|
|
395
|
+
async refreshProviderToken(sessionIdArg, subjectArg) {
|
|
396
|
+
const running = this.#refreshesInFlight.get(sessionIdArg);
|
|
397
|
+
if (running) {
|
|
398
|
+
return running;
|
|
399
|
+
}
|
|
400
|
+
const pending = this.runProviderRefresh(sessionIdArg, subjectArg).finally(() => {
|
|
401
|
+
this.#refreshesInFlight.delete(sessionIdArg);
|
|
402
|
+
});
|
|
403
|
+
this.#refreshesInFlight.set(sessionIdArg, pending);
|
|
404
|
+
return pending;
|
|
405
|
+
}
|
|
406
|
+
async runProviderRefresh(sessionIdArg, subjectArg) {
|
|
407
|
+
const provider = this.#provider;
|
|
408
|
+
if (!provider) {
|
|
409
|
+
throw new AuthorityTokenError('authority_unavailable');
|
|
410
|
+
}
|
|
411
|
+
const now = this.#now();
|
|
412
|
+
const lockUntil = now + this.refreshLockMs;
|
|
413
|
+
const claimed = await this.#models.Session.atomicFindOneAndUpdate({
|
|
414
|
+
id: sessionIdArg,
|
|
415
|
+
revokedAt: { $exists: false },
|
|
416
|
+
expiresAt: { $gt: now },
|
|
417
|
+
// a lock another replica holds is left alone until it expires
|
|
418
|
+
$or: [{ refreshLockUntil: { $lt: now } }, { refreshLockUntil: { $exists: false } }],
|
|
419
|
+
}, { $set: { refreshLockUntil: lockUntil } }, { returnDocument: 'after' });
|
|
420
|
+
if (claimed.status !== 'matched' || !claimed.document) {
|
|
421
|
+
return this.tokenAfterForeignRefresh(sessionIdArg, subjectArg);
|
|
422
|
+
}
|
|
423
|
+
const session = claimed.document;
|
|
424
|
+
// the lock holder decides on the state it just read: a rotation that
|
|
425
|
+
// landed in between is used as it stands, because spending a superseded
|
|
426
|
+
// refresh token would be reuse and would kill the family
|
|
427
|
+
const claimedExpiry = typeof session.accessTokenExpiresAt === 'number' ? session.accessTokenExpiresAt : 0;
|
|
428
|
+
const claimedToken = this.openSealed(session.sealedAccessToken);
|
|
429
|
+
if (claimedToken !== undefined && claimedExpiry > this.#now() + ACCESS_TOKEN_REFRESH_SKEW_MS) {
|
|
430
|
+
await this.#models.Session.atomicUpdate({ id: sessionIdArg, revokedAt: { $exists: false }, refreshLockUntil: lockUntil }, { $set: { refreshLockUntil: 0 } });
|
|
431
|
+
return { accessToken: claimedToken, subject: subjectArg, expiresAt: claimedExpiry };
|
|
432
|
+
}
|
|
433
|
+
const refreshToken = this.openSealed(session.sealedRefreshToken);
|
|
434
|
+
if (refreshToken === undefined) {
|
|
435
|
+
await this.endProviderSession(sessionIdArg, 'sealed_refresh_token_unreadable');
|
|
436
|
+
throw new AuthorityTokenError('authority_login_required');
|
|
437
|
+
}
|
|
438
|
+
let rotated;
|
|
439
|
+
try {
|
|
440
|
+
rotated = await provider.refreshTokens(refreshToken);
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
if (error instanceof OidcRefreshRefusedError) {
|
|
444
|
+
await this.endProviderSession(sessionIdArg, 'refresh_token_refused');
|
|
445
|
+
throw new AuthorityTokenError('authority_login_required', error);
|
|
446
|
+
}
|
|
447
|
+
this.emit({ event: 'idp_token_refresh_failed', sessionId: sessionIdArg, message: errorMessage(error) });
|
|
448
|
+
throw new AuthorityTokenError('authority_unavailable', error);
|
|
449
|
+
}
|
|
450
|
+
// the rotated pair replaces the old one under this lock, and only while
|
|
451
|
+
// the session is live: a logout in between leaves the update unmatched
|
|
452
|
+
let matched;
|
|
453
|
+
try {
|
|
454
|
+
const written = await this.#models.Session.atomicUpdate({ id: sessionIdArg, revokedAt: { $exists: false }, refreshLockUntil: lockUntil }, {
|
|
455
|
+
$set: {
|
|
456
|
+
sealedAccessToken: this.#secretBox.seal(rotated.accessToken),
|
|
457
|
+
...(rotated.refreshToken ? { sealedRefreshToken: this.#secretBox.seal(rotated.refreshToken) } : {}),
|
|
458
|
+
accessTokenExpiresAt: rotated.accessTokenExpiresAt,
|
|
459
|
+
refreshLockUntil: 0,
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
matched = written.matchedCount;
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
// the provider has superseded the old pair and nothing here can name the
|
|
466
|
+
// new one: it goes back at once, the lock expires on its own, and the
|
|
467
|
+
// request fails closed
|
|
468
|
+
await this.revokeRotatedTokens(rotated);
|
|
469
|
+
this.emit({ event: 'idp_token_store_failed', sessionId: sessionIdArg, message: errorMessage(error) });
|
|
470
|
+
throw new AuthorityTokenError('authority_unavailable', error);
|
|
471
|
+
}
|
|
472
|
+
if (matched === 0) {
|
|
473
|
+
await this.revokeRotatedTokens(rotated);
|
|
474
|
+
this.notifySessionEnded({ sessionId: sessionIdArg, reason: 'revoked_elsewhere', revokedHere: false });
|
|
475
|
+
throw new AuthorityTokenError('authority_login_required');
|
|
476
|
+
}
|
|
477
|
+
return { accessToken: rotated.accessToken, subject: subjectArg, expiresAt: rotated.accessTokenExpiresAt };
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Another replica is rotating this session right now. Whatever it has
|
|
481
|
+
* written by the time this request looks again is used; otherwise the
|
|
482
|
+
* request fails closed and the next one finds the rotated pair.
|
|
483
|
+
*/
|
|
484
|
+
async tokenAfterForeignRefresh(sessionIdArg, subjectArg) {
|
|
485
|
+
const session = await this.#models.Session.getInstance({ id: sessionIdArg });
|
|
486
|
+
const now = this.#now();
|
|
487
|
+
const expiresAt = typeof session?.accessTokenExpiresAt === 'number' ? session.accessTokenExpiresAt : 0;
|
|
488
|
+
const accessToken = session ? this.openSealed(session.sealedAccessToken) : undefined;
|
|
489
|
+
if (session
|
|
490
|
+
&& session.revokedAt === undefined
|
|
491
|
+
&& session.expiresAt > now
|
|
492
|
+
&& accessToken !== undefined
|
|
493
|
+
&& expiresAt > now + ACCESS_TOKEN_REFRESH_SKEW_MS) {
|
|
494
|
+
return { accessToken, subject: subjectArg, expiresAt };
|
|
495
|
+
}
|
|
496
|
+
throw new AuthorityTokenError('authority_unavailable');
|
|
497
|
+
}
|
|
498
|
+
// ==========================================================================
|
|
499
|
+
// internals
|
|
500
|
+
// ==========================================================================
|
|
501
|
+
requireProvider() {
|
|
502
|
+
if (!this.#provider) {
|
|
503
|
+
throw new RelyingPartyError('login_method_unavailable');
|
|
504
|
+
}
|
|
505
|
+
return this.#provider;
|
|
506
|
+
}
|
|
507
|
+
newSessionDoc(tokenArg, principalIdArg) {
|
|
508
|
+
const now = this.#now();
|
|
509
|
+
const session = new this.#models.Session();
|
|
510
|
+
session.id = 'ses_' + randomToken(18);
|
|
511
|
+
session.tokenHash = sha256(tokenArg);
|
|
512
|
+
session.userId = principalIdArg;
|
|
513
|
+
session.createdAt = now;
|
|
514
|
+
session.lastSeenAt = now;
|
|
515
|
+
session.expiresAt = now + this.sessionMaxAgeSeconds * 1_000;
|
|
516
|
+
return session;
|
|
517
|
+
}
|
|
518
|
+
csrfTokenOf(tokenArg) {
|
|
519
|
+
return deriveCsrfToken(tokenArg, this.#csrfKey, this.#csrfLabel);
|
|
520
|
+
}
|
|
521
|
+
opening(sessionArg, tokenArg) {
|
|
522
|
+
return {
|
|
523
|
+
session: this.snapshot(sessionArg),
|
|
524
|
+
csrfToken: this.csrfTokenOf(tokenArg),
|
|
525
|
+
sessionCookie: serializeCookie(this.sessionCookieName, tokenArg, {
|
|
526
|
+
httpOnly: true,
|
|
527
|
+
secure: this.secureCookies,
|
|
528
|
+
sameSite: 'Lax',
|
|
529
|
+
maxAgeSeconds: this.sessionMaxAgeSeconds,
|
|
530
|
+
}),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
snapshot(sessionArg) {
|
|
534
|
+
return {
|
|
535
|
+
id: sessionArg.id,
|
|
536
|
+
principalId: sessionArg.userId,
|
|
537
|
+
createdAt: sessionArg.createdAt,
|
|
538
|
+
lastSeenAt: sessionArg.lastSeenAt,
|
|
539
|
+
expiresAt: sessionArg.expiresAt,
|
|
540
|
+
...(sessionArg.idpSubject ? { idpSubject: sessionArg.idpSubject } : {}),
|
|
541
|
+
...(sessionArg.loginAttemptId ? { loginAttemptId: sessionArg.loginAttemptId } : {}),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
/** A session document that is live right now with an active principal, touched at most once a minute. */
|
|
545
|
+
async liveSession(sessionArg) {
|
|
546
|
+
const now = this.#now();
|
|
547
|
+
if (!sessionArg || sessionArg.revokedAt !== undefined || sessionArg.expiresAt <= now) {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
if (!(await this.#principals.isActive(sessionArg.userId))) {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
if (now - sessionArg.lastSeenAt > SESSION_TOUCH_INTERVAL_MS) {
|
|
554
|
+
// the touch carries the liveness it was read under into the write: a
|
|
555
|
+
// revocation that landed in between leaves it unmatched
|
|
556
|
+
const touched = await this.#models.Session.atomicUpdate({ id: sessionArg.id, revokedAt: { $exists: false }, expiresAt: { $gt: now } }, { $set: { lastSeenAt: now } });
|
|
557
|
+
if (touched.matchedCount === 0) {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
sessionArg.lastSeenAt = now;
|
|
561
|
+
}
|
|
562
|
+
return sessionArg;
|
|
563
|
+
}
|
|
564
|
+
async createProviderSession(attemptArg, principalIdArg, completionArg) {
|
|
565
|
+
const token = randomToken(32);
|
|
566
|
+
const session = this.newSessionDoc(token, principalIdArg);
|
|
567
|
+
session.idpSubject = completionArg.subject;
|
|
568
|
+
session.loginAttemptId = attemptArg.id;
|
|
569
|
+
session.sealedAccessToken = this.#secretBox.seal(completionArg.accessToken);
|
|
570
|
+
session.sealedRefreshToken = completionArg.refreshToken ? this.#secretBox.seal(completionArg.refreshToken) : '';
|
|
571
|
+
session.accessTokenExpiresAt = completionArg.accessTokenExpiresAt;
|
|
572
|
+
session.refreshLockUntil = 0;
|
|
573
|
+
await session.save();
|
|
574
|
+
const linked = await this.#models.LoginAttempt.atomicUpdate({ id: attemptArg.id, createdSessionId: null }, { $set: { createdSessionId: session.id } });
|
|
575
|
+
if (linked.matchedCount !== 1) {
|
|
576
|
+
// the attempt was consumed by another callback in the meantime
|
|
577
|
+
await this.discardUnopenedSession(session.id);
|
|
578
|
+
throw new RelyingPartyError('invalid_or_replayed_login');
|
|
579
|
+
}
|
|
580
|
+
const opening = this.opening(session, token);
|
|
581
|
+
try {
|
|
582
|
+
await this.#hooks.onProviderSessionOpened?.({ session: opening.session });
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
// the cookie never leaves, so the session is unreachable: it is revoked
|
|
586
|
+
// and its sealed tokens erased before the application's error goes on;
|
|
587
|
+
// the caller hands the provider tokens back
|
|
588
|
+
try {
|
|
589
|
+
await this.discardUnopenedSession(session.id);
|
|
590
|
+
}
|
|
591
|
+
catch (discardError) {
|
|
592
|
+
this.emit({ event: 'session_discard_failed', sessionId: session.id, message: errorMessage(discardError) });
|
|
593
|
+
}
|
|
594
|
+
throw error;
|
|
595
|
+
}
|
|
596
|
+
return opening;
|
|
597
|
+
}
|
|
598
|
+
/** Revokes a session that was never handed out and erases its sealed tokens. */
|
|
599
|
+
async discardUnopenedSession(sessionIdArg) {
|
|
600
|
+
await this.#models.Session.atomicUpdate({ id: sessionIdArg, revokedAt: { $exists: false } }, { $set: { revokedAt: this.#now(), ...REVOKED_TOKEN_FIELDS } });
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Tells the application a session ended. Its hook never stands between the
|
|
604
|
+
* local revocation and the provider revocation: a throw or a rejection is
|
|
605
|
+
* reported as `hook_failed` and the end runs on.
|
|
606
|
+
*/
|
|
607
|
+
notifySessionEnded(eventArg) {
|
|
608
|
+
const hook = this.#hooks.onSessionEnded;
|
|
609
|
+
if (!hook) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const reportFailure = (errorArg) => {
|
|
613
|
+
this.emit({ event: 'hook_failed', hook: 'onSessionEnded', sessionId: eventArg.sessionId, message: errorMessage(errorArg) });
|
|
614
|
+
};
|
|
615
|
+
try {
|
|
616
|
+
const result = hook.call(this.#hooks, eventArg);
|
|
617
|
+
if (result instanceof Promise) {
|
|
618
|
+
result.catch(reportFailure);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
reportFailure(error);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* The conditional revocation every end of a session runs. Answers the
|
|
627
|
+
* sealed tokens it took off the session when this call revoked it, `null`
|
|
628
|
+
* when the session was already over.
|
|
629
|
+
*/
|
|
630
|
+
async revokeSession(sessionIdArg, reasonArg) {
|
|
631
|
+
if (typeof sessionIdArg !== 'string' || sessionIdArg.length === 0) {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
const revoked = await this.#models.Session.atomicFindOneAndUpdate({ id: sessionIdArg, revokedAt: { $exists: false } }, { $set: { revokedAt: this.#now(), ...REVOKED_TOKEN_FIELDS } }, { returnDocument: 'before' });
|
|
635
|
+
const revokedHere = revoked.status === 'matched' && revoked.document !== null;
|
|
636
|
+
this.notifySessionEnded({ sessionId: sessionIdArg, reason: reasonArg, revokedHere });
|
|
637
|
+
if (!revokedHere) {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
const before = revoked.document;
|
|
641
|
+
return { sealedAccessToken: before.sealedAccessToken, sealedRefreshToken: before.sealedRefreshToken };
|
|
642
|
+
}
|
|
643
|
+
/** The provider says the login is over: the session ends here without asking it anything more. */
|
|
644
|
+
async endProviderSession(sessionIdArg, reasonArg) {
|
|
645
|
+
if (await this.revokeSession(sessionIdArg, reasonArg)) {
|
|
646
|
+
this.emit({ event: 'idp_session_ended', sessionId: sessionIdArg, reason: reasonArg });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Hands the tokens of an ended session back, best-effort and within the
|
|
651
|
+
* budget. A sealed value this deployment can no longer open is skipped.
|
|
652
|
+
*/
|
|
653
|
+
async revokeSealedTokens(sealedArg) {
|
|
654
|
+
if (!this.#provider || (!sealedArg.sealedAccessToken && !sealedArg.sealedRefreshToken)) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
await this.withinRevocationBudget(async () => {
|
|
658
|
+
for (const [sealed, hint] of [
|
|
659
|
+
[sealedArg.sealedRefreshToken, 'refresh_token'],
|
|
660
|
+
[sealedArg.sealedAccessToken, 'access_token'],
|
|
661
|
+
]) {
|
|
662
|
+
if (!sealed) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const token = this.openSealed(sealed);
|
|
666
|
+
if (token === undefined) {
|
|
667
|
+
this.emit({ event: 'oidc_sealed_token_unreadable', hint, message: 'The sealed provider token could not be opened' });
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
await this.revokeToken(token, hint);
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
async revokeRotatedTokens(rotatedArg) {
|
|
675
|
+
await this.withinRevocationBudget(async () => {
|
|
676
|
+
if (rotatedArg.refreshToken) {
|
|
677
|
+
await this.revokeToken(rotatedArg.refreshToken, 'refresh_token');
|
|
678
|
+
}
|
|
679
|
+
await this.revokeToken(rotatedArg.accessToken, 'access_token');
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
async revokeCompletionTokens(completionArg) {
|
|
683
|
+
await this.withinRevocationBudget(async () => {
|
|
684
|
+
if (completionArg.refreshToken) {
|
|
685
|
+
await this.revokeToken(completionArg.refreshToken, 'refresh_token');
|
|
686
|
+
}
|
|
687
|
+
await this.revokeToken(completionArg.accessToken, 'access_token');
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
async revokeToken(tokenArg, hintArg) {
|
|
691
|
+
try {
|
|
692
|
+
await this.#provider?.revokeToken(tokenArg, hintArg);
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
// the local session is gone either way; the provider expires the token
|
|
696
|
+
this.emit({ event: 'oidc_token_revocation_failed', hint: hintArg, message: errorMessage(error) });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Runs a best-effort provider step under `TOKEN_REVOCATION_BUDGET_MS`. What
|
|
701
|
+
* is still in flight at the deadline keeps running and ends on the provider
|
|
702
|
+
* client's own request timeout; only the waiting stops. The timer is cleared
|
|
703
|
+
* on every path and unreferenced.
|
|
704
|
+
*/
|
|
705
|
+
async withinRevocationBudget(workArg) {
|
|
706
|
+
let timer;
|
|
707
|
+
const work = workArg().then(() => 'done', (errorArg) => {
|
|
708
|
+
this.emit({ event: 'oidc_token_revocation_failed', message: errorMessage(errorArg) });
|
|
709
|
+
return 'done';
|
|
710
|
+
});
|
|
711
|
+
const deadline = new Promise((resolveArg) => {
|
|
712
|
+
timer = setTimeout(() => resolveArg('deadline'), TOKEN_REVOCATION_BUDGET_MS);
|
|
713
|
+
timer.unref();
|
|
714
|
+
});
|
|
715
|
+
try {
|
|
716
|
+
if ((await Promise.race([work, deadline])) === 'deadline') {
|
|
717
|
+
this.emit({
|
|
718
|
+
event: 'oidc_token_revocation_timed_out',
|
|
719
|
+
message: 'The provider did not answer within the revocation budget',
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
finally {
|
|
724
|
+
clearTimeout(timer);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/** A sealed value this deployment can still open, or `undefined`. */
|
|
728
|
+
openSealed(sealedArg) {
|
|
729
|
+
if (!sealedArg) {
|
|
730
|
+
return undefined;
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
return this.#secretBox.open(sealedArg);
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
return undefined;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Removes attempts an hour past their expiry, one bounded page per login
|
|
741
|
+
* start. A sweep that cannot run never blocks a sign-in.
|
|
742
|
+
*/
|
|
743
|
+
async sweepExpiredAttempts() {
|
|
744
|
+
try {
|
|
745
|
+
const page = await this.#models.LoginAttempt.getPagedInstances({
|
|
746
|
+
filter: { expiresAt: { $lt: this.#now() - LOGIN_ATTEMPT_SWEEP_GRACE_MS } },
|
|
747
|
+
sortField: 'expiresAt',
|
|
748
|
+
sortDirection: 'asc',
|
|
749
|
+
limit: LOGIN_ATTEMPT_SWEEP_BATCH,
|
|
750
|
+
});
|
|
751
|
+
for (const attempt of page.documents) {
|
|
752
|
+
await this.#models.LoginAttempt.atomicDelete({ id: attempt.id });
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
this.emit({ event: 'login_attempt_sweep_failed', message: errorMessage(error) });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Reports one diagnostic. Every cleanup step reports through here, so a
|
|
761
|
+
* throwing sink must not end it: the event and the sink's failure go to
|
|
762
|
+
* `console.error` instead.
|
|
763
|
+
*/
|
|
764
|
+
emit(eventArg) {
|
|
765
|
+
const sink = this.#hooks.onEvent;
|
|
766
|
+
if (!sink) {
|
|
767
|
+
console.error(JSON.stringify(eventArg));
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const reportFailure = (errorArg) => {
|
|
771
|
+
const failure = { event: 'hook_failed', hook: 'onEvent', message: errorMessage(errorArg) };
|
|
772
|
+
console.error(JSON.stringify(failure));
|
|
773
|
+
console.error(JSON.stringify(eventArg));
|
|
774
|
+
};
|
|
775
|
+
try {
|
|
776
|
+
const result = sink.call(this.#hooks, eventArg);
|
|
777
|
+
if (result instanceof Promise) {
|
|
778
|
+
result.catch(reportFailure);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
reportFailure(error);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5yZWx5aW5ncGFydHlzZXNzaW9ucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzX3JlbHlpbmdwYXJ0eS9jbGFzc2VzLnJlbHlpbmdwYXJ0eXNlc3Npb25zLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUNuRCxPQUFPLEVBQ0wsbUJBQW1CLEVBQ25CLGtCQUFrQixFQUNsQixpQkFBaUIsRUFDakIsbUJBQW1CLEdBQ3BCLE1BQU0scUJBQXFCLENBQUM7QUFDN0IsT0FBTyxFQUNMLG1CQUFtQixFQUNuQix1QkFBdUIsR0FLeEIsTUFBTSw4QkFBOEIsQ0FBQztBQUV0QyxPQUFPLEVBQUUsWUFBWSxFQUFFLHNCQUFzQixFQUFFLGVBQWUsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQzdGLE9BQU8sRUFBRSxlQUFlLEVBQUUsV0FBVyxFQUFFLE1BQU0sRUFBRSxxQkFBcUIsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBQ2xHLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBZTdELE1BQU0sQ0FBQyxNQUFNLDZDQUE2QyxHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsRUFBRSxDQUFDO0FBQzFFLE1BQU0sQ0FBQyxNQUFNLHVDQUF1QyxHQUFHLFFBQVEsQ0FBQztBQUNoRSxnRkFBZ0Y7QUFDaEYsTUFBTSxDQUFDLE1BQU0sZ0NBQWdDLEdBQUcsa0NBQWtDLEdBQUcsTUFBTSxDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUU1RyxNQUFNLHVCQUF1QixHQUFHLEdBQUcsQ0FBQztBQUNwQyxNQUFNLHlCQUF5QixHQUFHLE1BQU0sQ0FBQztBQUN6QyxvR0FBb0c7QUFDcEcsTUFBTSxDQUFDLE1BQU0sNkJBQTZCLEdBQUcsRUFBRSxHQUFHLEVBQUUsQ0FBQztBQUNyRCwwRUFBMEU7QUFDMUUsTUFBTSw0QkFBNEIsR0FBRyxF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
|