@mcp-z/oauth-microsoft 1.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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/dist/cjs/index.d.cts +16 -0
  4. package/dist/cjs/index.d.ts +16 -0
  5. package/dist/cjs/index.js +112 -0
  6. package/dist/cjs/index.js.map +1 -0
  7. package/dist/cjs/lib/dcr-router.d.cts +44 -0
  8. package/dist/cjs/lib/dcr-router.d.ts +44 -0
  9. package/dist/cjs/lib/dcr-router.js +1227 -0
  10. package/dist/cjs/lib/dcr-router.js.map +1 -0
  11. package/dist/cjs/lib/dcr-utils.d.cts +160 -0
  12. package/dist/cjs/lib/dcr-utils.d.ts +160 -0
  13. package/dist/cjs/lib/dcr-utils.js +860 -0
  14. package/dist/cjs/lib/dcr-utils.js.map +1 -0
  15. package/dist/cjs/lib/dcr-verify.d.cts +53 -0
  16. package/dist/cjs/lib/dcr-verify.d.ts +53 -0
  17. package/dist/cjs/lib/dcr-verify.js +193 -0
  18. package/dist/cjs/lib/dcr-verify.js.map +1 -0
  19. package/dist/cjs/lib/fetch-with-timeout.d.cts +14 -0
  20. package/dist/cjs/lib/fetch-with-timeout.d.ts +14 -0
  21. package/dist/cjs/lib/fetch-with-timeout.js +257 -0
  22. package/dist/cjs/lib/fetch-with-timeout.js.map +1 -0
  23. package/dist/cjs/lib/token-verifier.d.cts +44 -0
  24. package/dist/cjs/lib/token-verifier.d.ts +44 -0
  25. package/dist/cjs/lib/token-verifier.js +253 -0
  26. package/dist/cjs/lib/token-verifier.js.map +1 -0
  27. package/dist/cjs/package.json +1 -0
  28. package/dist/cjs/providers/dcr.d.cts +110 -0
  29. package/dist/cjs/providers/dcr.d.ts +110 -0
  30. package/dist/cjs/providers/dcr.js +600 -0
  31. package/dist/cjs/providers/dcr.js.map +1 -0
  32. package/dist/cjs/providers/device-code.d.cts +179 -0
  33. package/dist/cjs/providers/device-code.d.ts +179 -0
  34. package/dist/cjs/providers/device-code.js +896 -0
  35. package/dist/cjs/providers/device-code.js.map +1 -0
  36. package/dist/cjs/providers/loopback-oauth.d.cts +125 -0
  37. package/dist/cjs/providers/loopback-oauth.d.ts +125 -0
  38. package/dist/cjs/providers/loopback-oauth.js +1325 -0
  39. package/dist/cjs/providers/loopback-oauth.js.map +1 -0
  40. package/dist/cjs/schemas/index.d.cts +20 -0
  41. package/dist/cjs/schemas/index.d.ts +20 -0
  42. package/dist/cjs/schemas/index.js +37 -0
  43. package/dist/cjs/schemas/index.js.map +1 -0
  44. package/dist/cjs/setup/config.d.cts +113 -0
  45. package/dist/cjs/setup/config.d.ts +113 -0
  46. package/dist/cjs/setup/config.js +246 -0
  47. package/dist/cjs/setup/config.js.map +1 -0
  48. package/dist/cjs/types.d.cts +188 -0
  49. package/dist/cjs/types.d.ts +188 -0
  50. package/dist/cjs/types.js +18 -0
  51. package/dist/cjs/types.js.map +1 -0
  52. package/dist/esm/index.d.ts +16 -0
  53. package/dist/esm/index.js +16 -0
  54. package/dist/esm/index.js.map +1 -0
  55. package/dist/esm/lib/dcr-router.d.ts +44 -0
  56. package/dist/esm/lib/dcr-router.js +556 -0
  57. package/dist/esm/lib/dcr-router.js.map +1 -0
  58. package/dist/esm/lib/dcr-utils.d.ts +160 -0
  59. package/dist/esm/lib/dcr-utils.js +270 -0
  60. package/dist/esm/lib/dcr-utils.js.map +1 -0
  61. package/dist/esm/lib/dcr-verify.d.ts +53 -0
  62. package/dist/esm/lib/dcr-verify.js +53 -0
  63. package/dist/esm/lib/dcr-verify.js.map +1 -0
  64. package/dist/esm/lib/fetch-with-timeout.d.ts +14 -0
  65. package/dist/esm/lib/fetch-with-timeout.js +30 -0
  66. package/dist/esm/lib/fetch-with-timeout.js.map +1 -0
  67. package/dist/esm/lib/token-verifier.d.ts +44 -0
  68. package/dist/esm/lib/token-verifier.js +53 -0
  69. package/dist/esm/lib/token-verifier.js.map +1 -0
  70. package/dist/esm/package.json +1 -0
  71. package/dist/esm/providers/dcr.d.ts +110 -0
  72. package/dist/esm/providers/dcr.js +235 -0
  73. package/dist/esm/providers/dcr.js.map +1 -0
  74. package/dist/esm/providers/device-code.d.ts +179 -0
  75. package/dist/esm/providers/device-code.js +417 -0
  76. package/dist/esm/providers/device-code.js.map +1 -0
  77. package/dist/esm/providers/loopback-oauth.d.ts +125 -0
  78. package/dist/esm/providers/loopback-oauth.js +643 -0
  79. package/dist/esm/providers/loopback-oauth.js.map +1 -0
  80. package/dist/esm/schemas/index.d.ts +20 -0
  81. package/dist/esm/schemas/index.js +18 -0
  82. package/dist/esm/schemas/index.js.map +1 -0
  83. package/dist/esm/setup/config.d.ts +113 -0
  84. package/dist/esm/setup/config.js +268 -0
  85. package/dist/esm/setup/config.js.map +1 -0
  86. package/dist/esm/types.d.ts +188 -0
  87. package/dist/esm/types.js +8 -0
  88. package/dist/esm/types.js.map +1 -0
  89. package/package.json +87 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/Projects/ai/mcp-z/oauth/oauth-microsoft/src/types.ts"],"sourcesContent":["/**\n * Standalone types for Microsoft OAuth\n * No dependencies on other @mcp-z packages except @mcp-z/oauth\n */\n\n// Import shared types from base @mcp-z/oauth package\n// Public types (will be re-exported)\n// Internal-only types (not re-exported, used by providers)\nimport type { AuthFlowDescriptor, CachedToken, DcrClientInformation, DcrClientMetadata, Logger, OAuth2TokenStorageProvider, ProviderTokens, ToolHandler, ToolModule, UserAuthProvider } from '@mcp-z/oauth';\nimport type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';\nimport type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types.js';\nimport type { Keyv } from 'keyv';\n\n// Re-export only essential shared types for public API\nexport type { Logger, CachedToken, ToolModule, ProviderTokens, DcrClientMetadata, DcrClientInformation };\n\n// Re-export error class\nexport { AuthRequiredError } from '@mcp-z/oauth';\n\n// Make internal types available for internal use without exporting\nexport type { ToolHandler, AuthFlowDescriptor, OAuth2TokenStorageProvider, UserAuthProvider, RequestHandlerExtra, ServerRequest, ServerNotification };\n\n// ============================================================================\n// Core Authentication Types\n// ============================================================================\n\n/**\n * Microsoft service types that support OAuth\n * OAuth clients support all Microsoft services provided by Microsoft Graph\n * @public\n */\nexport type MicrosoftService = string;\n\n// ============================================================================\n// Configuration Types\n// ============================================================================\n\n/**\n * OAuth client configuration for upstream provider\n * @public\n */\nexport interface OAuthClientConfig {\n /** OAuth client ID for upstream provider */\n clientId: string;\n /** OAuth client secret (optional for some flows) */\n clientSecret?: string;\n /** Tenant/directory ID (for multi-tenant providers) */\n tenantId?: string;\n}\n\n/**\n * Microsoft OAuth configuration interface.\n * Contains all OAuth-related configuration from CLI arguments and environment variables.\n * @public\n */\nexport interface OAuthConfig {\n /** OAuth client ID */\n clientId: string;\n /** OAuth client secret (optional for public clients) */\n clientSecret?: string;\n /** Azure AD tenant ID */\n tenantId: string;\n /** OAuth adapter mode */\n auth: 'loopback-oauth' | 'device-code' | 'dcr';\n /** Whether to run in headless mode (no browser interaction) */\n headless: boolean;\n /** Optional redirect URI override (defaults to ephemeral loopback) */\n redirectUri?: string;\n}\n\n/**\n * DCR configuration for dynamic client registration\n * @public\n */\nexport interface DcrConfig {\n /** DCR mode: self-hosted (runs own OAuth server) or external (uses Auth0/Stitch) */\n mode: 'self-hosted' | 'external';\n /** External verification endpoint URL (required for external mode) */\n verifyUrl?: string;\n /** DCR client storage URI (required for self-hosted mode) */\n storeUri?: string;\n /** OAuth client ID for Microsoft Graph */\n clientId: string;\n /** OAuth client secret (optional for public clients) */\n clientSecret?: string;\n /** Azure AD tenant ID */\n tenantId: string;\n /** OAuth scopes to request */\n scope: string;\n /** Logger instance */\n logger?: Logger;\n}\n\n/**\n * Configuration for loopback OAuth client\n * @public\n */\nexport interface LoopbackOAuthConfig {\n /** Microsoft service type (e.g., 'outlook') */\n service: MicrosoftService;\n /** OAuth client ID */\n clientId: string;\n /** OAuth client secret (optional for public clients) */\n clientSecret?: string | undefined;\n /** Azure AD tenant ID */\n tenantId: string;\n /** OAuth scopes to request */\n scope: string;\n /** Whether to run in headless mode (no browser interaction) */\n headless: boolean;\n /** Logger instance */\n logger: Logger;\n /** Token storage */\n tokenStore: Keyv<unknown>;\n /** Optional redirect URI override (defaults to ephemeral loopback) */\n redirectUri?: string;\n}\n\n// ============================================================================\n// Middleware Types\n// ============================================================================\n\n/**\n * Microsoft Graph AuthenticationProvider interface\n * Used by Microsoft Graph SDK for API authentication\n * @public\n */\nexport interface MicrosoftAuthProvider {\n getAccessToken: () => Promise<string>;\n}\n\n/**\n * Auth context injected into extra by middleware\n * @public\n */\nexport interface AuthContext {\n /**\n * Microsoft Graph AuthenticationProvider ready for Graph SDK\n * GUARANTEED to exist when handler runs\n */\n auth: MicrosoftAuthProvider;\n\n /**\n * Account being used (for logging, debugging)\n */\n accountId: string;\n\n /**\n * User ID (multi-tenant only)\n */\n}\n\n/**\n * Enriched extra with guaranteed auth context and logger\n * Handlers receive this type - never plain RequestHandlerExtra\n * @public\n */\nexport interface EnrichedExtra extends RequestHandlerExtra<ServerRequest, ServerNotification> {\n /**\n * Auth context injected by middleware\n * GUARANTEED to exist (middleware catches auth failures)\n */\n authContext: AuthContext;\n\n /**\n * Logger injected by middleware\n * GUARANTEED to exist\n */\n logger: Logger;\n\n /**\n * HTTP request object (for HTTP transport scenarios)\n * Optional - present when using HTTP transport with JWT/session auth\n */\n req?: unknown;\n\n // Preserve backchannel support\n _meta?: {\n accountId?: string;\n [key: string]: unknown;\n };\n}\n\n// ============================================================================\n// DCR Internal Types\n// ============================================================================\n\n/**\n * Registered client with full metadata\n * Extends DcrClientInformation with internal timestamps\n * @internal\n */\nexport interface RegisteredClient extends DcrClientInformation {\n /** Creation timestamp (milliseconds since epoch) */\n created_at: number;\n}\n\n/**\n * Authorization code data structure\n * @public\n */\nexport interface AuthorizationCode {\n code: string;\n client_id: string;\n redirect_uri: string;\n scope: string;\n code_challenge?: string;\n code_challenge_method?: string;\n /** Microsoft provider tokens obtained during authorization */\n providerTokens: ProviderTokens;\n created_at: number;\n expires_at: number;\n}\n\n/**\n * Access token data structure\n * @public\n */\nexport interface AccessToken {\n access_token: string;\n token_type: 'Bearer';\n expires_in: number;\n refresh_token?: string;\n scope: string;\n client_id: string;\n /** Microsoft provider tokens */\n providerTokens: ProviderTokens;\n created_at: number;\n}\n\n// ============================================================================\n// Schema Types\n// ============================================================================\n\n/**\n * Auth required response interface\n * Re-exported from @mcp-z/oauth for consistency\n */\nexport type { AuthRequired, AuthRequiredBranch } from './schemas/index.ts';\n"],"names":["AuthRequiredError"],"mappings":"AAAA;;;CAGC,GAED,qDAAqD;AACrD,qCAAqC;AACrC,2DAA2D;;;;;+BAUlDA;;;eAAAA,wBAAiB;;;qBAAQ"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @mcp-z/oauth-microsoft - Shared Microsoft OAuth implementation
