@dotdo/oauth 0.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/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # @dotdo/oauth
2
+
3
+ OAuth 2.1 authorization server for MCP (Model Context Protocol) compatibility.
4
+
5
+ ## The Problem
6
+
7
+ You're building an MCP server. Claude and ChatGPT need to authenticate. But OAuth 2.1 is new, the docs are scattered, and you've spent hours debugging `.well-known` endpoints and CORS headers.
8
+
9
+ ## The Solution
10
+
11
+ ```typescript
12
+ import { createOAuth21Server, MemoryOAuthStorage } from '@dotdo/oauth'
13
+
14
+ const oauth = createOAuth21Server({
15
+ issuer: 'https://your-mcp.do',
16
+ storage: new MemoryOAuthStorage(),
17
+ upstream: {
18
+ provider: 'workos',
19
+ apiKey: process.env.WORKOS_API_KEY,
20
+ clientId: process.env.WORKOS_CLIENT_ID,
21
+ },
22
+ })
23
+
24
+ // Mount it. Done.
25
+ app.route('/', oauth)
26
+ ```
27
+
28
+ Your MCP server now speaks OAuth 2.1. Claude can connect.
29
+
30
+ ## What This Does
31
+
32
+ This package creates a **federated OAuth 2.1 server**:
33
+
34
+ - It's an OAuth **SERVER** to MCP clients (Claude, ChatGPT)
35
+ - It's an OAuth **CLIENT** to your identity provider (WorkOS, Auth0)
36
+
37
+ ```
38
+ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────┐
39
+ │ Claude/ChatGPT │ ───> │ Your MCP Server │ ───> │ WorkOS │
40
+ │ (OAuth Client) │ │ (OAuth Server + │ │ (Upstream) │
41
+ │ │ <─── │ OAuth Client) │ <─── │ │
42
+ └─────────────────┘ └──────────────────┘ └─────────────┘
43
+ ```
44
+
45
+ ## Endpoints
46
+
47
+ Once mounted, your server provides:
48
+
49
+ | Endpoint | Purpose |
50
+ |----------|---------|
51
+ | `/.well-known/oauth-authorization-server` | Server metadata (RFC 8414) |
52
+ | `/.well-known/oauth-protected-resource` | Resource metadata |
53
+ | `/authorize` | Authorization endpoint |
54
+ | `/token` | Token endpoint |
55
+ | `/register` | Dynamic client registration |
56
+ | `/revoke` | Token revocation |
57
+
58
+ ## Features
59
+
60
+ - **OAuth 2.1 compliant** - PKCE required, S256 only
61
+ - **MCP compatible** - Works with Claude, ChatGPT, and other MCP clients
62
+ - **Federated auth** - Delegates to WorkOS, Auth0, or custom providers
63
+ - **Storage agnostic** - Bring your own storage backend
64
+ - **Zero dependencies** on `oauth.do` or `@dotdo/do` - this is the leaf package
65
+
66
+ ## Storage
67
+
68
+ The package provides `MemoryOAuthStorage` for testing. For production, implement the `OAuthStorage` interface:
69
+
70
+ ```typescript
71
+ import type { OAuthStorage } from '@dotdo/oauth'
72
+
73
+ class MyStorage implements OAuthStorage {
74
+ async getUser(id: string) { /* ... */ }
75
+ async saveUser(user: OAuthUser) { /* ... */ }
76
+ async getClient(clientId: string) { /* ... */ }
77
+ // ... other methods
78
+ }
79
+ ```
80
+
81
+ If you're using `@dotdo/do`, it provides `DOAuthStorage` that implements this interface using Durable Object SQLite storage.
82
+
83
+ ## Configuration
84
+
85
+ ```typescript
86
+ createOAuth21Server({
87
+ // Required
88
+ issuer: 'https://your-domain.com', // Your server's URL
89
+ storage: new MemoryOAuthStorage(), // Storage backend
90
+ upstream: { // Identity provider
91
+ provider: 'workos',
92
+ apiKey: 'sk_...',
93
+ clientId: 'client_...',
94
+ },
95
+
96
+ // Optional
97
+ scopes: ['openid', 'profile', 'email'], // Supported scopes
98
+ accessTokenTtl: 3600, // 1 hour (default)
99
+ refreshTokenTtl: 2592000, // 30 days (default)
100
+ authCodeTtl: 600, // 10 minutes (default)
101
+ enableDynamicRegistration: true, // Allow client registration
102
+ debug: false, // Debug logging
103
+
104
+ // Callbacks
105
+ onUserAuthenticated: async (user) => {
106
+ console.log('User logged in:', user.email)
107
+ },
108
+ })
109
+ ```
110
+
111
+ ## Upstream Providers
112
+
113
+ ### WorkOS
114
+
115
+ ```typescript
116
+ upstream: {
117
+ provider: 'workos',
118
+ apiKey: process.env.WORKOS_API_KEY,
119
+ clientId: process.env.WORKOS_CLIENT_ID,
120
+ }
121
+ ```
122
+
123
+ ### Custom Provider
124
+
125
+ ```typescript
126
+ upstream: {
127
+ provider: 'custom',
128
+ apiKey: 'your-client-secret',
129
+ clientId: 'your-client-id',
130
+ authorizationEndpoint: 'https://auth.example.com/authorize',
131
+ tokenEndpoint: 'https://auth.example.com/token',
132
+ }
133
+ ```
134
+
135
+ ## PKCE Utilities
136
+
137
+ The package also exports PKCE utilities:
138
+
139
+ ```typescript
140
+ import { generatePkce, verifyCodeChallenge } from '@dotdo/oauth/pkce'
141
+
142
+ // Generate PKCE pair
143
+ const { verifier, challenge } = await generatePkce()
144
+
145
+ // Verify during token exchange
146
+ const valid = await verifyCodeChallenge(verifier, challenge, 'S256')
147
+ ```
148
+
149
+ ## Architecture
150
+
151
+ This package is the **leaf** in the dependency chain:
152
+
153
+ ```
154
+ @dotdo/oauth (this package - pure OAuth 2.1, storage interface)
155
+
156
+ @dotdo/do (depends on @dotdo/oauth, implements DOAuthStorage)
157
+
158
+ oauth.do (depends on @dotdo/do for storage)
159
+
160
+ dotdo (depends on oauth.do for auth)
161
+ ```
162
+
163
+ Each layer adds capabilities:
164
+ - **@dotdo/oauth** - OAuth 2.1 primitives, abstract storage interface
165
+ - **@dotdo/do** - Concrete storage using Durable Object SQLite
166
+ - **oauth.do** - High-level auth SDK with session management
167
+ - **dotdo** - Full framework with auth built-in
168
+
169
+ ## License
170
+
171
+ MIT
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @dotdo/oauth - OAuth 2.1 Server for MCP
3
+ *
4
+ * A minimal OAuth 2.1 authorization server implementation designed for
5
+ * Model Context Protocol (MCP) compatibility with Claude, ChatGPT, and other AI clients.
6
+ *
7
+ * This package is the "leaf" in the dependency tree - it has no dependencies on
8
+ * oauth.do or @dotdo/do, allowing them to depend on it without circular dependencies.
9
+ *
10
+ * @example Basic usage
11
+ * ```typescript
12
+ * import { createOAuth21Server, MemoryOAuthStorage } from '@dotdo/oauth'
13
+ *
14
+ * const server = createOAuth21Server({
15
+ * issuer: 'https://mcp.do',
16
+ * storage: new MemoryOAuthStorage(),
17
+ * upstream: {
18
+ * provider: 'workos',
19
+ * apiKey: env.WORKOS_API_KEY,
20
+ * clientId: env.WORKOS_CLIENT_ID,
21
+ * },
22
+ * })
23
+ *
24
+ * // Mount on Hono app
25
+ * app.route('/', server)
26
+ * ```
27
+ *
28
+ * @example With DO storage (provided by @dotdo/do)
29
+ * ```typescript
30
+ * import { createOAuth21Server } from '@dotdo/oauth'
31
+ * import { DOAuthStorage } from '@dotdo/do/oauth'
32
+ *
33
+ * const server = createOAuth21Server({
34
+ * issuer: 'https://mcp.do',
35
+ * storage: new DOAuthStorage(digitalObject),
36
+ * upstream: { ... },
37
+ * })
38
+ * ```
39
+ *
40
+ * @packageDocumentation
41
+ */
42
+ export { createOAuth21Server } from './server';
43
+ export type { OAuth21ServerConfig } from './server';
44
+ export { MemoryOAuthStorage } from './storage';
45
+ export type { OAuthStorage, ListOptions } from './storage';
46
+ export { generateCodeVerifier, generateCodeChallenge, verifyCodeChallenge, generatePkce, generateState, generateToken, generateAuthorizationCode, hashClientSecret, verifyClientSecret, base64UrlEncode, base64UrlDecode, constantTimeEqual, } from './pkce';
47
+ export type { OAuthUser, OAuthOrganization, OAuthClient, OAuthAuthorizationCode, OAuthAccessToken, OAuthRefreshToken, OAuthGrant, OAuthServerMetadata, OAuthResourceMetadata, TokenResponse, OAuthError, UpstreamOAuthConfig, } from './types';
48
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAC9C,YAAY,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAGnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAC9C,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAG1D,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,aAAa,EACb,yBAAyB,EACzB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,eAAe,EACf,iBAAiB,GAClB,MAAM,QAAQ,CAAA;AAGf,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,WAAW,EACX,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,EACV,mBAAmB,EACnB,qBAAqB,EACrB,aAAa,EACb,UAAU,EACV,mBAAmB,GACpB,MAAM,SAAS,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @dotdo/oauth - OAuth 2.1 Server for MCP
3
+ *
4
+ * A minimal OAuth 2.1 authorization server implementation designed for
5
+ * Model Context Protocol (MCP) compatibility with Claude, ChatGPT, and other AI clients.
6
+ *
7
+ * This package is the "leaf" in the dependency tree - it has no dependencies on
8
+ * oauth.do or @dotdo/do, allowing them to depend on it without circular dependencies.
9
+ *
10
+ * @example Basic usage
11
+ * ```typescript
12
+ * import { createOAuth21Server, MemoryOAuthStorage } from '@dotdo/oauth'
13
+ *
14
+ * const server = createOAuth21Server({
15
+ * issuer: 'https://mcp.do',
16
+ * storage: new MemoryOAuthStorage(),
17
+ * upstream: {
18
+ * provider: 'workos',
19
+ * apiKey: env.WORKOS_API_KEY,
20
+ * clientId: env.WORKOS_CLIENT_ID,
21
+ * },
22
+ * })
23
+ *
24
+ * // Mount on Hono app
25
+ * app.route('/', server)
26
+ * ```
27
+ *
28
+ * @example With DO storage (provided by @dotdo/do)
29
+ * ```typescript
30
+ * import { createOAuth21Server } from '@dotdo/oauth'
31
+ * import { DOAuthStorage } from '@dotdo/do/oauth'
32
+ *
33
+ * const server = createOAuth21Server({
34
+ * issuer: 'https://mcp.do',
35
+ * storage: new DOAuthStorage(digitalObject),
36
+ * upstream: { ... },
37
+ * })
38
+ * ```
39
+ *
40
+ * @packageDocumentation
41
+ */
42
+ // Server
43
+ export { createOAuth21Server } from './server';
44
+ // Storage
45
+ export { MemoryOAuthStorage } from './storage';
46
+ // PKCE
47
+ export { generateCodeVerifier, generateCodeChallenge, verifyCodeChallenge, generatePkce, generateState, generateToken, generateAuthorizationCode, hashClientSecret, verifyClientSecret, base64UrlEncode, base64UrlDecode, constantTimeEqual, } from './pkce';
48
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,SAAS;AACT,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAG9C,UAAU;AACV,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAG9C,OAAO;AACP,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,aAAa,EACb,yBAAyB,EACzB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,eAAe,EACf,iBAAiB,GAClB,MAAM,QAAQ,CAAA"}
package/dist/pkce.d.ts ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @dotdo/oauth - PKCE (Proof Key for Code Exchange) utilities
3
+ *
4
+ * OAuth 2.1 requires PKCE for all authorization code flows.
5
+ * Only S256 is supported (plain is deprecated in OAuth 2.1).
6
+ */
7
+ /**
8
+ * Generate a cryptographically random code verifier
9
+ *
10
+ * Per RFC 7636, the verifier must be:
11
+ * - Between 43 and 128 characters long
12
+ * - Using only unreserved URI characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
13
+ *
14
+ * @param length - Length of the verifier (default: 64)
15
+ * @returns Random code verifier string
16
+ */
17
+ export declare function generateCodeVerifier(length?: number): string;
18
+ /**
19
+ * Generate a code challenge from a code verifier using S256 method
20
+ *
21
+ * S256: BASE64URL(SHA256(code_verifier))
22
+ *
23
+ * @param verifier - The code verifier
24
+ * @returns Base64URL-encoded SHA-256 hash of the verifier
25
+ */
26
+ export declare function generateCodeChallenge(verifier: string): Promise<string>;
27
+ /**
28
+ * Verify a code verifier against a code challenge
29
+ *
30
+ * @param verifier - The code verifier from the token request
31
+ * @param challenge - The code challenge from the authorization request
32
+ * @param method - The challenge method (must be 'S256' for OAuth 2.1)
33
+ * @returns True if the verifier matches the challenge
34
+ */
35
+ export declare function verifyCodeChallenge(verifier: string, challenge: string, method?: string): Promise<boolean>;
36
+ /**
37
+ * Generate a PKCE pair (verifier and challenge)
38
+ *
39
+ * @param length - Length of the verifier (default: 64)
40
+ * @returns Object with verifier and challenge
41
+ */
42
+ export declare function generatePkce(length?: number): Promise<{
43
+ verifier: string;
44
+ challenge: string;
45
+ }>;
46
+ /**
47
+ * Base64URL encode an ArrayBuffer
48
+ *
49
+ * @param buffer - The buffer to encode
50
+ * @returns Base64URL-encoded string (no padding)
51
+ */
52
+ export declare function base64UrlEncode(buffer: ArrayBuffer): string;
53
+ /**
54
+ * Base64URL decode a string to ArrayBuffer
55
+ *
56
+ * @param str - Base64URL-encoded string
57
+ * @returns Decoded ArrayBuffer
58
+ */
59
+ export declare function base64UrlDecode(str: string): ArrayBuffer;
60
+ /**
61
+ * Constant-time string comparison to prevent timing attacks
62
+ *
63
+ * @param a - First string
64
+ * @param b - Second string
65
+ * @returns True if strings are equal
66
+ */
67
+ export declare function constantTimeEqual(a: string, b: string): boolean;
68
+ /**
69
+ * Generate a random state parameter for CSRF protection
70
+ *
71
+ * @param length - Length of the state (default: 32)
72
+ * @returns Random state string
73
+ */
74
+ export declare function generateState(length?: number): string;
75
+ /**
76
+ * Generate a random token (for access tokens, refresh tokens, etc.)
77
+ *
78
+ * @param length - Length of the token (default: 32)
79
+ * @returns Random token string
80
+ */
81
+ export declare function generateToken(length?: number): string;
82
+ /**
83
+ * Generate a unique authorization code
84
+ *
85
+ * @returns Random authorization code
86
+ */
87
+ export declare function generateAuthorizationCode(): string;
88
+ /**
89
+ * Hash a client secret for storage
90
+ *
91
+ * @param secret - The client secret to hash
92
+ * @returns SHA-256 hash of the secret
93
+ */
94
+ export declare function hashClientSecret(secret: string): Promise<string>;
95
+ /**
96
+ * Verify a client secret against a hash
97
+ *
98
+ * @param secret - The client secret to verify
99
+ * @param hash - The stored hash
100
+ * @returns True if the secret matches the hash
101
+ */
102
+ export declare function verifyClientSecret(secret: string, hash: string): Promise<boolean>;
103
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,GAAE,MAAW,GAAG,MAAM,CAehE;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK7E;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE,MAAe,GACtB,OAAO,CAAC,OAAO,CAAC,CAQlB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,MAAM,GAAE,MAAW,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAIxG;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAU3D;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAWxD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAW/D;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,GAAE,MAAW,GAAG,MAAM,CAWzD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,GAAE,MAAW,GAAG,MAAM,CAWzD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKtE;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAGvF"}
package/dist/pkce.js ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * @dotdo/oauth - PKCE (Proof Key for Code Exchange) utilities
3
+ *
4
+ * OAuth 2.1 requires PKCE for all authorization code flows.
5
+ * Only S256 is supported (plain is deprecated in OAuth 2.1).
6
+ */
7
+ /**
8
+ * Generate a cryptographically random code verifier
9
+ *
10
+ * Per RFC 7636, the verifier must be:
11
+ * - Between 43 and 128 characters long
12
+ * - Using only unreserved URI characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
13
+ *
14
+ * @param length - Length of the verifier (default: 64)
15
+ * @returns Random code verifier string
16
+ */
17
+ export function generateCodeVerifier(length = 64) {
18
+ if (length < 43 || length > 128) {
19
+ throw new Error('Code verifier length must be between 43 and 128 characters');
20
+ }
21
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
22
+ const randomValues = new Uint8Array(length);
23
+ crypto.getRandomValues(randomValues);
24
+ let verifier = '';
25
+ for (let i = 0; i < length; i++) {
26
+ verifier += chars[randomValues[i] % chars.length];
27
+ }
28
+ return verifier;
29
+ }
30
+ /**
31
+ * Generate a code challenge from a code verifier using S256 method
32
+ *
33
+ * S256: BASE64URL(SHA256(code_verifier))
34
+ *
35
+ * @param verifier - The code verifier
36
+ * @returns Base64URL-encoded SHA-256 hash of the verifier
37
+ */
38
+ export async function generateCodeChallenge(verifier) {
39
+ const encoder = new TextEncoder();
40
+ const data = encoder.encode(verifier);
41
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
42
+ return base64UrlEncode(hashBuffer);
43
+ }
44
+ /**
45
+ * Verify a code verifier against a code challenge
46
+ *
47
+ * @param verifier - The code verifier from the token request
48
+ * @param challenge - The code challenge from the authorization request
49
+ * @param method - The challenge method (must be 'S256' for OAuth 2.1)
50
+ * @returns True if the verifier matches the challenge
51
+ */
52
+ export async function verifyCodeChallenge(verifier, challenge, method = 'S256') {
53
+ if (method !== 'S256') {
54
+ // OAuth 2.1 only supports S256
55
+ return false;
56
+ }
57
+ const expectedChallenge = await generateCodeChallenge(verifier);
58
+ return constantTimeEqual(expectedChallenge, challenge);
59
+ }
60
+ /**
61
+ * Generate a PKCE pair (verifier and challenge)
62
+ *
63
+ * @param length - Length of the verifier (default: 64)
64
+ * @returns Object with verifier and challenge
65
+ */
66
+ export async function generatePkce(length = 64) {
67
+ const verifier = generateCodeVerifier(length);
68
+ const challenge = await generateCodeChallenge(verifier);
69
+ return { verifier, challenge };
70
+ }
71
+ /**
72
+ * Base64URL encode an ArrayBuffer
73
+ *
74
+ * @param buffer - The buffer to encode
75
+ * @returns Base64URL-encoded string (no padding)
76
+ */
77
+ export function base64UrlEncode(buffer) {
78
+ const bytes = new Uint8Array(buffer);
79
+ let binary = '';
80
+ for (let i = 0; i < bytes.length; i++) {
81
+ binary += String.fromCharCode(bytes[i]);
82
+ }
83
+ return btoa(binary)
84
+ .replace(/\+/g, '-')
85
+ .replace(/\//g, '_')
86
+ .replace(/=+$/, '');
87
+ }
88
+ /**
89
+ * Base64URL decode a string to ArrayBuffer
90
+ *
91
+ * @param str - Base64URL-encoded string
92
+ * @returns Decoded ArrayBuffer
93
+ */
94
+ export function base64UrlDecode(str) {
95
+ // Add padding if needed
96
+ const padded = str + '='.repeat((4 - (str.length % 4)) % 4);
97
+ // Convert from base64url to base64
98
+ const base64 = padded.replace(/-/g, '+').replace(/_/g, '/');
99
+ const binary = atob(base64);
100
+ const bytes = new Uint8Array(binary.length);
101
+ for (let i = 0; i < binary.length; i++) {
102
+ bytes[i] = binary.charCodeAt(i);
103
+ }
104
+ return bytes.buffer;
105
+ }
106
+ /**
107
+ * Constant-time string comparison to prevent timing attacks
108
+ *
109
+ * @param a - First string
110
+ * @param b - Second string
111
+ * @returns True if strings are equal
112
+ */
113
+ export function constantTimeEqual(a, b) {
114
+ if (a.length !== b.length) {
115
+ return false;
116
+ }
117
+ let result = 0;
118
+ for (let i = 0; i < a.length; i++) {
119
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
120
+ }
121
+ return result === 0;
122
+ }
123
+ /**
124
+ * Generate a random state parameter for CSRF protection
125
+ *
126
+ * @param length - Length of the state (default: 32)
127
+ * @returns Random state string
128
+ */
129
+ export function generateState(length = 32) {
130
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
131
+ const randomValues = new Uint8Array(length);
132
+ crypto.getRandomValues(randomValues);
133
+ let state = '';
134
+ for (let i = 0; i < length; i++) {
135
+ state += chars[randomValues[i] % chars.length];
136
+ }
137
+ return state;
138
+ }
139
+ /**
140
+ * Generate a random token (for access tokens, refresh tokens, etc.)
141
+ *
142
+ * @param length - Length of the token (default: 32)
143
+ * @returns Random token string
144
+ */
145
+ export function generateToken(length = 32) {
146
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
147
+ const randomValues = new Uint8Array(length);
148
+ crypto.getRandomValues(randomValues);
149
+ let token = '';
150
+ for (let i = 0; i < length; i++) {
151
+ token += chars[randomValues[i] % chars.length];
152
+ }
153
+ return token;
154
+ }
155
+ /**
156
+ * Generate a unique authorization code
157
+ *
158
+ * @returns Random authorization code
159
+ */
160
+ export function generateAuthorizationCode() {
161
+ return generateToken(48);
162
+ }
163
+ /**
164
+ * Hash a client secret for storage
165
+ *
166
+ * @param secret - The client secret to hash
167
+ * @returns SHA-256 hash of the secret
168
+ */
169
+ export async function hashClientSecret(secret) {
170
+ const encoder = new TextEncoder();
171
+ const data = encoder.encode(secret);
172
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
173
+ return base64UrlEncode(hashBuffer);
174
+ }
175
+ /**
176
+ * Verify a client secret against a hash
177
+ *
178
+ * @param secret - The client secret to verify
179
+ * @param hash - The stored hash
180
+ * @returns True if the secret matches the hash
181
+ */
182
+ export async function verifyClientSecret(secret, hash) {
183
+ const expectedHash = await hashClientSecret(secret);
184
+ return constantTimeEqual(expectedHash, hash);
185
+ }
186
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @dotdo/oauth - OAuth 2.1 Server Implementation
3
+ *
4
+ * Creates a Hono app that implements OAuth 2.1 authorization server endpoints:
5
+ * - /.well-known/oauth-authorization-server (RFC 8414)
6
+ * - /.well-known/oauth-protected-resource (draft-ietf-oauth-resource-metadata)
7
+ * - /authorize (authorization endpoint)
8
+ * - /callback (upstream OAuth callback)
9
+ * - /token (token endpoint)
10
+ * - /register (dynamic client registration - RFC 7591)
11
+ * - /revoke (token revocation - RFC 7009)
12
+ *
13
+ * This server acts as a federated OAuth 2.1 server:
14
+ * - It is an OAuth SERVER to downstream clients (Claude, ChatGPT, etc.)
15
+ * - It is an OAuth CLIENT to upstream providers (WorkOS, Auth0, etc.)
16
+ */
17
+ import { Hono } from 'hono';
18
+ import type { OAuthStorage } from './storage';
19
+ import type { OAuthUser, UpstreamOAuthConfig } from './types';
20
+ /**
21
+ * Configuration for the OAuth 2.1 server
22
+ */
23
+ export interface OAuth21ServerConfig {
24
+ /** Server issuer URL (e.g., https://mcp.do) */
25
+ issuer: string;
26
+ /** Storage backend for users, clients, tokens */
27
+ storage: OAuthStorage;
28
+ /** Upstream OAuth provider configuration */
29
+ upstream: UpstreamOAuthConfig;
30
+ /** Supported scopes */
31
+ scopes?: string[];
32
+ /** Access token lifetime in seconds (default: 3600) */
33
+ accessTokenTtl?: number;
34
+ /** Refresh token lifetime in seconds (default: 2592000 = 30 days) */
35
+ refreshTokenTtl?: number;
36
+ /** Authorization code lifetime in seconds (default: 600 = 10 minutes) */
37
+ authCodeTtl?: number;
38
+ /** Enable dynamic client registration */
39
+ enableDynamicRegistration?: boolean;
40
+ /** Callback after successful user authentication */
41
+ onUserAuthenticated?: (user: OAuthUser) => void | Promise<void>;
42
+ /** Enable debug logging */
43
+ debug?: boolean;
44
+ }
45
+ /**
46
+ * Create an OAuth 2.1 server as a Hono app
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * import { createOAuth21Server, MemoryOAuthStorage } from '@dotdo/oauth'
51
+ *
52
+ * const oauthServer = createOAuth21Server({
53
+ * issuer: 'https://mcp.do',
54
+ * storage: new MemoryOAuthStorage(),
55
+ * upstream: {
56
+ * provider: 'workos',
57
+ * apiKey: env.WORKOS_API_KEY,
58
+ * clientId: env.WORKOS_CLIENT_ID,
59
+ * },
60
+ * })
61
+ *
62
+ * // Mount on your main app
63
+ * app.route('/', oauthServer)
64
+ * ```
65
+ */
66
+ export declare function createOAuth21Server(config: OAuth21ServerConfig): Hono;
67
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAC7C,OAAO,KAAK,EAIV,SAAS,EAGT,mBAAmB,EACpB,MAAM,SAAS,CAAA;AAShB;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAA;IACd,iDAAiD;IACjD,OAAO,EAAE,YAAY,CAAA;IACrB,4CAA4C;IAC5C,QAAQ,EAAE,mBAAmB,CAAA;IAC7B,uBAAuB;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,uDAAuD;IACvD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,yCAAyC;IACzC,yBAAyB,CAAC,EAAE,OAAO,CAAA;IACnC,oDAAoD;IACpD,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAuXrE"}