@gydr-ai/mcp-oauth 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -365,7 +365,11 @@ const auth = createMcpAuth({
365
365
  `consentReplaySeconds` (default 10, at most 60) receives the same
366
366
  redirect again, when the store implements the optional
367
367
  `putConsentOutcome` / `getConsentOutcome`; otherwise a repeat sees an
368
- error page. The remembered redirect is stored encrypted.
368
+ error page. A repeat that arrives while the first is still being decided
369
+ (in `beforeGrant` or `afterGrant`) waits for its answer, within the same
370
+ window; the first leaves a short "being decided" mark for that, so a
371
+ forged repeat, which finds none, is answered at once. Everything
372
+ remembered is stored encrypted.
369
373
 
370
374
  **Pages**
371
375
 
@@ -423,9 +427,40 @@ the deleted record so the last stored upstream token is the one revoked.
423
427
  the token endpoint checks the client, redirect URI, resource and PKCE
424
428
  verifier against it first.
425
429
 
426
- Three methods are optional: `putConsentOutcome` / `getConsentOutcome`
427
- (answering a repeated consent submission) and `listGrantsBySubject`
428
- (`revokeGrantsForSubject`).
430
+ `updateGrant` must save every field it is given, including `expiresAt`
431
+ (sent on refresh under `lifetimes.grantIdle`); `putGrant` must keep the
432
+ whole record, including the optional `redirectUri`.
433
+
434
+ Four methods are optional: `putRefreshTokenReplacement` (recommended; see
435
+ below), `putConsentOutcome` / `getConsentOutcome` (answering a repeated
436
+ consent submission; one submission writes twice, the "being decided" mark
437
+ and then the answer, so a later write replaces an earlier one) and
438
+ `listGrantsBySubject` (`revokeGrantsForSubject`).
439
+
440
+ **Refresh retries.** `putRefreshTokenReplacement(hash, sealedReplacement)`
441
+ saves, on the refresh token record just used, the pair it was exchanged
442
+ for, encrypted by your `SecretSealer`; keep every other field (`usedAt`
443
+ included) and return `sealedReplacement` from `getRefreshToken` and
444
+ `useRefreshToken` afterwards. A refresh token presented again within
445
+ `refreshReuseGraceSeconds` then receives exactly that pair, so a copied
446
+ token never starts a second chain: once either holder rotates the shared
447
+ pair, the other's next use is a replay and the grant ends. A retry that
448
+ arrives while the first exchange is still storing its pair gets
449
+ `503 temporarily_unavailable`. Without the method, a retry within the
450
+ window receives an access token only, with no refresh token, and the
451
+ client keeps the refresh token from the response it lost (re-presenting the
452
+ original one within the window if that response never arrived).
453
+
454
+ **Key length.** Any `client_id` a caller sends, up to 2,048 characters,
455
+ can reach `getClient`, and a Client ID Metadata Document URL of that length
456
+ reaches `getCachedClientMetadata` and `putCachedClientMetadata` as a key
457
+ (and grants and tokens carry it as a field). Some databases cap key or index
458
+ length near that (a DynamoDB partition key is at most 2,048 bytes, a MySQL
459
+ index prefix often 767 or 3,072 bytes), especially once you add a prefix.
460
+ Hash long ids into the key, or refuse them in the adapter (`getClient` and
461
+ `getCachedClientMetadata` resolving `undefined`, `putCachedClientMetadata`
462
+ skipping the write); never truncate one, since two ids would then share a
463
+ record, and never let a too-long key throw.
429
464
 
430
465
  Good fits: PostgreSQL or MySQL (`UPDATE … WHERE used_at IS NULL`,
431
466
  `DELETE … RETURNING`), Redis (`GETDEL`, Lua), MongoDB (`findOneAndUpdate`),
@@ -476,47 +511,50 @@ evict the least recently used grant-less dynamic client first when full.
476
511
 
477
512
  ### Options
478
513
 
479
- | Option | Default | Meaning |
480
- | --------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
481
- | `issuer` | required | HTTPS URL; endpoints live under its path. |
482
- | `resource` | required | Canonical URL of your MCP endpoint, the audience of every token. |
483
- | `identityProvider` | required | `createOidcProvider(…)` or your own `IdentityProvider`. |
484
- | `store` | required | Your `OAuthStore`. |
485
- | `sealer` | required | `createAesGcmSealer(…)` or your own `SecretSealer`. |
486
- | `scopes` / `defaultScopes` | `["mcp"]` / all | Scopes offered, and those granted when none are asked for. |
487
- | `scopeDescriptions` | none | Consent-screen text per scope. |
488
- | `resourceName` | none | RFC 9728 `resource_name`. |
489
- | `clients` | none | Pre-registered public clients. |
490
- | `dynamicClientRegistration` | `true` | Serve `POST /register`. Deprecated by MCP 2026-07-28 in favour of metadata documents; `dynamicClientRegistration: false` turns it off. |
491
- | `clientIdMetadataDocuments` | `true` | Accept HTTPS URL client ids. |
492
- | `fetchClientMetadata(url, { signal })` | SSRF-safe fetcher | Replace how metadata documents are downloaded. Must stay SSRF-safe. |
493
- | `lifetimes` | see below | Token and flow lifetimes, in seconds. |
494
- | `limits` | see below | Sizes and counts. |
495
- | `refreshReuseGraceSeconds` | `10` | Retry window (0–300) for a just-used refresh token. `0` treats any reuse as replay. |
496
- | `consentReplaySeconds` | `10` | Window (0–60) in which a repeated consent submission from the same browser gets the same redirect. |
497
- | `scriptHashes` | none | `sha256-…` hashes of inline scripts a custom consent page may run. |
498
- | `paths` | `/authorize`, `/callback`, `/consent`, `/token`, `/register`, `/revoke` | Route paths under the issuer path: one leading `/`, no `//`, `\`, dot segments or whitespace, distinct, and never a metadata path or the resource path. |
499
- | `cookiePrefix` | `__Host-mcp-auth-` | Must start with `__Host-`. |
500
- | `rateLimit(key, scope)` | none | Resolve `true` to continue; anything else (including a throw) answers 429. |
501
- | `getClientIp(request)` / `trustProxy` | none / `false` | Rate-limit key. `trustProxy` uses the right-most `X-Forwarded-For` entry. |
502
- | `renderConsentPage` / `renderErrorPage` | built in | Your own pages; see `ConsentPageContext`. |
503
- | `onEvent(event)` | none | Typed audit events; never contain secrets. |
504
- | policy and enrichment hooks | none | See [Extending safely](#extending-safely). |
505
- | `allowInsecureLocalhost` | `false` | Allow an `http://localhost` issuer and resource in development. |
506
- | `now` | `Date.now` | Clock, for tests. A non-finite value fails the request instead of disabling expiry checks. |
514
+ | Option | Default | Meaning |
515
+ | --------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
516
+ | `issuer` | required | HTTPS URL; endpoints live under its path. |
517
+ | `resource` | required | Canonical URL of your MCP endpoint, the audience of every token. |
518
+ | `identityProvider` | required | `createOidcProvider(…)` or your own `IdentityProvider`. |
519
+ | `store` | required | Your `OAuthStore`. |
520
+ | `sealer` | required | `createAesGcmSealer(…)` or your own `SecretSealer`. |
521
+ | `scopes` / `defaultScopes` | `["mcp"]` / all | Scopes offered, and those granted when none are asked for. |
522
+ | `scopeDescriptions` | none | Consent-screen text per scope. |
523
+ | `resourceName` | none | RFC 9728 `resource_name`. |
524
+ | `clients` | none | Pre-registered public clients. |
525
+ | `dynamicClientRegistration` | `true` | Serve `POST /register`. Deprecated by MCP 2026-07-28 in favour of metadata documents; `dynamicClientRegistration: false` turns it off. |
526
+ | `clientIdMetadataDocuments` | `true` | Accept HTTPS URL client ids. |
527
+ | `fetchClientMetadata(url, { signal })` | SSRF-safe fetcher | Replace how metadata documents are downloaded. Must stay SSRF-safe. |
528
+ | `lifetimes` | see below | Token and flow lifetimes, in seconds. |
529
+ | `limits` | see below | Sizes and counts. |
530
+ | `defaultGrantTypes` | `["authorization_code"]` | Grant types for a client that registers or publishes none, applied at registration or fetch. Must include `authorization_code`. `clients` default to both. |
531
+ | `refreshReuseGraceSeconds` | `10` | Retry window (0–300) for a just-used refresh token: a retry gets the same pair again (see `putRefreshTokenReplacement`), never a second chain. `0` treats any reuse as replay. |
532
+ | `strictTransportSecurity` | `true` | Send `Strict-Transport-Security: max-age=63072000; includeSubDomains` on every response the package builds. `false` only if your proxy already sets it. |
533
+ | `consentReplaySeconds` | `10` | Window (0–60) in which a repeated consent submission from the same browser gets the same redirect. |
534
+ | `scriptHashes` | none | `sha256-…` hashes of inline scripts a custom consent page may run. |
535
+ | `paths` | `/authorize`, `/callback`, `/consent`, `/token`, `/register`, `/revoke` | Route paths under the issuer path: one leading `/`, no `//`, `\`, dot segments or whitespace, distinct, and never a metadata path or the resource path. |
536
+ | `cookiePrefix` | `__Host-mcp-auth-` | Must start with `__Host-`. |
537
+ | `rateLimit(key, scope)` | none | Resolve `true` to continue; anything else (including a throw) answers 429. |
538
+ | `getClientIp(request)` / `trustProxy` | none / `false` | Rate-limit key. `trustProxy` uses the right-most `X-Forwarded-For` entry. |
539
+ | `renderConsentPage` / `renderErrorPage` | built in | Your own pages; see `ConsentPageContext`. |
540
+ | `onEvent(event)` | none | Typed audit events; never contain secrets. |
541
+ | policy and enrichment hooks | none | See [Extending safely](#extending-safely). |
542
+ | `allowInsecureLocalhost` | `false` | Allow an `http://localhost` issuer and resource in development. |
543
+ | `now` | `Date.now` | Clock, for tests. A non-finite value fails the request instead of disabling expiry checks. |
507
544
 
508
545
  <details>
509
546
  <summary><strong><code>lifetimes</code></strong> (seconds)</summary>
510
547
 
511
- | Key | Default | Allowed |
512
- | ----------------------- | ---------------------------------------------------------------------------- | ------------ |
513
- | `accessToken` | 1,800 (30 minutes) | 1–86,400 |
514
- | `refreshToken` | 1,209,600 (14 days, renewed on every rotation) | 1–34,560,000 |
515
- | `grant` | 7,776,000 (90 days, absolute; then consent again) | 1–34,560,000 |
516
- | `authorizationCode` | 60 | 1–600 |
517
- | `pendingAuthorization` | 900 (sign-in plus consent) | 1–3,600 |
518
- | `clientMetadataCache` | 1,800 (`0` disables the cache); a document's own `Cache-Control` shortens it | 0–86,400 |
519
- | `clientMetadataFailure` | 60 (back-off for a failed metadata URL; `0` disables) | 0–3,600 |
548
+ | Key | Default | Allowed |
549
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
550
+ | `accessToken` | 1,800 (30 minutes) | 1–86,400 |
551
+ | `refreshToken` | 1,209,600 (14 days, renewed on every rotation) | 1–34,560,000 |
552
+ | `grant` | 7,776,000 (90 days, absolute; then consent again) | 1–34,560,000 |
553
+ | `grantIdle` | 0 (off). When set, a grant also ends after this long without a refresh; each refresh extends it, never past `grant` | 0, or `accessToken`–34,560,000 |
554
+ | `authorizationCode` | 60 | 1–600 |
555
+ | `pendingAuthorization` | 900 (sign-in plus consent) | 1–3,600 |
556
+ | `clientMetadataCache` | 1,800 (`0` disables the cache); a document's own `Cache-Control` shortens it | 0–86,400 |
557
+ | `clientMetadataFailure` | 60 (back-off for a failed metadata URL; `0` disables) | 0–3,600 |
520
558
 
521
559
  </details>
522
560
 
@@ -582,7 +620,9 @@ OAuth errors or error pages instead. No error message contains a token.
582
620
  - **`grant_types`**: a client that omits it gets `authorization_code` only
583
621
  (RFC 7591 § 2) and **no refresh token**. Clients that want refresh tokens
584
622
  register `["authorization_code", "refresh_token"]`. Metadata documents
585
- are treated the same way.
623
+ are treated the same way. Set `defaultGrantTypes:
624
+ ["authorization_code", "refresh_token"]` to give such clients refresh
625
+ tokens anyway.
586
626
  - **`scope`**: a client that registers a scope can only request scopes
587
627
  within it. Unknown scopes are refused at registration and ignored in a
588
628
  metadata document.
@@ -621,7 +661,7 @@ off, and concurrent requests for one URL share a download.
621
661
  obtained.
622
662
  - Pages send a strict Content-Security-Policy, `X-Frame-Options: DENY`,
623
663
  `Cache-Control: no-store` and `Referrer-Policy: no-referrer`, and escape
624
- every value. Client names with invisible or formatting characters are
664
+ every value. Every response carries `Strict-Transport-Security`. Client names with invisible or formatting characters are
625
665
  refused.
626
666
  - Tokens are 256-bit random values, stored only as SHA-256 hashes, bound to
627
667
  your resource and checked against a live grant on every request. Refresh
@@ -646,7 +686,9 @@ off, and concurrent requests for one URL share a download.
646
686
  rotates refresh tokens with no grace period and several processes may
647
687
  renew one grant at once, route a grant's renewals to one process.
648
688
  - Catch a refresh token copied and used within `refreshReuseGraceSeconds`
649
- of the real use. Use `0` if your clients never retry.
689
+ of the real use at that moment: inside the window both holders get the
690
+ same pair, and the grant ends only when their chains diverge. Use `0` if
691
+ your clients never retry.
650
692
  - Protect against a compromised browser, a client the person chose to
651
693
  approve, or a compromised identity provider.
652
694