@flytedesk/app-kit 0.8.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"oidc-client.js","sourceRoot":"","sources":["../../src/auth/oidc-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"oidc-client.js","sourceRoot":"","sources":["../../src/auth/oidc-client.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,7 @@ import fastifyCookie from "@fastify/cookie";
4
4
  import { AuthStoreError, IdpAccessRevokedError, classifyIdpError } from "./errors.js";
5
5
  import { createOidcClient } from "./oidc-client.js";
6
6
  import { generatePkce, randomState } from "./pkce.js";
7
- import { AUTH_ROUTE_PREFIX, authRoutePath } from "./shared-types.js";
7
+ import { AUTH_REQUEST_DEADLINE_MS, AUTH_ROUTE_PREFIX, authRoutePath, SILENT_AUTH_MESSAGE_SOURCE, } from "./shared-types.js";
8
8
  import { generateRefreshToken, hashRefreshToken, signAccessToken, verifyAccessToken } from "./tokens.js";
9
9
  const DEFAULT_ACCESS_TOKEN_TTL_SECONDS = 900;
10
10
  const DEFAULT_REFRESH_TOKEN_TTL_SECONDS = 2_592_000;
@@ -42,6 +42,15 @@ const flytedeskAuthPlugin = async (fastify, options) => {
42
42
  const accessTokenTtlSeconds = options.accessTokenTtlSeconds ?? DEFAULT_ACCESS_TOKEN_TTL_SECONDS;
43
43
  const refreshTokenTtlSeconds = options.refreshTokenTtlSeconds ?? DEFAULT_REFRESH_TOKEN_TTL_SECONDS;
44
44
  const loginAttemptTtlSeconds = options.loginAttemptTtlSeconds ?? DEFAULT_LOGIN_ATTEMPT_TTL_SECONDS;
45
+ // See FlytedeskAuthOptions.refreshReuseGraceMs's doc comment (AK-14, gap 3; MP-127)
46
+ // for why 2x the client's own request deadline is the right default.
47
+ const refreshReuseGraceMs = options.refreshReuseGraceMs ?? 2 * AUTH_REQUEST_DEADLINE_MS;
48
+ // The origin a silent (`prompt=none`) callback's postMessage targets — see
49
+ // sendSilentResult below. Reuses postLoginRedirect's origin (the SPA a hidden iframe's
50
+ // parent page is running) rather than a new, redundant option: every consumer of the
51
+ // silent flow already has to set postLoginRedirect to that same origin for the
52
+ // interactive flow to land anywhere sensible.
53
+ const silentAuthTargetOrigin = new URL(options.postLoginRedirect).origin;
45
54
  const store = options.store;
46
55
  if (options.authorizationCacheTtlMs) {
47
56
  fastify.log.warn({ authorizationCacheTtlMs: options.authorizationCacheTtlMs }, "@flytedesk/app-kit/auth: authorizationCacheTtlMs is set — flytedesk-id authorization " +
@@ -53,6 +62,8 @@ const flytedeskAuthPlugin = async (fastify, options) => {
53
62
  clientId: options.clientId,
54
63
  clientSecret: options.clientSecret,
55
64
  redirectUri: options.redirectUri,
65
+ scopes: options.scopes,
66
+ idpRequestTimeoutMs: options.idpRequestTimeoutMs,
56
67
  });
57
68
  await fastify.register(fastifyCookie, { secret: options.cookieSecret });
58
69
  fastify.decorateRequest("authUser", undefined);
@@ -83,6 +94,27 @@ const flytedeskAuthPlugin = async (fastify, options) => {
83
94
  maxAge: maxAgeSeconds,
84
95
  };
85
96
  }
97
+ /**
98
+ * The postMessage contract a silent (`prompt=none`) `{routePrefix}/callback` answers
99
+ * with (AK-14, gap 2) — read by `./client/silentAuth.ts`'s hidden-iframe listener.
100
+ * Never rendered by a top-level navigation: only ever loaded inside the hidden iframe
101
+ * `attemptSilentResume()` creates, so this is safe to keep unauthenticated/undecorated
102
+ * HTML — generalized from media-planner's `routes/auth.ts`'s `sendSilentResult`.
103
+ */
104
+ function sendSilentResult(reply, status, detail) {
105
+ const message = {
106
+ source: SILENT_AUTH_MESSAGE_SOURCE,
107
+ status,
108
+ detail: detail ?? null,
109
+ };
110
+ return reply
111
+ .type("text/html")
112
+ .send(`<!doctype html><html><body><script>` +
113
+ `if (window.parent && window.parent !== window) {` +
114
+ `window.parent.postMessage(${JSON.stringify(message)}, ${JSON.stringify(silentAuthTargetOrigin)});` +
115
+ `}` +
116
+ `</script></body></html>`);
117
+ }
86
118
  const authorizationCache = new Map();
87
119
  const EXPIRY_SKEW_MS = 5_000;
88
120
  /**
@@ -90,68 +122,86 @@ const flytedeskAuthPlugin = async (fastify, options) => {
90
122
  * it's not near expiry, otherwise a freshly refreshed one (persisted back via
91
123
  * store.updateIdpSession before returning, since flytedesk-id rotates the refresh
92
124
  * token on every use — see ./oidc-client.ts's refreshIdpTokens doc comment). Shared
93
- * by loadAuthedUser (below) and the GET {routePrefix}/apps route, so the refresh
94
- * behaviour — and its failure classification — can't drift between the two call
95
- * sites. Throws IdpAccessRevokedError (nothing left to refresh with, or flytedesk-id
125
+ * by authorizeIdpSession (below, and therefore by both loadAuthedUser and POST
126
+ * {routePrefix}/refresh) and the GET {routePrefix}/apps route, so the refresh
127
+ * behaviour — and its failure classification — can't drift between call sites.
128
+ * Throws IdpAccessRevokedError (nothing left to refresh with, or flytedesk-id
96
129
  * rejected the refresh token) or IdpUnavailableError (an outage) — never swallows;
97
130
  * every caller is expected to log what it decides to do with the failure (fail
98
131
  * loudly, AK-16).
132
+ *
133
+ * AK-20, gap 2326: the actual refresh happens under `store.withIdpSessionLock`, and
134
+ * RE-READS the session once the lock is held — flytedesk-id rotates its own refresh
135
+ * token on every use, so two concurrent requests on the same near-expiry session
136
+ * would otherwise both read "stale" before either write landed, both present the
137
+ * SAME (about-to-be-consumed) refresh token to flytedesk-id, and the loser would get
138
+ * an otherwise perfectly good session signed out. The re-read lets a lock loser
139
+ * notice a sibling already refreshed while it waited and reuse that result instead of
140
+ * refreshing again.
99
141
  */
100
142
  async function getLiveIdpAccessToken(session) {
101
143
  if (session.idpAccessTokenExpiresAt.getTime() - EXPIRY_SKEW_MS >= Date.now()) {
102
144
  return session.idpAccessToken;
103
145
  }
104
- if (!session.idpRefreshToken) {
105
- throw new IdpAccessRevokedError("No IdP refresh token on this session — re-login required");
106
- }
107
146
  try {
108
- const refreshed = await oidc.refreshIdpTokens(session.idpRefreshToken);
109
- await store.updateIdpSession(session.id, {
110
- idpAccessToken: refreshed.access_token,
111
- idpAccessTokenExpiresAt: new Date(Date.now() + refreshed.expires_in * 1000),
112
- idpRefreshToken: refreshed.refresh_token ?? session.idpRefreshToken,
147
+ return await store.withIdpSessionLock(session.id, async () => {
148
+ const fresh = await store.findIdpSession(session.id);
149
+ if (!fresh) {
150
+ throw new IdpAccessRevokedError("IdP session no longer exists — re-login required");
151
+ }
152
+ if (fresh.idpAccessTokenExpiresAt.getTime() - EXPIRY_SKEW_MS >= Date.now()) {
153
+ // A sibling request already refreshed this session while this one waited for
154
+ // the lock — reuse its result rather than refreshing (and rotating) again.
155
+ return fresh.idpAccessToken;
156
+ }
157
+ if (!fresh.idpRefreshToken) {
158
+ throw new IdpAccessRevokedError("No IdP refresh token on this session — re-login required");
159
+ }
160
+ const refreshed = await oidc.refreshIdpTokens(fresh.idpRefreshToken);
161
+ await store.updateIdpSession(fresh.id, {
162
+ idpAccessToken: refreshed.access_token,
163
+ idpAccessTokenExpiresAt: new Date(Date.now() + refreshed.expires_in * 1000),
164
+ idpRefreshToken: refreshed.refresh_token ?? fresh.idpRefreshToken,
165
+ });
166
+ return refreshed.access_token;
113
167
  });
114
- return refreshed.access_token;
115
168
  }
116
169
  catch (err) {
117
170
  throw classifyIdpError(err);
118
171
  }
119
172
  }
120
173
  /**
121
- * Live per-request by default: resolves `request.authUser` from the access token's
122
- * `sid` by looking up the IdpSession, refreshing the IdP token pair if it's near
123
- * expiry, then calling flytedesk-id's authorization endpoint fresh — see
124
- * FlytedeskAuthOptions.authorizationCacheTtlMs for the one opt-in exception. Never
125
- * throws: any failure just means no AuthedUser gets attached — requireAuth 401s from
126
- * there. Every failure that is NOT a routine "no session"/"nothing to refresh with"
127
- * outcome is still logged with context (fail loudly, AK-16), distinguishing a
128
- * flytedesk-id outage from a genuinely revoked session from an AuthStore failure.
174
+ * The authorization pipeline shared by loadAuthedUser (below, run on every
175
+ * authenticated request from an access token's already-verified claims) and POST
176
+ * {routePrefix}/refresh (AK-20, gap 2) — refreshes the IdP token pair if it's near
177
+ * expiry, then calls flytedesk-id's authorization endpoint fresh (see
178
+ * FlytedeskAuthOptions.authorizationCacheTtlMs for the one opt-in exception), builds
179
+ * the AuthedUser, then runs the onAuthorizeUser hook if configured. Extracted into one
180
+ * function specifically so `/refresh` can never drift from what an ordinary
181
+ * authenticated request would decide: a refresh must not mint a fresh access token for
182
+ * a user this SAME pipeline would reject (e.g. a suspended user) — otherwise that user
183
+ * could keep refreshing (rotating their family, extending its lifetime) indefinitely
184
+ * even though every resource request the minted token is then used for still 401s.
185
+ * Never throws: any failure just means no AuthedUser — the caller decides what that
186
+ * means for its own route. Every failure that is NOT a routine "nothing to refresh
187
+ * with" outcome is still logged with context (fail loudly, AK-16), distinguishing a
188
+ * flytedesk-id outage from a genuinely revoked session.
129
189
  */
130
- async function loadAuthedUser(request, claims) {
131
- let session;
132
- try {
133
- session = await store.findIdpSession(claims.sid);
134
- }
135
- catch (err) {
136
- request.log.error({ err: new AuthStoreError("AuthStore.findIdpSession failed", err), sid: claims.sid }, "@flytedesk/app-kit/auth: AuthStore error looking up IdP session");
137
- return undefined;
138
- }
139
- if (!session || session.subject !== claims.sub)
140
- return undefined; // no session for this claim — expected, not an error
190
+ async function authorizeIdpSession(request, session) {
141
191
  let idpAccessToken;
142
192
  try {
143
193
  idpAccessToken = await getLiveIdpAccessToken(session);
144
194
  }
145
195
  catch (err) {
146
196
  const classified = classifyIdpError(err);
147
- request.log.error({ err: classified, sid: claims.sid }, classified instanceof IdpAccessRevokedError
197
+ request.log.error({ err: classified, sid: session.id }, classified instanceof IdpAccessRevokedError
148
198
  ? "@flytedesk/app-kit/auth: flytedesk-id refresh token revoked or expired — forcing re-login"
149
199
  : "@flytedesk/app-kit/auth: flytedesk-id token refresh failed — possible IdP outage");
150
200
  return undefined;
151
201
  }
152
202
  let authorization;
153
203
  if (options.authorizationCacheTtlMs) {
154
- const cached = authorizationCache.get(claims.sid);
204
+ const cached = authorizationCache.get(session.id);
155
205
  if (cached && cached.expiresAt > Date.now())
156
206
  authorization = cached.value;
157
207
  }
@@ -161,13 +211,13 @@ const flytedeskAuthPlugin = async (fastify, options) => {
161
211
  }
162
212
  catch (err) {
163
213
  const classified = classifyIdpError(err);
164
- request.log.error({ err: classified, sid: claims.sid }, classified instanceof IdpAccessRevokedError
214
+ request.log.error({ err: classified, sid: session.id }, classified instanceof IdpAccessRevokedError
165
215
  ? "@flytedesk/app-kit/auth: flytedesk-id revoked this session's authorization"
166
216
  : "@flytedesk/app-kit/auth: flytedesk-id authorization lookup failed — possible IdP outage");
167
217
  return undefined;
168
218
  }
169
219
  if (options.authorizationCacheTtlMs) {
170
- authorizationCache.set(claims.sid, {
220
+ authorizationCache.set(session.id, {
171
221
  value: authorization,
172
222
  expiresAt: Date.now() + options.authorizationCacheTtlMs,
173
223
  });
@@ -177,7 +227,7 @@ const flytedeskAuthPlugin = async (fastify, options) => {
177
227
  // never collide across apps in its catalog — strip the prefix so the consuming
178
228
  // app's own permission checks don't have to know about it.
179
229
  const prefix = `${options.appId}:`;
180
- return {
230
+ const user = {
181
231
  id: session.subject,
182
232
  email: session.email,
183
233
  name: session.name,
@@ -185,6 +235,45 @@ const flytedeskAuthPlugin = async (fastify, options) => {
185
235
  roles: authorization.roles,
186
236
  permissions: authorization.permissions.map((p) => (p.startsWith(prefix) ? p.slice(prefix.length) : p)),
187
237
  };
238
+ // AK-14, gap 4: the one extension point a consumer has to reject or enrich an
239
+ // otherwise-valid, still-IdP-authorized session on every request (e.g. media-planner's
240
+ // own local suspension flag) — see FlytedeskAuthOptions.onAuthorizeUser's doc comment.
241
+ // A throwing/rejecting hook is treated exactly like a `null` return: fail loudly
242
+ // (logged with context, AK-16) but never a 500 — a hook error must never be
243
+ // indistinguishable from "authenticated".
244
+ if (options.onAuthorizeUser) {
245
+ let authorized;
246
+ try {
247
+ authorized = await options.onAuthorizeUser(user, request);
248
+ }
249
+ catch (err) {
250
+ request.log.error({ err, sid: session.id }, "@flytedesk/app-kit/auth: onAuthorizeUser threw — treating the session as rejected");
251
+ return undefined;
252
+ }
253
+ if (!authorized)
254
+ return undefined;
255
+ return authorized;
256
+ }
257
+ return user;
258
+ }
259
+ /**
260
+ * Live per-request by default: resolves `request.authUser` from the access token's
261
+ * `sid` by looking up the IdpSession, then running it through authorizeIdpSession
262
+ * above. Never throws: any failure just means no AuthedUser gets attached —
263
+ * requireAuth 401s from there.
264
+ */
265
+ async function loadAuthedUser(request, claims) {
266
+ let session;
267
+ try {
268
+ session = await store.findIdpSession(claims.sid);
269
+ }
270
+ catch (err) {
271
+ request.log.error({ err: new AuthStoreError("AuthStore.findIdpSession failed", err), sid: claims.sid }, "@flytedesk/app-kit/auth: AuthStore error looking up IdP session");
272
+ return undefined;
273
+ }
274
+ if (!session || session.subject !== claims.sub)
275
+ return undefined; // no session for this claim — expected, not an error
276
+ return authorizeIdpSession(request, session);
188
277
  }
189
278
  // onRequest, not preHandler: this needs to run before any route-level preHandler
190
279
  // (requireAuth/requirePermission included) so request.authUser is already resolved
@@ -215,6 +304,8 @@ const flytedeskAuthPlugin = async (fastify, options) => {
215
304
  idpSessionId,
216
305
  userAgent: request.headers["user-agent"] ?? null,
217
306
  ip: request.ip,
307
+ // The first token of a fresh login — nothing precedes it in the family.
308
+ replacesId: null,
218
309
  });
219
310
  const { token: accessToken, expiresAt: accessExpiresAt } = await signAccessToken(options.accessTokenSecret, accessTokenTtlSeconds, userId, idpSessionId);
220
311
  reply.setCookie(refreshCookieName, refreshToken, refreshCookieOptions(refreshTokenTtlSeconds));
@@ -222,15 +313,43 @@ const flytedeskAuthPlugin = async (fastify, options) => {
222
313
  }
223
314
  /**
224
315
  * GET {routePrefix}/login — starts the flytedesk-id sign-in. A plain redirect meant
225
- * to be hit via full-page navigation, not fetch/XHR. Generates a PKCE verifier/
226
- * challenge (S256 — flytedesk-id makes PKCE mandatory) and a CSRF `state`, stashes
227
- * both in a short-lived signed cookie tied to this browser, then redirects to
228
- * flytedesk-id's own /auth endpoint.
316
+ * to be hit via full-page navigation, not fetch/XHR (except for the `?prompt=none`
317
+ * silent case below, which is driven from a hidden iframe — same route, same
318
+ * redirect, just never shown). Generates a PKCE verifier/challenge (S256 —
319
+ * flytedesk-id makes PKCE mandatory) and a CSRF `state`, stashes both (plus whether
320
+ * this attempt is silent) in a short-lived signed cookie tied to this browser, then
321
+ * redirects to flytedesk-id's own /auth endpoint.
322
+ *
323
+ * `?prompt=none` (AK-14, gap 2; OIDC Core 3.1.2.1) is the silent-resume flow
324
+ * `./client/silentAuth.ts` drives from a hidden iframe: `prompt` is passed straight
325
+ * through to flytedesk-id, which answers from its own existing end-user session
326
+ * cookie with no visible UI at all — a fresh `code` if that session is live,
327
+ * `error=login_required` back to redirect_uri if it's not. Everything else about this
328
+ * route is identical to an interactive sign-in; only the stashed `silent` flag (which
329
+ * routes the callback's response — see GET {routePrefix}/callback below) and the
330
+ * `prompt` passed to buildAuthorizeUrl differ.
331
+ *
332
+ * `prompt` is schema-validated to the literal `"none"` (AK-20, gap 4) — the only
333
+ * value this route's silent-resume handling actually understands; anything else (a
334
+ * typo, or some other OIDC prompt semantics this plugin never implements) is rejected
335
+ * with a 400 by Fastify's own schema validation rather than being passed straight
336
+ * through to flytedesk-id's `/auth` endpoint unexamined.
229
337
  */
230
- fastify.get(authRoutePath(routePrefix, "login"), async (request, reply) => {
338
+ fastify.get(authRoutePath(routePrefix, "login"), {
339
+ schema: {
340
+ querystring: {
341
+ type: "object",
342
+ properties: {
343
+ prompt: { type: "string", enum: ["none"] },
344
+ },
345
+ },
346
+ },
347
+ }, async (request, reply) => {
231
348
  const { verifier, challenge } = generatePkce();
232
349
  const state = randomState();
233
- reply.setCookie(loginCookieName, JSON.stringify({ verifier, state }), {
350
+ const prompt = request.query.prompt;
351
+ const silent = prompt === "none";
352
+ reply.setCookie(loginCookieName, JSON.stringify({ verifier, state, silent }), {
234
353
  httpOnly: true,
235
354
  sameSite: "lax",
236
355
  secure: options.secureCookies,
@@ -238,7 +357,7 @@ const flytedeskAuthPlugin = async (fastify, options) => {
238
357
  maxAge: loginAttemptTtlSeconds,
239
358
  signed: true,
240
359
  });
241
- return reply.redirect(oidc.buildAuthorizeUrl({ state, challenge }));
360
+ return reply.redirect(oidc.buildAuthorizeUrl({ state, challenge, prompt }));
242
361
  });
243
362
  /**
244
363
  * GET {routePrefix}/callback — flytedesk-id's redirect_uri for this client. Validates
@@ -251,33 +370,41 @@ const flytedeskAuthPlugin = async (fastify, options) => {
251
370
  const { code, state, error } = request.query;
252
371
  const raw = request.cookies[loginCookieName];
253
372
  reply.clearCookie(loginCookieName, { path: cookiePath });
254
- if (error) {
255
- return reply
256
- .code(400)
257
- .send({ error: `flytedesk-id sign-in failed: ${error}`, code: "idp_error" });
373
+ // Recover `silent` before any failure branch below runs, including ones that
374
+ // never get as far as validating the rest of the stash — a `prompt=none` attempt
375
+ // that fails (e.g. `error=login_required`, the expected outcome when the
376
+ // IdP-side session is genuinely gone too) still needs to answer through
377
+ // sendSilentResult, or the hidden iframe's listener would wait out its full
378
+ // timeout for a postMessage that was never going to arrive (AK-14, gap 2).
379
+ let stash;
380
+ if (raw) {
381
+ const unsigned = request.unsignCookie(raw);
382
+ if (unsigned.valid && unsigned.value) {
383
+ try {
384
+ stash = JSON.parse(unsigned.value);
385
+ }
386
+ catch {
387
+ stash = undefined;
388
+ }
389
+ }
258
390
  }
259
- if (!code || !state || !raw) {
260
- return reply.code(400).send({
261
- error: "Missing code/state, or the login attempt expired",
262
- code: "invalid_callback",
263
- });
391
+ const silent = stash?.silent === true;
392
+ function fail(status, code, message) {
393
+ if (silent)
394
+ return sendSilentResult(reply, "error", code);
395
+ return reply.code(status).send({ error: message, code });
264
396
  }
265
- const unsigned = request.unsignCookie(raw);
266
- if (!unsigned.valid || !unsigned.value) {
267
- return reply.code(400).send({
268
- error: "Login attempt cookie failed verification",
269
- code: "invalid_login_cookie",
270
- });
397
+ if (error) {
398
+ return fail(400, error, `flytedesk-id sign-in failed: ${error}`);
271
399
  }
272
- let stash;
273
- try {
274
- stash = JSON.parse(unsigned.value);
400
+ if (!code || !state || !raw) {
401
+ return fail(400, "invalid_callback", "Missing code/state, or the login attempt expired");
275
402
  }
276
- catch {
277
- return reply.code(400).send({ error: "Corrupt login attempt", code: "invalid_login_cookie" });
403
+ if (!stash) {
404
+ return fail(400, "invalid_login_cookie", "Login attempt cookie failed verification");
278
405
  }
279
406
  if (stash.state !== state) {
280
- return reply.code(400).send({ error: "State mismatch", code: "invalid_state" });
407
+ return fail(400, "invalid_state", "State mismatch");
281
408
  }
282
409
  try {
283
410
  const tokens = await oidc.exchangeCodeForTokens(code, stash.verifier);
@@ -286,16 +413,10 @@ const flytedeskAuthPlugin = async (fastify, options) => {
286
413
  const idToken = await oidc.verifyIdToken(tokens.id_token);
287
414
  const profile = await oidc.fetchUserinfo(tokens.access_token);
288
415
  if (profile.sub !== idToken.sub) {
289
- return reply.code(401).send({
290
- error: "flytedesk-id id_token/userinfo subject mismatch",
291
- code: "subject_mismatch",
292
- });
416
+ return fail(401, "subject_mismatch", "flytedesk-id id_token/userinfo subject mismatch");
293
417
  }
294
418
  if (!profile.emailVerified) {
295
- return reply.code(401).send({
296
- error: "flytedesk-id account email is not verified",
297
- code: "email_not_verified",
298
- });
419
+ return fail(401, "email_not_verified", "flytedesk-id account email is not verified");
299
420
  }
300
421
  if (options.onSignIn) {
301
422
  await options.onSignIn(profile, request);
@@ -313,26 +434,61 @@ const flytedeskAuthPlugin = async (fastify, options) => {
313
434
  // only ever revokes the chain descended from one login, never sibling sessions.
314
435
  const familyId = randomUUID();
315
436
  await issueSession(request, reply, profile.sub, idpSession.id, familyId);
437
+ if (silent) {
438
+ // No top-level navigation at all — issueSession() above already set the real
439
+ // refresh cookie on this (the API's) origin as a side effect of this same
440
+ // response, which is the only thing that actually needed to happen. This page
441
+ // just tells the hidden iframe's listener it can stop waiting; the parent page
442
+ // picks the new session up via its own regular {routePrefix}/refresh + /me,
443
+ // exactly like any other reload would (AK-14, gap 2).
444
+ return sendSilentResult(reply, "success");
445
+ }
316
446
  return reply.redirect(options.postLoginRedirect);
317
447
  }
318
448
  catch (err) {
319
449
  request.log.error({ err }, "flytedesk-id callback failed");
320
- return reply
321
- .code(502)
322
- .send({ error: "Sign-in with flytedesk-id failed", code: "idp_exchange_failed" });
450
+ return fail(502, "idp_exchange_failed", "Sign-in with flytedesk-id failed");
323
451
  }
324
452
  });
453
+ /** The one answer POST {routePrefix}/refresh gives a refresh token it will not
454
+ * rotate. `clearCookie` only for a token that is genuinely dead (unrecognized,
455
+ * expired, or reused — its whole family just ended); NOT for one that merely lost a
456
+ * concurrent-tab race (AK-14, gap 3; MP-127): the winner is setting the browser's
457
+ * fresh cookie at the same moment, and a clearing Set-Cookie on the loser's response
458
+ * can land after it and delete the fresh token. */
459
+ function rejectRefresh(reply, { clearCookie }) {
460
+ if (clearCookie)
461
+ reply.clearCookie(refreshCookieName, { path: cookiePath });
462
+ return reply.code(401).send({ error: "Refresh token expired or revoked", code: "unauthenticated" });
463
+ }
325
464
  /**
326
465
  * POST {routePrefix}/refresh — no body, reads the refresh cookie. Rotates it on every
327
- * successful call. A hash match on a row that's already revoked means this exact
328
- * token was replayed (its legitimate rotation already consumed it) — that's treated
329
- * as a security event: the whole family is revoked, not just this row, since every
330
- * token downstream of the replayed one must be assumed compromised too.
466
+ * successful call.
467
+ *
468
+ * A presented token can be rejected two different ways (AK-14, gap 3; MP-127; RFC
469
+ * 9700 §4.14.2), and telling them apart is the whole point of this route:
331
470
  *
332
- * The claim (store.claimRefreshTokenForRotation) happens BEFORE the expiry check and
333
- * is what actually detects reuse — not a `stored.revokedAt` read, which would leave a
334
- * window between two concurrent requests both observing "not yet revoked" before
335
- * either write lands. See that method's doc comment in ./types.ts.
471
+ * - It was ALREADY revoked before this request even started
472
+ * (`stored.revokedAt`, from `findRefreshTokenByHash`'s own read, is non-null) — a
473
+ * LATE event, from this request's point of view. If it was rotated
474
+ * (`stored.rotated`) and that rotation happened less than `refreshReuseGraceMs`
475
+ * ago, it's still plausibly a concurrent-tab race whose loser's request simply
476
+ * took a while to arrive: 401, nothing revoked. Otherwise (never rotated at all, or
477
+ * outside the window) it's reuse: the whole family is revoked, since every token
478
+ * downstream of the replayed one must be assumed compromised too.
479
+ * - It looked live at that same read (`revokedAt: null`), but a SIBLING request
480
+ * claims it first (`store.claimRefreshTokenForRotation` returns `false`) — a
481
+ * genuine, same-instant concurrent-tab race, decided purely by that method's
482
+ * atomicity. Treated as a race unconditionally: no rotated/timing check needed
483
+ * (there's nothing to check yet — the winner's rotation is still in flight), 401,
484
+ * nothing revoked. This mirrors media-planner's own shape exactly
485
+ * (`updateMany({revokedAt: null}); if (count !== 1) return false`).
486
+ *
487
+ * The claim happens BEFORE the expiry check and is what actually detects that second
488
+ * kind of loss — not a re-read of `revokedAt`, which would leave the exact window this
489
+ * method exists to close: two concurrent requests both observing "not yet revoked"
490
+ * before either write lands. See `AuthStore.claimRefreshTokenForRotation`'s doc
491
+ * comment in ./types.ts.
336
492
  */
337
493
  fastify.post(authRoutePath(routePrefix, "refresh"), async (request, reply) => {
338
494
  const token = request.cookies[refreshCookieName];
@@ -345,25 +501,77 @@ const flytedeskAuthPlugin = async (fastify, options) => {
345
501
  reply.clearCookie(refreshCookieName, { path: cookiePath });
346
502
  return reply.code(401).send({ error: "Refresh token not recognized", code: "unauthenticated" });
347
503
  }
348
- const claimed = await store.claimRefreshTokenForRotation(stored.id);
349
- if (!claimed) {
350
- // Already revoked by the time this call's UPDATE ran — either a genuine prior
351
- // revocation or a concurrent /refresh call that won the race. Both are reuse.
352
- request.log.warn({ familyId: stored.familyId, userId: stored.userId }, "@flytedesk/app-kit/auth: refresh token reuse detected — revoking token family");
353
- await store.revokeFamily(stored.familyId);
504
+ if (stored.revokedAt) {
505
+ const sinceRevokedMs = Date.now() - stored.revokedAt.getTime();
506
+ if (stored.rotated && sinceRevokedMs < refreshReuseGraceMs) {
507
+ return rejectRefresh(reply, { clearCookie: false });
508
+ }
509
+ request.log.warn({
510
+ familyId: stored.familyId,
511
+ userId: stored.userId,
512
+ rotated: stored.rotated,
513
+ sinceRevokedMs,
514
+ }, "@flytedesk/app-kit/auth: refresh token reuse detected — revoking token family");
515
+ await store.revokeFamily(stored.familyId, {
516
+ type: "reuse_detected",
517
+ replayedToken: stored,
518
+ sinceRotatedMs: sinceRevokedMs,
519
+ ip: request.ip,
520
+ userAgent: request.headers["user-agent"] ?? null,
521
+ });
354
522
  reply.clearCookie(refreshCookieName, { path: cookiePath });
355
523
  return reply.code(401).send({ error: "Refresh token reuse detected", code: "token_reuse_detected" });
356
524
  }
357
525
  if (stored.expiresAt < new Date()) {
358
- // Claimed (now revoked, so it can never be presented again) but past its TTL —
359
- // an expired token, not a reuse event.
360
526
  reply.clearCookie(refreshCookieName, { path: cookiePath });
361
527
  return reply.code(401).send({ error: "Refresh token expired", code: "unauthenticated" });
362
528
  }
529
+ // AK-20, gap 2: run the SAME authorization pipeline loadAuthedUser runs on every
530
+ // other authenticated request — including onAuthorizeUser — before minting a fresh
531
+ // access token. Without this, a user the app suspended (via onAuthorizeUser) could
532
+ // keep refreshing forever: their resource requests with the minted token would
533
+ // still 401, but nothing ever ended the session, so their family kept rotating and
534
+ // its lifetime kept extending indefinitely. A rejection here revokes the whole
535
+ // family, exactly like a detected reuse — the point of rotating at all is defeated
536
+ // if a rejected user's family just keeps living. Deliberately BEFORE the atomic
537
+ // claim below (not after): this call can be slow (a real network round trip to
538
+ // flytedesk-id), and running it between the claim and the mint would widen the
539
+ // window in which a genuinely concurrent sibling request's read of this same row
540
+ // could land — the atomic claim and the mint that follows it must stay adjacent,
541
+ // exactly as they were before this card, or the plugin's own concurrent-tab-race
542
+ // handling (gap 3) becomes measurably flakier under load, not just under test.
543
+ let idpSession;
544
+ try {
545
+ idpSession = await store.findIdpSession(stored.idpSessionId);
546
+ }
547
+ catch (err) {
548
+ request.log.error({ err: new AuthStoreError("AuthStore.findIdpSession failed", err), idpSessionId: stored.idpSessionId }, "@flytedesk/app-kit/auth: /refresh AuthStore error looking up IdP session");
549
+ return reply.code(502).send({ error: "Could not load the signed-in session", code: "auth_store_failed" });
550
+ }
551
+ if (!idpSession) {
552
+ request.log.warn({ familyId: stored.familyId, idpSessionId: stored.idpSessionId }, "@flytedesk/app-kit/auth: /refresh — no IdP session left for this refresh token — revoking family");
553
+ await store.revokeFamily(stored.familyId, { type: "authorization_rejected" });
554
+ reply.clearCookie(refreshCookieName, { path: cookiePath });
555
+ return reply.code(401).send({ error: "Session not found", code: "unauthenticated" });
556
+ }
557
+ const authorized = await authorizeIdpSession(request, idpSession);
558
+ if (!authorized) {
559
+ request.log.warn({ familyId: stored.familyId, userId: stored.userId }, "@flytedesk/app-kit/auth: /refresh rejected by the authorization check — revoking token family");
560
+ await store.revokeFamily(stored.familyId, { type: "authorization_rejected" });
561
+ reply.clearCookie(refreshCookieName, { path: cookiePath });
562
+ return reply.code(401).send({ error: "Authorization rejected", code: "unauthenticated" });
563
+ }
564
+ const claimed = await store.claimRefreshTokenForRotation(stored.id);
565
+ if (!claimed) {
566
+ // A sibling request claimed it first, in the tiny window since the read above —
567
+ // a same-instant concurrent-tab race, not reuse (see the route's doc comment).
568
+ return rejectRefresh(reply, { clearCookie: false });
569
+ }
363
570
  // Rotate: the claim above already revoked the used token; issue a fresh one in the
364
571
  // same family — limits replay if a cookie leaks, and idpSessionId carries forward
365
572
  // unchanged so this session's link to its flytedesk-id token pair survives the
366
- // rotation.
573
+ // rotation. `replacesId` records the rotation so a later reuse-vs-race decision on
574
+ // THIS token (`stored.id`) can find it.
367
575
  const next = generateRefreshToken();
368
576
  const expiresAt = new Date(Date.now() + refreshTokenTtlSeconds * 1000);
369
577
  await store.createRefreshToken({
@@ -375,6 +583,7 @@ const flytedeskAuthPlugin = async (fastify, options) => {
375
583
  idpSessionId: stored.idpSessionId,
376
584
  userAgent: request.headers["user-agent"] ?? null,
377
585
  ip: request.ip,
586
+ replacesId: stored.id,
378
587
  });
379
588
  const { token: accessToken, expiresAt: accessExpiresAt } = await signAccessToken(options.accessTokenSecret, accessTokenTtlSeconds, stored.userId, stored.idpSessionId);
380
589
  reply.setCookie(refreshCookieName, next.token, refreshCookieOptions(refreshTokenTtlSeconds));
@@ -465,7 +674,7 @@ const flytedeskAuthPlugin = async (fastify, options) => {
465
674
  if (stored) {
466
675
  // Revoke the local family FIRST and unconditionally — that's what actually
467
676
  // keeps a signed-out browser locked out, independent of anything below.
468
- await store.revokeFamily(stored.familyId);
677
+ await store.revokeFamily(stored.familyId, { type: "signed_out" });
469
678
  const idpSession = await store.findIdpSession(stored.idpSessionId);
470
679
  if (idpSession?.idpRefreshToken) {
471
680
  // revokeIdpToken's own contract (see oidc-client.ts) is never to throw — this