@gydr-ai/mcp-oauth 0.1.1 → 0.1.3
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 +116 -58
- package/dist/index.js +15 -15
- package/dist/index.js.map +4 -4
- package/dist/types/core/grant-expiry.d.ts +1 -0
- package/dist/types/interfaces/options.d.ts +17 -0
- package/dist/types/interfaces/records.d.ts +8 -1
- package/dist/types/interfaces/store.d.ts +4 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -12,9 +12,14 @@
|
|
|
12
12
|
<img alt="MCP 2026-07-28" src="https://img.shields.io/badge/MCP-2026--07--28-6f42c1" />
|
|
13
13
|
<img alt="OAuth 2.1 draft-17" src="https://img.shields.io/badge/OAuth_2.1-draft--17-0969da" />
|
|
14
14
|
<img alt="CIMD draft-03" src="https://img.shields.io/badge/CIMD-draft--03-0969da" />
|
|
15
|
+
<img alt="Node.js 22.12+" src="https://img.shields.io/node/v/@gydr-ai/mcp-oauth" />
|
|
15
16
|
<a href="LICENSE"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/license-Apache--2.0-blue" /></a>
|
|
16
17
|
</p>
|
|
17
18
|
|
|
19
|
+
<p align="center">
|
|
20
|
+
<sub>Built and maintained by <a href="https://gydr.ai"><strong>Gydr</strong></a></sub>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
18
23
|
---
|
|
19
24
|
|
|
20
25
|
A remote [Model Context Protocol](https://modelcontextprotocol.io) server
|
|
@@ -31,9 +36,9 @@ The provider's tokens never leave your server.
|
|
|
31
36
|
## Highlights
|
|
32
37
|
|
|
33
38
|
- **Spec-complete for MCP.** Protected-resource and authorization-server
|
|
34
|
-
metadata, Client ID Metadata Documents
|
|
35
|
-
default)
|
|
36
|
-
|
|
39
|
+
metadata, Client ID Metadata Documents, Dynamic Client Registration
|
|
40
|
+
(deprecated, still on by default), PKCE, resource indicators and issuer
|
|
41
|
+
identification: everything the
|
|
37
42
|
[MCP Authorization specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization)
|
|
38
43
|
asks of a server.
|
|
39
44
|
- **No token passthrough.** Clients get tokens bound to your MCP endpoint.
|
|
@@ -62,6 +67,8 @@ The provider's tokens never leave your server.
|
|
|
62
67
|
- [Security model](#security-model)
|
|
63
68
|
- [Specification compliance](#specification-compliance)
|
|
64
69
|
- [Compatibility](#compatibility)
|
|
70
|
+
- [Security](#security)
|
|
71
|
+
- [About Gydr](#about-gydr)
|
|
65
72
|
|
|
66
73
|
## How it works
|
|
67
74
|
|
|
@@ -282,9 +289,10 @@ const auth = createMcpAuth({
|
|
|
282
289
|
literal `true` (or `{ allow: true }`) continues; `false`, any other value, a
|
|
283
290
|
rejection or a throw refuses. A refusal may name a reason,
|
|
284
291
|
`{ allow: false, reason: "no_seat" }` (1–64 characters of
|
|
285
|
-
`A-Z a-z 0-9 _ . : -`), which reaches your error page as `policyReason`.
|
|
286
|
-
|
|
287
|
-
|
|
292
|
+
`A-Z a-z 0-9 _ . : -`), which reaches your error page as `policyReason`.
|
|
293
|
+
Every hook receives deeply frozen copies of the data it inspects (the
|
|
294
|
+
client, scopes, metadata, claims and grant), so it can read validated state
|
|
295
|
+
but cannot change what the server goes on to use.
|
|
288
296
|
|
|
289
297
|
| Hook | Called | On refusal |
|
|
290
298
|
| ------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
@@ -357,7 +365,11 @@ const auth = createMcpAuth({
|
|
|
357
365
|
`consentReplaySeconds` (default 10, at most 60) receives the same
|
|
358
366
|
redirect again, when the store implements the optional
|
|
359
367
|
`putConsentOutcome` / `getConsentOutcome`; otherwise a repeat sees an
|
|
360
|
-
error page.
|
|
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.
|
|
361
373
|
|
|
362
374
|
**Pages**
|
|
363
375
|
|
|
@@ -410,14 +422,19 @@ or a delete that returns the row, never a read followed by a write.
|
|
|
410
422
|
Expired records read as absent and may be deleted at any time, for example
|
|
411
423
|
by a database TTL (`getConsentRequest` may also return an expired one).
|
|
412
424
|
`deleteGrant` must make `getGrant` return nothing, and should resolve with
|
|
413
|
-
the deleted record so the last stored upstream token is the one revoked.
|
|
414
|
-
reads a code, consumed or not, without using it up:
|
|
415
|
-
checks the client, redirect URI, resource and PKCE
|
|
416
|
-
first.
|
|
425
|
+
the deleted record so the last stored upstream token is the one revoked.
|
|
426
|
+
`getAuthorizationCode` reads a code, consumed or not, without using it up:
|
|
427
|
+
the token endpoint checks the client, redirect URI, resource and PKCE
|
|
428
|
+
verifier against it first.
|
|
429
|
+
|
|
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`.
|
|
417
433
|
|
|
418
434
|
Three methods are optional: `putConsentOutcome` / `getConsentOutcome`
|
|
419
|
-
(answering a repeated consent submission
|
|
420
|
-
|
|
435
|
+
(answering a repeated consent submission; one submission writes twice, the
|
|
436
|
+
"being decided" mark and then the answer, so a later write replaces an
|
|
437
|
+
earlier one) and `listGrantsBySubject` (`revokeGrantsForSubject`).
|
|
421
438
|
|
|
422
439
|
Good fits: PostgreSQL or MySQL (`UPDATE … WHERE used_at IS NULL`,
|
|
423
440
|
`DELETE … RETURNING`), Redis (`GETDEL`, Lua), MongoDB (`findOneAndUpdate`),
|
|
@@ -439,9 +456,9 @@ evict the least recently used grant-less dynamic client first when full.
|
|
|
439
456
|
`trustProxy` behind exactly one proxy you control). **Without
|
|
440
457
|
`getClientIp` or `trustProxy`, every request shares the single key
|
|
441
458
|
`"unknown"`**, so one busy client can exhaust the limit for everyone.
|
|
442
|
-
Without `rateLimit`, `POST /register` is open to flooding. A starting
|
|
443
|
-
registrations per address per hour, 30 authorization starts
|
|
444
|
-
address per minute.
|
|
459
|
+
Without `rateLimit`, `POST /register` is open to flooding. A starting
|
|
460
|
+
point: 10 registrations per address per hour, 30 authorization starts
|
|
461
|
+
per address per minute.
|
|
445
462
|
- [ ] **Alerts** from `onEvent` on `refresh_token_replayed`,
|
|
446
463
|
`authorization_code_replayed`, and bursts of `flow_binding_mismatch` or
|
|
447
464
|
`client_metadata_refused`.
|
|
@@ -468,47 +485,49 @@ evict the least recently used grant-less dynamic client first when full.
|
|
|
468
485
|
|
|
469
486
|
### Options
|
|
470
487
|
|
|
471
|
-
| Option | Default | Meaning
|
|
472
|
-
| --------------------------------------- | ----------------------------------------------------------------------- |
|
|
473
|
-
| `issuer` | required | HTTPS URL; endpoints live under its path.
|
|
474
|
-
| `resource` | required | Canonical URL of your MCP endpoint, the audience of every token.
|
|
475
|
-
| `identityProvider` | required | `createOidcProvider(…)` or your own `IdentityProvider`.
|
|
476
|
-
| `store` | required | Your `OAuthStore`.
|
|
477
|
-
| `sealer` | required | `createAesGcmSealer(…)` or your own `SecretSealer`.
|
|
478
|
-
| `scopes` / `defaultScopes` | `["mcp"]` / all | Scopes offered, and those granted when none are asked for.
|
|
479
|
-
| `scopeDescriptions` | none | Consent-screen text per scope.
|
|
480
|
-
| `resourceName` | none | RFC 9728 `resource_name`.
|
|
481
|
-
| `clients` | none | Pre-registered public clients.
|
|
482
|
-
| `dynamicClientRegistration` | `true` | Serve `POST /register`. Deprecated by MCP 2026-07-28 in favour of metadata documents; `dynamicClientRegistration: false` turns it off.
|
|
483
|
-
| `clientIdMetadataDocuments` | `true` | Accept HTTPS URL client ids.
|
|
484
|
-
| `fetchClientMetadata(url, { signal })` | SSRF-safe fetcher | Replace how metadata documents are downloaded. Must stay SSRF-safe.
|
|
485
|
-
| `lifetimes` | see below | Token and flow lifetimes, in seconds.
|
|
486
|
-
| `limits` | see below | Sizes and counts.
|
|
487
|
-
| `
|
|
488
|
-
| `
|
|
489
|
-
| `
|
|
490
|
-
| `
|
|
491
|
-
| `
|
|
492
|
-
| `
|
|
493
|
-
| `
|
|
494
|
-
| `
|
|
495
|
-
| `
|
|
496
|
-
|
|
|
497
|
-
|
|
|
498
|
-
| `
|
|
488
|
+
| Option | Default | Meaning |
|
|
489
|
+
| --------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
490
|
+
| `issuer` | required | HTTPS URL; endpoints live under its path. |
|
|
491
|
+
| `resource` | required | Canonical URL of your MCP endpoint, the audience of every token. |
|
|
492
|
+
| `identityProvider` | required | `createOidcProvider(…)` or your own `IdentityProvider`. |
|
|
493
|
+
| `store` | required | Your `OAuthStore`. |
|
|
494
|
+
| `sealer` | required | `createAesGcmSealer(…)` or your own `SecretSealer`. |
|
|
495
|
+
| `scopes` / `defaultScopes` | `["mcp"]` / all | Scopes offered, and those granted when none are asked for. |
|
|
496
|
+
| `scopeDescriptions` | none | Consent-screen text per scope. |
|
|
497
|
+
| `resourceName` | none | RFC 9728 `resource_name`. |
|
|
498
|
+
| `clients` | none | Pre-registered public clients. |
|
|
499
|
+
| `dynamicClientRegistration` | `true` | Serve `POST /register`. Deprecated by MCP 2026-07-28 in favour of metadata documents; `dynamicClientRegistration: false` turns it off. |
|
|
500
|
+
| `clientIdMetadataDocuments` | `true` | Accept HTTPS URL client ids. |
|
|
501
|
+
| `fetchClientMetadata(url, { signal })` | SSRF-safe fetcher | Replace how metadata documents are downloaded. Must stay SSRF-safe. |
|
|
502
|
+
| `lifetimes` | see below | Token and flow lifetimes, in seconds. |
|
|
503
|
+
| `limits` | see below | Sizes and counts. |
|
|
504
|
+
| `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. |
|
|
505
|
+
| `refreshReuseGraceSeconds` | `10` | Retry window (0–300) for a just-used refresh token. `0` treats any reuse as replay. |
|
|
506
|
+
| `consentReplaySeconds` | `10` | Window (0–60) in which a repeated consent submission from the same browser gets the same redirect. |
|
|
507
|
+
| `scriptHashes` | none | `sha256-…` hashes of inline scripts a custom consent page may run. |
|
|
508
|
+
| `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. |
|
|
509
|
+
| `cookiePrefix` | `__Host-mcp-auth-` | Must start with `__Host-`. |
|
|
510
|
+
| `rateLimit(key, scope)` | none | Resolve `true` to continue; anything else (including a throw) answers 429. |
|
|
511
|
+
| `getClientIp(request)` / `trustProxy` | none / `false` | Rate-limit key. `trustProxy` uses the right-most `X-Forwarded-For` entry. |
|
|
512
|
+
| `renderConsentPage` / `renderErrorPage` | built in | Your own pages; see `ConsentPageContext`. |
|
|
513
|
+
| `onEvent(event)` | none | Typed audit events; never contain secrets. |
|
|
514
|
+
| policy and enrichment hooks | none | See [Extending safely](#extending-safely). |
|
|
515
|
+
| `allowInsecureLocalhost` | `false` | Allow an `http://localhost` issuer and resource in development. |
|
|
516
|
+
| `now` | `Date.now` | Clock, for tests. A non-finite value fails the request instead of disabling expiry checks. |
|
|
499
517
|
|
|
500
518
|
<details>
|
|
501
519
|
<summary><strong><code>lifetimes</code></strong> (seconds)</summary>
|
|
502
520
|
|
|
503
|
-
| Key | Default
|
|
504
|
-
| ----------------------- |
|
|
505
|
-
| `accessToken` | 1,800 (30 minutes)
|
|
506
|
-
| `refreshToken` | 1,209,600 (14 days, renewed on every rotation)
|
|
507
|
-
| `grant` | 7,776,000 (90 days, absolute; then consent again)
|
|
508
|
-
| `
|
|
509
|
-
| `
|
|
510
|
-
| `
|
|
511
|
-
| `
|
|
521
|
+
| Key | Default | Allowed |
|
|
522
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
|
|
523
|
+
| `accessToken` | 1,800 (30 minutes) | 1–86,400 |
|
|
524
|
+
| `refreshToken` | 1,209,600 (14 days, renewed on every rotation) | 1–34,560,000 |
|
|
525
|
+
| `grant` | 7,776,000 (90 days, absolute; then consent again) | 1–34,560,000 |
|
|
526
|
+
| `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 |
|
|
527
|
+
| `authorizationCode` | 60 | 1–600 |
|
|
528
|
+
| `pendingAuthorization` | 900 (sign-in plus consent) | 1–3,600 |
|
|
529
|
+
| `clientMetadataCache` | 1,800 (`0` disables the cache); a document's own `Cache-Control` shortens it | 0–86,400 |
|
|
530
|
+
| `clientMetadataFailure` | 60 (back-off for a failed metadata URL; `0` disables) | 0–3,600 |
|
|
512
531
|
|
|
513
532
|
</details>
|
|
514
533
|
|
|
@@ -555,12 +574,28 @@ evict the least recently used grant-less dynamic client first when full.
|
|
|
555
574
|
| `createMemoryStore(options?)` | A correct **single-process, non-persistent** store for development and tests. |
|
|
556
575
|
| `escapeHtml(text)` | The escaping the built-in pages use, for your own page renderers. |
|
|
557
576
|
|
|
577
|
+
### Errors
|
|
578
|
+
|
|
579
|
+
These classes reach your own code, for example from
|
|
580
|
+
`getUpstreamAccessToken()`; requests to the OAuth routes are answered with
|
|
581
|
+
OAuth errors or error pages instead. No error message contains a token.
|
|
582
|
+
|
|
583
|
+
| Class | Thrown when |
|
|
584
|
+
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
585
|
+
| `McpAuthConfigError` | `createMcpAuth` options are invalid, or `revokeGrantsForSubject` is called on a store without `listGrantsBySubject`. |
|
|
586
|
+
| `GrantInactiveError` | `getUpstreamAccessToken()` or `renewUpstream()` runs for a grant that was revoked or has expired. |
|
|
587
|
+
| `UpstreamTokenError` | No upstream access token could be obtained for a live grant. |
|
|
588
|
+
| `IdentityProviderError` | A call to the identity provider failed (discovery, code exchange, ID token checks, renewal). `code` is the provider's OAuth error, when it sent one. |
|
|
589
|
+
| `SealerError` | A sealed value is malformed, was altered, belongs to another owner, or uses a key id that is not configured. |
|
|
590
|
+
|
|
558
591
|
### Registered metadata is enforced
|
|
559
592
|
|
|
560
593
|
- **`grant_types`**: a client that omits it gets `authorization_code` only
|
|
561
594
|
(RFC 7591 § 2) and **no refresh token**. Clients that want refresh tokens
|
|
562
595
|
register `["authorization_code", "refresh_token"]`. Metadata documents
|
|
563
|
-
are treated the same way.
|
|
596
|
+
are treated the same way. Set `defaultGrantTypes:
|
|
597
|
+
["authorization_code", "refresh_token"]` to give such clients refresh
|
|
598
|
+
tokens anyway.
|
|
564
599
|
- **`scope`**: a client that registers a scope can only request scopes
|
|
565
600
|
within it. Unknown scopes are refused at registration and ignored in a
|
|
566
601
|
metadata document.
|
|
@@ -661,13 +696,36 @@ off, and concurrent requests for one URL share a download.
|
|
|
661
696
|
## Security
|
|
662
697
|
|
|
663
698
|
Report vulnerabilities privately, as described in [SECURITY.md](SECURITY.md).
|
|
664
|
-
|
|
665
|
-
|
|
699
|
+
Please do not open a public issue for a vulnerability.
|
|
700
|
+
|
|
701
|
+
Every release is built and published by GitHub Actions with
|
|
702
|
+
[npm provenance](https://docs.npmjs.com/generating-provenance-statements),
|
|
703
|
+
and a maintainer approves each one with a hardware security key. Verify what
|
|
704
|
+
you installed with:
|
|
705
|
+
|
|
706
|
+
```bash
|
|
707
|
+
npm audit signatures
|
|
708
|
+
```
|
|
666
709
|
|
|
667
710
|
## Contributing
|
|
668
711
|
|
|
669
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
712
|
+
Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
713
|
+
for the development setup and the checks a change must pass.
|
|
714
|
+
|
|
715
|
+
## About Gydr
|
|
716
|
+
|
|
717
|
+
`@gydr-ai/mcp-oauth` is built and maintained by [Gydr](https://gydr.ai), an
|
|
718
|
+
AI chatbot platform that answers customers on websites, WhatsApp, Messenger,
|
|
719
|
+
Instagram and Telegram. Businesses on Gydr manage their chatbots from Claude,
|
|
720
|
+
Codex and other MCP clients, and this package grew out of the authorization
|
|
721
|
+
server that makes those connections safe. See also
|
|
722
|
+
[`@gydr-ai/safe-fetch`](https://github.com/gydr-ai/safe-fetch), SSRF-safe
|
|
723
|
+
`fetch` for Node.js.
|
|
670
724
|
|
|
671
725
|
## License
|
|
672
726
|
|
|
673
|
-
[Apache-2.0](LICENSE) © Gydr.ai
|
|
727
|
+
[Apache-2.0](LICENSE) © [Gydr.ai](https://gydr.ai)
|
|
728
|
+
|
|
729
|
+
<p align="center">
|
|
730
|
+
<a href="https://gydr.ai"><img alt="Gydr" src="https://img.shields.io/badge/made_by-Gydr-0f172a?style=for-the-badge" /></a>
|
|
731
|
+
</p>
|