3
+ *
4
+ * Provides OAuth authentication:
5
+ * - Loopback OAuth (RFC 8252) - Server-managed, file-based tokens
6
+ * - Device Code flow (RFC 8628) - For headless/limited-input scenarios
7
+ */
8
+ export { createDcrRouter, type DcrRouterConfig } from './lib/dcr-router.js';
9
+ export { type VerificationResult, verifyBearerToken } from './lib/dcr-verify.js';
10
+ export { type AuthInfo, DcrTokenVerifier } from './lib/token-verifier.js';
11
+ export { DcrOAuthProvider, type DcrOAuthProviderConfig } from './providers/dcr.js';
12
+ export { type DeviceCodeConfig, DeviceCodeProvider } from './providers/device-code.js';
13
+ export { LoopbackOAuthProvider } from './providers/loopback-oauth.js';
14
+ export * as schemas from './schemas/index.js';
15
+ export { createConfig, parseConfig, parseDcrConfig } from './setup/config.js';
16
+ export * from './types.js';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @mcp-z/oauth-microsoft - Shared Microsoft OAuth implementation
3
+ *
4
+ * Provides OAuth authentication:
5
+ * - Loopback OAuth (RFC 8252) - Server-managed, file-based tokens
6
+ * - Device Code flow (RFC 8628) - For headless/limited-input scenarios
7
+ */ export { createDcrRouter } from './lib/dcr-router.js';
8
+ export { verifyBearerToken } from './lib/dcr-verify.js';
9
+ export { DcrTokenVerifier } from './lib/token-verifier.js';
10
+ export { DcrOAuthProvider } from './providers/dcr.js';
11
+ export { DeviceCodeProvider } from './providers/device-code.js';
12
+ export { LoopbackOAuthProvider } from './providers/loopback-oauth.js';
13
+ import * as _schemas from './schemas/index.js';
14
+ export { _schemas as schemas };
15
+ export { createConfig, parseConfig, parseDcrConfig } from './setup/config.js';
16
+ export * from './types.js';
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/Projects/ai/mcp-z/oauth/oauth-microsoft/src/index.ts"],"sourcesContent":["/**\n * @mcp-z/oauth-microsoft - Shared Microsoft OAuth implementation\n *\n * Provides OAuth authentication:\n * - Loopback OAuth (RFC 8252) - Server-managed, file-based tokens\n * - Device Code flow (RFC 8628) - For headless/limited-input scenarios\n */\n\nexport { createDcrRouter, type DcrRouterConfig } from './lib/dcr-router.ts';\nexport { type VerificationResult, verifyBearerToken } from './lib/dcr-verify.ts';\nexport { type AuthInfo, DcrTokenVerifier } from './lib/token-verifier.ts';\nexport { DcrOAuthProvider, type DcrOAuthProviderConfig } from './providers/dcr.ts';\nexport { type DeviceCodeConfig, DeviceCodeProvider } from './providers/device-code.ts';\nexport { LoopbackOAuthProvider } from './providers/loopback-oauth.ts';\nexport * as schemas from './schemas/index.ts';\nexport { createConfig, parseConfig, parseDcrConfig } from './setup/config.ts';\nexport * from './types.ts';\n"],"names":["createDcrRouter","verifyBearerToken","DcrTokenVerifier","DcrOAuthProvider","DeviceCodeProvider","LoopbackOAuthProvider","schemas","createConfig","parseConfig","parseDcrConfig"],"mappings":"AAAA;;;;;;CAMC,GAED,SAASA,eAAe,QAA8B,sBAAsB;AAC5E,SAAkCC,iBAAiB,QAAQ,sBAAsB;AACjF,SAAwBC,gBAAgB,QAAQ,0BAA0B;AAC1E,SAASC,gBAAgB,QAAqC,qBAAqB;AACnF,SAAgCC,kBAAkB,QAAQ,6BAA6B;AACvF,SAASC,qBAAqB,QAAQ,gCAAgC;AACtE,0BAAyB,qBAAqB;AAA9C,SAAO,YAAKC,OAAO,GAA2B;AAC9C,SAASC,YAAY,EAAEC,WAAW,EAAEC,cAAc,QAAQ,oBAAoB;AAC9E,cAAc,aAAa"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * DCR Router - OAuth 2.0 Authorization Server
3
+ *
4
+ * Implements OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591)
5
+ * and OAuth 2.0 Authorization Server endpoints (RFC 6749, RFC 8414, RFC 9728).
6
+ *
7
+ * Endpoints:
8
+ * - GET /.well-known/oauth-authorization-server (RFC 8414 metadata)
9
+ * - GET /.well-known/oauth-protected-resource (RFC 9728 metadata - root)
10
+ * - GET /.well-known/oauth-protected-resource/mcp (RFC 9728 metadata - sub-path)
11
+ * - POST /oauth/register (RFC 7591 client registration)
12
+ * - GET /oauth/authorize (RFC 6749 authorization endpoint)
13
+ * - POST /oauth/token (RFC 6749 token endpoint)
14
+ * - POST /oauth/revoke (RFC 7009 token revocation)
15
+ * - GET /oauth/verify (token verification for Resource Server)
16
+ */
17
+ import express from 'express';
18
+ import type { Keyv } from 'keyv';
19
+ import type { OAuthClientConfig } from '../types.js';
20
+ /**
21
+ * Configuration for DCR Router (self-hosted mode only)
22
+ */
23
+ export interface DcrRouterConfig {
24
+ /** Single Keyv store for all DCR data */
25
+ store: Keyv;
26
+ /** Authorization Server issuer URL */
27
+ issuerUrl: string;
28
+ /** Base URL for OAuth endpoints */
29
+ baseUrl: string;
30
+ /** Supported OAuth scopes */
31
+ scopesSupported: string[];
32
+ /** OAuth client configuration for upstream provider */
33
+ clientConfig: OAuthClientConfig;
34
+ }
35
+ /**
36
+ * Create DCR Router with OAuth 2.0 endpoints (self-hosted mode)
37
+ *
38
+ * For external mode (Auth0/Stitch), don't call this function - no router needed.
39
+ * The server code should check DcrConfig.mode and only call this for 'self-hosted'.
40
+ *
41
+ * @param config - Router configuration
42
+ * @returns Express router with OAuth endpoints
43
+ */
44
+ export declare function createDcrRouter(config: DcrRouterConfig): express.Router;
@@ -0,0 +1,556 @@
1
+ /**
2
+ * DCR Router - OAuth 2.0 Authorization Server
3
+ *
4
+ * Implements OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591)
5
+ * and OAuth 2.0 Authorization Server endpoints (RFC 6749, RFC 8414, RFC 9728).
6
+ *
7
+ * Endpoints:
8
+ * - GET /.well-known/oauth-authorization-server (RFC 8414 metadata)
9
+ * - GET /.well-known/oauth-protected-resource (RFC 9728 metadata - root)
10
+ * - GET /.well-known/oauth-protected-resource/mcp (RFC 9728 metadata - sub-path)
11
+ * - POST /oauth/register (RFC 7591 client registration)
12
+ * - GET /oauth/authorize (RFC 6749 authorization endpoint)
13
+ * - POST /oauth/token (RFC 6749 token endpoint)
14
+ * - POST /oauth/revoke (RFC 7009 token revocation)
15
+ * - GET /oauth/verify (token verification for Resource Server)
16
+ */ import { createHash, randomUUID } from 'crypto';
17
+ import express from 'express';
18
+ import { DcrOAuthProvider } from '../providers/dcr.js';
19
+ import * as dcrUtils from './dcr-utils.js';
20
+ /**
21
+ * Create DCR Router with OAuth 2.0 endpoints (self-hosted mode)
22
+ *
23
+ * For external mode (Auth0/Stitch), don't call this function - no router needed.
24
+ * The server code should check DcrConfig.mode and only call this for 'self-hosted'.
25
+ *
26
+ * @param config - Router configuration
27
+ * @returns Express router with OAuth endpoints
28
+ */ export function createDcrRouter(config) {
29
+ const router = express.Router();
30
+ const { store, issuerUrl, baseUrl, scopesSupported, clientConfig } = config;
31
+ // Apply required middleware for OAuth 2.0 endpoints (RFC 6749)
32
+ router.use(express.json()); // For /oauth/register (application/json)
33
+ router.use(express.urlencoded({
34
+ extended: true
35
+ })); // For /oauth/token (application/x-www-form-urlencoded)
36
+ /**
37
+ * OAuth Authorization Server Metadata (RFC 8414)
38
+ * GET /.well-known/oauth-authorization-server
39
+ */ router.get('/.well-known/oauth-authorization-server', (_req, res)=>{
40
+ const metadata = {
41
+ issuer: issuerUrl,
42
+ authorization_endpoint: `${baseUrl}/oauth/authorize`,
43
+ token_endpoint: `${baseUrl}/oauth/token`,
44
+ registration_endpoint: `${baseUrl}/oauth/register`,
45
+ revocation_endpoint: `${baseUrl}/oauth/revoke`,
46
+ scopes_supported: scopesSupported,
47
+ response_types_supported: [
48
+ 'code'
49
+ ],
50
+ grant_types_supported: [
51
+ 'authorization_code',
52
+ 'refresh_token'
53
+ ],
54
+ token_endpoint_auth_methods_supported: [
55
+ 'client_secret_basic',
56
+ 'client_secret_post'
57
+ ],
58
+ code_challenge_methods_supported: [
59
+ 'S256',
60
+ 'plain'
61
+ ],
62
+ service_documentation: `${baseUrl}/docs`
63
+ };
64
+ res.json(metadata);
65
+ });
66
+ /**
67
+ * OAuth Protected Resource Metadata (RFC 9728 - Root)
68
+ * GET /.well-known/oauth-protected-resource
69
+ */ router.get('/.well-known/oauth-protected-resource', (_req, res)=>{
70
+ const metadata = {
71
+ resource: baseUrl,
72
+ authorization_servers: [
73
+ baseUrl
74
+ ],
75
+ scopes_supported: scopesSupported,
76
+ bearer_methods_supported: [
77
+ 'header'
78
+ ]
79
+ };
80
+ res.json(metadata);
81
+ });
82
+ /**
83
+ * OAuth Protected Resource Metadata (RFC 9728 - Sub-path /mcp)
84
+ * GET /.well-known/oauth-protected-resource/mcp
85
+ */ router.get('/.well-known/oauth-protected-resource/mcp', (_req, res)=>{
86
+ const metadata = {
87
+ resource: `${baseUrl}/mcp`,
88
+ authorization_servers: [
89
+ baseUrl
90
+ ],
91
+ scopes_supported: scopesSupported,
92
+ bearer_methods_supported: [
93
+ 'header'
94
+ ]
95
+ };
96
+ res.json(metadata);
97
+ });
98
+ /**
99
+ * Dynamic Client Registration (RFC 7591)
100
+ * POST /oauth/register
101
+ */ router.post('/oauth/register', async (req, res)=>{
102
+ try {
103
+ const registrationRequest = req.body;
104
+ // Register the client
105
+ const client = await dcrUtils.registerClient(store, registrationRequest);
106
+ // Return client information (RFC 7591 Section 3.2.1)
107
+ res.status(201).json(client);
108
+ } catch (error) {
109
+ res.status(400).json({
110
+ error: 'invalid_client_metadata',
111
+ error_description: error instanceof Error ? error.message : 'Invalid registration request'
112
+ });
113
+ }
114
+ });
115
+ /**
116
+ * OAuth Authorization Endpoint (RFC 6749 Section 3.1)
117
+ * GET /oauth/authorize
118
+ *
119
+ * Initiates Microsoft OAuth flow, then generates DCR authorization code
120
+ */ router.get('/oauth/authorize', async (req, res)=>{
121
+ const { response_type, client_id, redirect_uri, scope = '', state = '', code_challenge, code_challenge_method } = req.query;
122
+ // Validate required parameters
123
+ if (response_type !== 'code') {
124
+ return res.status(400).json({
125
+ error: 'unsupported_response_type',
126
+ error_description: 'Only response_type=code is supported'
127
+ });
128
+ }
129
+ if (!client_id || typeof client_id !== 'string') {
130
+ return res.status(400).json({
131
+ error: 'invalid_request',
132
+ error_description: 'client_id is required'
133
+ });
134
+ }
135
+ if (!redirect_uri || typeof redirect_uri !== 'string') {
136
+ return res.status(400).json({
137
+ error: 'invalid_request',
138
+ error_description: 'redirect_uri is required'
139
+ });
140
+ }
141
+ // Validate client
142
+ const client = await dcrUtils.getClient(store, client_id);
143
+ if (!client) {
144
+ return res.status(400).json({
145
+ error: 'invalid_client',
146
+ error_description: 'Unknown client_id'
147
+ });
148
+ }
149
+ // Validate redirect_uri
150
+ const isValidRedirect = await dcrUtils.validateRedirectUri(store, client_id, redirect_uri);
151
+ if (!isValidRedirect) {
152
+ return res.status(400).json({
153
+ error: 'invalid_request',
154
+ error_description: 'Invalid redirect_uri'
155
+ });
156
+ }
157
+ // Store DCR request state for Microsoft OAuth callback
158
+ const msState = randomUUID();
159
+ const dcrRequestState = {
160
+ client_id,
161
+ redirect_uri,
162
+ scope: typeof scope === 'string' ? scope : '',
163
+ state: typeof state === 'string' ? state : undefined,
164
+ code_challenge: typeof code_challenge === 'string' ? code_challenge : undefined,
165
+ code_challenge_method: typeof code_challenge_method === 'string' ? code_challenge_method : undefined,
166
+ created_at: Date.now(),
167
+ expires_at: Date.now() + 600000
168
+ };
169
+ await store.set(`dcr:ms-state:${msState}`, dcrRequestState, 600000); // 10 min TTL
170
+ // Build Microsoft authorization URL
171
+ const msAuthUrl = new URL(`https://login.microsoftonline.com/${clientConfig.tenantId || 'common'}/oauth2/v2.0/authorize`);
172
+ msAuthUrl.searchParams.set('client_id', clientConfig.clientId);
173
+ msAuthUrl.searchParams.set('response_type', 'code');
174
+ msAuthUrl.searchParams.set('redirect_uri', `${baseUrl}/oauth/callback`);
175
+ msAuthUrl.searchParams.set('scope', typeof scope === 'string' ? scope : '');
176
+ msAuthUrl.searchParams.set('state', msState);
177
+ msAuthUrl.searchParams.set('response_mode', 'query');
178
+ // Redirect user to Microsoft for authorization
179
+ return res.redirect(msAuthUrl.toString());
180
+ });
181
+ /**
182
+ * OAuth Callback Handler
183
+ * GET /oauth/callback
184
+ *
185
+ * Handles callback from Microsoft after user authorization
186
+ */ router.get('/oauth/callback', async (req, res)=>{
187
+ const { code: msCode, state: msState, error, error_description } = req.query;
188
+ // Handle Microsoft OAuth errors
189
+ if (error) {
190
+ return res.status(400).json({
191
+ error,
192
+ error_description: error_description || 'Microsoft OAuth authorization failed'
193
+ });
194
+ }
195
+ if (!msCode || typeof msCode !== 'string') {
196
+ return res.status(400).json({
197
+ error: 'invalid_request',
198
+ error_description: 'Authorization code is required'
199
+ });
200
+ }
201
+ if (!msState || typeof msState !== 'string') {
202
+ return res.status(400).json({
203
+ error: 'invalid_request',
204
+ error_description: 'State parameter is required'
205
+ });
206
+ }
207
+ // Retrieve original DCR request state
208
+ const dcrRequestState = await store.get(`dcr:ms-state:${msState}`);
209
+ if (!dcrRequestState) {
210
+ return res.status(400).json({
211
+ error: 'invalid_request',
212
+ error_description: 'Invalid or expired state parameter'
213
+ });
214
+ }
215
+ // Delete state (one-time use)
216
+ await store.delete(`dcr:ms-state:${msState}`);
217
+ // Exchange Microsoft authorization code for tokens
218
+ try {
219
+ const tokenUrl = `https://login.microsoftonline.com/${clientConfig.tenantId || 'common'}/oauth2/v2.0/token`;
220
+ const tokenParams = new URLSearchParams({
221
+ grant_type: 'authorization_code',
222
+ code: msCode,
223
+ client_id: clientConfig.clientId,
224
+ redirect_uri: `${baseUrl}/oauth/callback`,
225
+ scope: dcrRequestState.scope
226
+ });
227
+ // Add client_secret if available (confidential client)
228
+ if (clientConfig.clientSecret) {
229
+ tokenParams.set('client_secret', clientConfig.clientSecret);
230
+ }
231
+ const tokenResponse = await fetch(tokenUrl, {
232
+ method: 'POST',
233
+ headers: {
234
+ 'Content-Type': 'application/x-www-form-urlencoded'
235
+ },
236
+ body: tokenParams.toString()
237
+ });
238
+ if (!tokenResponse.ok) {
239
+ const errorData = await tokenResponse.json();
240
+ throw new Error(`Microsoft token exchange failed: ${errorData.error_description || errorData.error}`);
241
+ }
242
+ const tokenData = await tokenResponse.json();
243
+ // Create provider tokens from Microsoft response
244
+ const providerTokens = {
245
+ accessToken: tokenData.access_token,
246
+ ...tokenData.refresh_token && {
247
+ refreshToken: tokenData.refresh_token
248
+ },
249
+ expiresAt: Date.now() + tokenData.expires_in * 1000,
250
+ scope: tokenData.scope
251
+ };
252
+ // Generate DCR authorization code with real provider tokens
253
+ const dcrCode = randomUUID();
254
+ const authCode = {
255
+ code: dcrCode,
256
+ client_id: dcrRequestState.client_id,
257
+ redirect_uri: dcrRequestState.redirect_uri,
258
+ scope: dcrRequestState.scope,
259
+ ...dcrRequestState.code_challenge && {
260
+ code_challenge: dcrRequestState.code_challenge
261
+ },
262
+ ...dcrRequestState.code_challenge_method && {
263
+ code_challenge_method: dcrRequestState.code_challenge_method
264
+ },
265
+ providerTokens,
266
+ created_at: Date.now(),
267
+ expires_at: Date.now() + 600000
268
+ };
269
+ await dcrUtils.setAuthCode(store, dcrCode, authCode);
270
+ // Redirect back to MCP client with DCR authorization code
271
+ const clientRedirectUrl = new URL(dcrRequestState.redirect_uri);
272
+ clientRedirectUrl.searchParams.set('code', dcrCode);
273
+ if (dcrRequestState.state) {
274
+ clientRedirectUrl.searchParams.set('state', dcrRequestState.state);
275
+ }
276
+ return res.redirect(clientRedirectUrl.toString());
277
+ } catch (error) {
278
+ return res.status(500).json({
279
+ error: 'server_error',
280
+ error_description: error instanceof Error ? error.message : 'Failed to exchange authorization code'
281
+ });
282
+ }
283
+ });
284
+ /**
285
+ * OAuth Token Endpoint (RFC 6749 Section 3.2)
286
+ * POST /oauth/token
287
+ */ router.post('/oauth/token', async (req, res)=>{
288
+ // Extract client credentials from either body or Basic Auth header
289
+ let client_id = req.body.client_id;
290
+ let client_secret = req.body.client_secret;
291
+ // Support client_secret_basic authentication (RFC 6749 Section 2.3.1)
292
+ const authHeader = req.headers.authorization;
293
+ if (authHeader && authHeader.startsWith('Basic ')) {
294
+ const base64Credentials = authHeader.substring(6);
295
+ const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
296
+ const [id, secret] = credentials.split(':');
297
+ client_id = id;
298
+ client_secret = secret;
299
+ }
300
+ const { grant_type, code, redirect_uri, refresh_token, code_verifier } = req.body;
301
+ // Validate grant_type
302
+ if (!grant_type) {
303
+ return res.status(400).json({
304
+ error: 'invalid_request',
305
+ error_description: 'grant_type is required'
306
+ });
307
+ }
308
+ if (grant_type === 'authorization_code') {
309
+ // Authorization Code Grant
310
+ if (!code || !client_id || !redirect_uri) {
311
+ return res.status(400).json({
312
+ error: 'invalid_request',
313
+ error_description: 'code, client_id, and redirect_uri are required'
314
+ });
315
+ }
316
+ // Validate client credentials
317
+ const isValidClient = await dcrUtils.validateClient(store, client_id, client_secret !== null && client_secret !== void 0 ? client_secret : '');
318
+ if (!isValidClient) {
319
+ return res.status(401).json({
320
+ error: 'invalid_client',
321
+ error_description: 'Invalid client credentials'
322
+ });
323
+ }
324
+ // Get authorization code
325
+ const authCode = await dcrUtils.getAuthCode(store, code);
326
+ if (!authCode) {
327
+ return res.status(400).json({
328
+ error: 'invalid_grant',
329
+ error_description: 'Invalid or expired authorization code'
330
+ });
331
+ }
332
+ // Validate authorization code
333
+ if (authCode.client_id !== client_id || authCode.redirect_uri !== redirect_uri) {
334
+ return res.status(400).json({
335
+ error: 'invalid_grant',
336
+ error_description: 'Authorization code mismatch'
337
+ });
338
+ }
339
+ if (Date.now() > authCode.expires_at) {
340
+ await dcrUtils.deleteAuthCode(store, code);
341
+ return res.status(400).json({
342
+ error: 'invalid_grant',
343
+ error_description: 'Authorization code expired'
344
+ });
345
+ }
346
+ // Validate PKCE if used
347
+ if (authCode.code_challenge) {
348
+ var _authCode_code_challenge_method;
349
+ if (!code_verifier) {
350
+ return res.status(400).json({
351
+ error: 'invalid_request',
352
+ error_description: 'code_verifier is required for PKCE'
353
+ });
354
+ }
355
+ // Validate code_verifier against code_challenge
356
+ const method = (_authCode_code_challenge_method = authCode.code_challenge_method) !== null && _authCode_code_challenge_method !== void 0 ? _authCode_code_challenge_method : 'plain';
357
+ const computedChallenge = method === 'S256' ? createHash('sha256').update(code_verifier).digest('base64url') : code_verifier;
358
+ if (computedChallenge !== authCode.code_challenge) {
359
+ return res.status(400).json({
360
+ error: 'invalid_grant',
361
+ error_description: 'Invalid code_verifier'
362
+ });
363
+ }
364
+ }
365
+ // Delete authorization code (one-time use)
366
+ await dcrUtils.deleteAuthCode(store, code);
367
+ // Generate DCR access token
368
+ const accessToken = randomUUID();
369
+ const refreshTokenValue = randomUUID();
370
+ const tokenData = {
371
+ access_token: accessToken,
372
+ token_type: 'Bearer',
373
+ expires_in: 3600,
374
+ refresh_token: refreshTokenValue,
375
+ scope: authCode.scope,
376
+ client_id,
377
+ providerTokens: authCode.providerTokens,
378
+ created_at: Date.now()
379
+ };
380
+ await dcrUtils.setAccessToken(store, accessToken, tokenData);
381
+ await dcrUtils.setRefreshToken(store, refreshTokenValue, tokenData);
382
+ // Store provider tokens indexed by DCR access token
383
+ await dcrUtils.setProviderTokens(store, accessToken, authCode.providerTokens);
384
+ // Return token response
385
+ return res.json({
386
+ access_token: tokenData.access_token,
387
+ token_type: tokenData.token_type,
388
+ expires_in: tokenData.expires_in,
389
+ refresh_token: tokenData.refresh_token,
390
+ scope: tokenData.scope
391
+ });
392
+ }
393
+ if (grant_type === 'refresh_token') {
394
+ // Refresh Token Grant
395
+ if (!refresh_token || !client_id) {
396
+ return res.status(400).json({
397
+ error: 'invalid_request',
398
+ error_description: 'refresh_token and client_id are required'
399
+ });
400
+ }
401
+ // Validate client credentials
402
+ const isValidClient = await dcrUtils.validateClient(store, client_id, client_secret !== null && client_secret !== void 0 ? client_secret : '');
403
+ if (!isValidClient) {
404
+ return res.status(401).json({
405
+ error: 'invalid_client',
406
+ error_description: 'Invalid client credentials'
407
+ });
408
+ }
409
+ // Get refresh token
410
+ const tokenData = await dcrUtils.getRefreshToken(store, refresh_token);
411
+ if (!tokenData || tokenData.client_id !== client_id) {
412
+ return res.status(400).json({
413
+ error: 'invalid_grant',
414
+ error_description: 'Invalid refresh token'
415
+ });
416
+ }
417
+ // Refresh provider tokens if available
418
+ let refreshedProviderTokens = tokenData.providerTokens;
419
+ if (tokenData.providerTokens.refreshToken) {
420
+ try {
421
+ var _clientConfig_tenantId;
422
+ // Create DcrOAuthProvider instance to refresh Microsoft tokens
423
+ const provider = new DcrOAuthProvider({
424
+ clientId: clientConfig.clientId,
425
+ ...clientConfig.clientSecret && {
426
+ clientSecret: clientConfig.clientSecret
427
+ },
428
+ tenantId: (_clientConfig_tenantId = clientConfig.tenantId) !== null && _clientConfig_tenantId !== void 0 ? _clientConfig_tenantId : 'common',
429
+ scope: tokenData.scope,
430
+ verifyEndpoint: `${baseUrl}/oauth/verify`,
431
+ logger: {
432
+ info: console.log,
433
+ error: console.error,
434
+ warn: console.warn,
435
+ debug: ()=>{}
436
+ }
437
+ });
438
+ // Refresh the Microsoft access token
439
+ refreshedProviderTokens = await provider.refreshAccessToken(tokenData.providerTokens.refreshToken);
440
+ } catch (error) {
441
+ // If refresh fails, continue with existing tokens (they may still be valid)
442
+ console.warn('Provider token refresh failed, using existing tokens:', error instanceof Error ? error.message : String(error));
443
+ }
444
+ }
445
+ // Generate new DCR access token
446
+ const newAccessToken = randomUUID();
447
+ const newTokenData = {
448
+ ...tokenData,
449
+ access_token: newAccessToken,
450
+ created_at: Date.now()
451
+ };
452
+ await dcrUtils.setAccessToken(store, newAccessToken, newTokenData);
453
+ // Store refreshed provider tokens indexed by new DCR access token
454
+ await dcrUtils.setProviderTokens(store, newAccessToken, refreshedProviderTokens);
455
+ return res.json({
456
+ access_token: newTokenData.access_token,
457
+ token_type: newTokenData.token_type,
458
+ expires_in: newTokenData.expires_in,
459
+ scope: newTokenData.scope
460
+ });
461
+ }
462
+ return res.status(400).json({
463
+ error: 'unsupported_grant_type',
464
+ error_description: 'Only authorization_code and refresh_token grants are supported'
465
+ });
466
+ });
467
+ /**
468
+ * OAuth Token Revocation (RFC 7009)
469
+ * POST /oauth/revoke
470
+ */ router.post('/oauth/revoke', async (req, res)=>{
471
+ const { token, token_type_hint, client_id, client_secret } = req.body;
472
+ if (!token) {
473
+ return res.status(400).json({
474
+ error: 'invalid_request',
475
+ error_description: 'token is required'
476
+ });
477
+ }
478
+ // Validate client if credentials provided
479
+ if (client_id && client_secret) {
480
+ const isValidClient = await dcrUtils.validateClient(store, client_id, client_secret);
481
+ if (!isValidClient) {
482
+ return res.status(401).json({
483
+ error: 'invalid_client',
484
+ error_description: 'Invalid client credentials'
485
+ });
486
+ }
487
+ }
488
+ // Revoke the token
489
+ if (token_type_hint === 'refresh_token') {
490
+ await dcrUtils.deleteRefreshToken(store, token);
491
+ } else if (token_type_hint === 'access_token') {
492
+ await dcrUtils.deleteAccessToken(store, token);
493
+ await dcrUtils.deleteProviderTokens(store, token);
494
+ } else {
495
+ // No hint - try both
496
+ await dcrUtils.deleteRefreshToken(store, token);
497
+ await dcrUtils.deleteAccessToken(store, token);
498
+ await dcrUtils.deleteProviderTokens(store, token);
499
+ }
500
+ // RFC 7009: Return 200 even if token not found
501
+ return res.status(200).send();
502
+ });
503
+ /**
504
+ * Token Verification Endpoint
505
+ * GET /oauth/verify
506
+ *
507
+ * Validates bearer tokens for Resource Server.
508
+ * Returns AuthInfo with provider tokens for stateless DCR pattern.
509
+ */ router.get('/oauth/verify', async (req, res)=>{
510
+ // Extract bearer token from Authorization header
511
+ const authHeader = req.headers.authorization;
512
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
513
+ return res.status(401).json({
514
+ error: 'invalid_request',
515
+ error_description: 'Missing or invalid Authorization header'
516
+ });
517
+ }
518
+ const token = authHeader.substring(7); // Remove 'Bearer ' prefix
519
+ // Validate token exists in access tokens store
520
+ const tokenData = await dcrUtils.getAccessToken(store, token);
521
+ if (!tokenData) {
522
+ return res.status(401).json({
523
+ error: 'invalid_token',
524
+ error_description: 'Unknown or expired access token'
525
+ });
526
+ }
527
+ // Check if token is expired
528
+ const now = Date.now();
529
+ const expiresAt = tokenData.created_at + tokenData.expires_in * 1000;
530
+ if (now > expiresAt) {
531
+ // Remove expired token
532
+ await dcrUtils.deleteAccessToken(store, token);
533
+ await dcrUtils.deleteProviderTokens(store, token);
534
+ return res.status(401).json({
535
+ error: 'invalid_token',
536
+ error_description: 'Access token has expired'
537
+ });
538
+ }
539
+ // Return AuthInfo with provider tokens for stateless DCR
540
+ const authInfo = {
541
+ token,
542
+ clientId: tokenData.client_id,
543
+ scopes: tokenData.scope ? tokenData.scope.split(' ') : [],
544
+ expiresAt,
545
+ providerTokens: tokenData.providerTokens
546
+ };
547
+ return res.json(authInfo);
548
+ });
549
+ /**
550
+ * Debug endpoint to list registered clients (development only)
551
+ */ router.get('/debug/clients', async (_req, res)=>{
552
+ const clients = await dcrUtils.listClients(store);
553
+ res.json(clients);
554
+ });
555
+ return router;
556
+ }