@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.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/cjs/index.d.cts +16 -0
- package/dist/cjs/index.d.ts +16 -0
- package/dist/cjs/index.js +112 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/lib/dcr-router.d.cts +44 -0
- package/dist/cjs/lib/dcr-router.d.ts +44 -0
- package/dist/cjs/lib/dcr-router.js +1227 -0
- package/dist/cjs/lib/dcr-router.js.map +1 -0
- package/dist/cjs/lib/dcr-utils.d.cts +160 -0
- package/dist/cjs/lib/dcr-utils.d.ts +160 -0
- package/dist/cjs/lib/dcr-utils.js +860 -0
- package/dist/cjs/lib/dcr-utils.js.map +1 -0
- package/dist/cjs/lib/dcr-verify.d.cts +53 -0
- package/dist/cjs/lib/dcr-verify.d.ts +53 -0
- package/dist/cjs/lib/dcr-verify.js +193 -0
- package/dist/cjs/lib/dcr-verify.js.map +1 -0
- package/dist/cjs/lib/fetch-with-timeout.d.cts +14 -0
- package/dist/cjs/lib/fetch-with-timeout.d.ts +14 -0
- package/dist/cjs/lib/fetch-with-timeout.js +257 -0
- package/dist/cjs/lib/fetch-with-timeout.js.map +1 -0
- package/dist/cjs/lib/token-verifier.d.cts +44 -0
- package/dist/cjs/lib/token-verifier.d.ts +44 -0
- package/dist/cjs/lib/token-verifier.js +253 -0
- package/dist/cjs/lib/token-verifier.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/providers/dcr.d.cts +110 -0
- package/dist/cjs/providers/dcr.d.ts +110 -0
- package/dist/cjs/providers/dcr.js +600 -0
- package/dist/cjs/providers/dcr.js.map +1 -0
- package/dist/cjs/providers/device-code.d.cts +179 -0
- package/dist/cjs/providers/device-code.d.ts +179 -0
- package/dist/cjs/providers/device-code.js +896 -0
- package/dist/cjs/providers/device-code.js.map +1 -0
- package/dist/cjs/providers/loopback-oauth.d.cts +125 -0
- package/dist/cjs/providers/loopback-oauth.d.ts +125 -0
- package/dist/cjs/providers/loopback-oauth.js +1325 -0
- package/dist/cjs/providers/loopback-oauth.js.map +1 -0
- package/dist/cjs/schemas/index.d.cts +20 -0
- package/dist/cjs/schemas/index.d.ts +20 -0
- package/dist/cjs/schemas/index.js +37 -0
- package/dist/cjs/schemas/index.js.map +1 -0
- package/dist/cjs/setup/config.d.cts +113 -0
- package/dist/cjs/setup/config.d.ts +113 -0
- package/dist/cjs/setup/config.js +246 -0
- package/dist/cjs/setup/config.js.map +1 -0
- package/dist/cjs/types.d.cts +188 -0
- package/dist/cjs/types.d.ts +188 -0
- package/dist/cjs/types.js +18 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/esm/index.d.ts +16 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/lib/dcr-router.d.ts +44 -0
- package/dist/esm/lib/dcr-router.js +556 -0
- package/dist/esm/lib/dcr-router.js.map +1 -0
- package/dist/esm/lib/dcr-utils.d.ts +160 -0
- package/dist/esm/lib/dcr-utils.js +270 -0
- package/dist/esm/lib/dcr-utils.js.map +1 -0
- package/dist/esm/lib/dcr-verify.d.ts +53 -0
- package/dist/esm/lib/dcr-verify.js +53 -0
- package/dist/esm/lib/dcr-verify.js.map +1 -0
- package/dist/esm/lib/fetch-with-timeout.d.ts +14 -0
- package/dist/esm/lib/fetch-with-timeout.js +30 -0
- package/dist/esm/lib/fetch-with-timeout.js.map +1 -0
- package/dist/esm/lib/token-verifier.d.ts +44 -0
- package/dist/esm/lib/token-verifier.js +53 -0
- package/dist/esm/lib/token-verifier.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/providers/dcr.d.ts +110 -0
- package/dist/esm/providers/dcr.js +235 -0
- package/dist/esm/providers/dcr.js.map +1 -0
- package/dist/esm/providers/device-code.d.ts +179 -0
- package/dist/esm/providers/device-code.js +417 -0
- package/dist/esm/providers/device-code.js.map +1 -0
- package/dist/esm/providers/loopback-oauth.d.ts +125 -0
- package/dist/esm/providers/loopback-oauth.js +643 -0
- package/dist/esm/providers/loopback-oauth.js.map +1 -0
- package/dist/esm/schemas/index.d.ts +20 -0
- package/dist/esm/schemas/index.js +18 -0
- package/dist/esm/schemas/index.js.map +1 -0
- package/dist/esm/setup/config.d.ts +113 -0
- package/dist/esm/setup/config.js +268 -0
- package/dist/esm/setup/config.js.map +1 -0
- package/dist/esm/types.d.ts +188 -0
- package/dist/esm/types.js +8 -0
- package/dist/esm/types.js.map +1 -0
- package/package.json +87 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DCR Token Verification Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides token verification for both self-hosted and external DCR modes:
|
|
5
|
+
* - Self-hosted: Verifies tokens against local DCR router (/oauth/verify)
|
|
6
|
+
* - External: Verifies tokens against Auth0/Stitch verification endpoint
|
|
7
|
+
*/ import { fetchWithTimeout } from './fetch-with-timeout.js';
|
|
8
|
+
/**
|
|
9
|
+
* Verify bearer token against DCR authorization server
|
|
10
|
+
*
|
|
11
|
+
* Supports both self-hosted and external DCR modes by calling the
|
|
12
|
+
* /oauth/verify endpoint (or equivalent external URL).
|
|
13
|
+
*
|
|
14
|
+
* @param bearerToken - Bearer token to verify (without "Bearer " prefix)
|
|
15
|
+
* @param verifyUrl - Verification endpoint URL (self-hosted or external)
|
|
16
|
+
* @returns Verification result with provider tokens
|
|
17
|
+
* @throws Error if verification fails
|
|
18
|
+
*
|
|
19
|
+
* @example Self-hosted mode
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const result = await verifyBearerToken(
|
|
22
|
+
* token,
|
|
23
|
+
* 'http://localhost:3456/oauth/verify'
|
|
24
|
+
* );
|
|
25
|
+
* const auth = provider.toAuthProvider(result.providerTokens);
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example External mode (Auth0/Stitch)
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const result = await verifyBearerToken(
|
|
31
|
+
* token,
|
|
32
|
+
* 'https://auth.example.com/oauth/verify'
|
|
33
|
+
* );
|
|
34
|
+
* const auth = provider.toAuthProvider(result.providerTokens);
|
|
35
|
+
* ```
|
|
36
|
+
*/ export async function verifyBearerToken(bearerToken, verifyUrl) {
|
|
37
|
+
const response = await fetchWithTimeout(verifyUrl, {
|
|
38
|
+
method: 'GET',
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Bearer ${bearerToken}`
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const errorText = await response.text();
|
|
45
|
+
throw new Error(`Token verification failed: ${response.status} ${errorText}`);
|
|
46
|
+
}
|
|
47
|
+
const result = await response.json();
|
|
48
|
+
// Validate required fields
|
|
49
|
+
if (!result.providerTokens || !result.providerTokens.accessToken) {
|
|
50
|
+
throw new Error('Verification response missing required provider tokens');
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/Projects/ai/mcp-z/oauth/oauth-microsoft/src/lib/dcr-verify.ts"],"sourcesContent":["/**\n * DCR Token Verification Utilities\n *\n * Provides token verification for both self-hosted and external DCR modes:\n * - Self-hosted: Verifies tokens against local DCR router (/oauth/verify)\n * - External: Verifies tokens against Auth0/Stitch verification endpoint\n */\n\nimport type { ProviderTokens } from '@mcp-z/oauth';\nimport { fetchWithTimeout } from './fetch-with-timeout.ts';\n\n/**\n * Verification result from DCR authorization server\n */\nexport interface VerificationResult {\n /** Bearer token that was verified */\n token: string;\n /** Client ID associated with the token */\n clientId: string;\n /** OAuth scopes granted to the token */\n scopes: string[];\n /** Token expiration timestamp (milliseconds since epoch) */\n expiresAt: number;\n /** Provider tokens (Microsoft access/refresh tokens) */\n providerTokens: ProviderTokens;\n}\n\n/**\n * Verify bearer token against DCR authorization server\n *\n * Supports both self-hosted and external DCR modes by calling the\n * /oauth/verify endpoint (or equivalent external URL).\n *\n * @param bearerToken - Bearer token to verify (without \"Bearer \" prefix)\n * @param verifyUrl - Verification endpoint URL (self-hosted or external)\n * @returns Verification result with provider tokens\n * @throws Error if verification fails\n *\n * @example Self-hosted mode\n * ```typescript\n * const result = await verifyBearerToken(\n * token,\n * 'http://localhost:3456/oauth/verify'\n * );\n * const auth = provider.toAuthProvider(result.providerTokens);\n * ```\n *\n * @example External mode (Auth0/Stitch)\n * ```typescript\n * const result = await verifyBearerToken(\n * token,\n * 'https://auth.example.com/oauth/verify'\n * );\n * const auth = provider.toAuthProvider(result.providerTokens);\n * ```\n */\nexport async function verifyBearerToken(bearerToken: string, verifyUrl: string): Promise<VerificationResult> {\n const response = await fetchWithTimeout(verifyUrl, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${bearerToken}`,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token verification failed: ${response.status} ${errorText}`);\n }\n\n const result = (await response.json()) as VerificationResult;\n\n // Validate required fields\n if (!result.providerTokens || !result.providerTokens.accessToken) {\n throw new Error('Verification response missing required provider tokens');\n }\n\n return result;\n}\n"],"names":["fetchWithTimeout","verifyBearerToken","bearerToken","verifyUrl","response","method","headers","Authorization","ok","errorText","text","Error","status","result","json","providerTokens","accessToken"],"mappings":"AAAA;;;;;;CAMC,GAGD,SAASA,gBAAgB,QAAQ,0BAA0B;AAkB3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,eAAeC,kBAAkBC,WAAmB,EAAEC,SAAiB;IAC5E,MAAMC,WAAW,MAAMJ,iBAAiBG,WAAW;QACjDE,QAAQ;QACRC,SAAS;YACPC,eAAe,CAAC,OAAO,EAAEL,aAAa;QACxC;IACF;IAEA,IAAI,CAACE,SAASI,EAAE,EAAE;QAChB,MAAMC,YAAY,MAAML,SAASM,IAAI;QACrC,MAAM,IAAIC,MAAM,CAAC,2BAA2B,EAAEP,SAASQ,MAAM,CAAC,CAAC,EAAEH,WAAW;IAC9E;IAEA,MAAMI,SAAU,MAAMT,SAASU,IAAI;IAEnC,2BAA2B;IAC3B,IAAI,CAACD,OAAOE,cAAc,IAAI,CAACF,OAAOE,cAAc,CAACC,WAAW,EAAE;QAChE,MAAM,IAAIL,MAAM;IAClB;IAEA,OAAOE;AACT"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch with timeout to prevent hanging requests
|
|
3
|
+
*
|
|
4
|
+
* This utility wraps the native fetch API with an AbortController to ensure
|
|
5
|
+
* requests don't hang indefinitely. This is critical for OAuth token operations
|
|
6
|
+
* where expired tokens might cause Microsoft's servers to hang or respond slowly.
|
|
7
|
+
*
|
|
8
|
+
* @param url - URL to fetch
|
|
9
|
+
* @param options - Fetch options
|
|
10
|
+
* @param timeoutMs - Timeout in milliseconds (default: 30000)
|
|
11
|
+
* @returns Fetch response
|
|
12
|
+
* @throws Error if request times out
|
|
13
|
+
*/
|
|
14
|
+
export declare function fetchWithTimeout(url: string, options?: RequestInit, timeoutMs?: number): Promise<Response>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch with timeout to prevent hanging requests
|
|
3
|
+
*
|
|
4
|
+
* This utility wraps the native fetch API with an AbortController to ensure
|
|
5
|
+
* requests don't hang indefinitely. This is critical for OAuth token operations
|
|
6
|
+
* where expired tokens might cause Microsoft's servers to hang or respond slowly.
|
|
7
|
+
*
|
|
8
|
+
* @param url - URL to fetch
|
|
9
|
+
* @param options - Fetch options
|
|
10
|
+
* @param timeoutMs - Timeout in milliseconds (default: 30000)
|
|
11
|
+
* @returns Fetch response
|
|
12
|
+
* @throws Error if request times out
|
|
13
|
+
*/ export async function fetchWithTimeout(url, options, timeoutMs = 30000) {
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timeoutId = setTimeout(()=>controller.abort(), timeoutMs);
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
...options,
|
|
19
|
+
signal: controller.signal
|
|
20
|
+
});
|
|
21
|
+
clearTimeout(timeoutId);
|
|
22
|
+
return response;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
clearTimeout(timeoutId);
|
|
25
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
26
|
+
throw new Error(`Request timeout after ${timeoutMs}ms`);
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/Projects/ai/mcp-z/oauth/oauth-microsoft/src/lib/fetch-with-timeout.ts"],"sourcesContent":["/**\n * Fetch with timeout to prevent hanging requests\n *\n * This utility wraps the native fetch API with an AbortController to ensure\n * requests don't hang indefinitely. This is critical for OAuth token operations\n * where expired tokens might cause Microsoft's servers to hang or respond slowly.\n *\n * @param url - URL to fetch\n * @param options - Fetch options\n * @param timeoutMs - Timeout in milliseconds (default: 30000)\n * @returns Fetch response\n * @throws Error if request times out\n */\nexport async function fetchWithTimeout(url: string, options?: RequestInit, timeoutMs = 30000): Promise<Response> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n ...options,\n signal: controller.signal,\n });\n clearTimeout(timeoutId);\n return response;\n } catch (error) {\n clearTimeout(timeoutId);\n if (error instanceof Error && error.name === 'AbortError') {\n throw new Error(`Request timeout after ${timeoutMs}ms`);\n }\n throw error;\n }\n}\n"],"names":["fetchWithTimeout","url","options","timeoutMs","controller","AbortController","timeoutId","setTimeout","abort","response","fetch","signal","clearTimeout","error","Error","name"],"mappings":"AAAA;;;;;;;;;;;;CAYC,GACD,OAAO,eAAeA,iBAAiBC,GAAW,EAAEC,OAAqB,EAAEC,YAAY,KAAK;IAC1F,MAAMC,aAAa,IAAIC;IACvB,MAAMC,YAAYC,WAAW,IAAMH,WAAWI,KAAK,IAAIL;IAEvD,IAAI;QACF,MAAMM,WAAW,MAAMC,MAAMT,KAAK;YAChC,GAAGC,OAAO;YACVS,QAAQP,WAAWO,MAAM;QAC3B;QACAC,aAAaN;QACb,OAAOG;IACT,EAAE,OAAOI,OAAO;QACdD,aAAaN;QACb,IAAIO,iBAAiBC,SAASD,MAAME,IAAI,KAAK,cAAc;YACzD,MAAM,IAAID,MAAM,CAAC,sBAAsB,EAAEX,UAAU,EAAE,CAAC;QACxD;QACA,MAAMU;IACR;AACF"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DCR Token Verifier
|
|
3
|
+
*
|
|
4
|
+
* Validates bearer tokens by calling Authorization Server's verification endpoint.
|
|
5
|
+
* Implements proper AS/RS separation - Resource Server doesn't access token storage.
|
|
6
|
+
*/
|
|
7
|
+
import type { ProviderTokens } from '@mcp-z/oauth';
|
|
8
|
+
/**
|
|
9
|
+
* Authentication information from token verification
|
|
10
|
+
*/
|
|
11
|
+
export interface AuthInfo {
|
|
12
|
+
/** Bearer access token */
|
|
13
|
+
token: string;
|
|
14
|
+
/** Client ID that owns the token */
|
|
15
|
+
clientId: string;
|
|
16
|
+
/** Granted scopes */
|
|
17
|
+
scopes: string[];
|
|
18
|
+
/** Token expiration timestamp (milliseconds since epoch) */
|
|
19
|
+
expiresAt: number;
|
|
20
|
+
/** Microsoft provider tokens (if available) */
|
|
21
|
+
providerTokens?: ProviderTokens;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* DCR Token Verifier validates access tokens via Authorization Server
|
|
25
|
+
*
|
|
26
|
+
* This implements proper OAuth 2.0 architecture where the Resource Server
|
|
27
|
+
* (MCP server) validates tokens by calling the Authorization Server's
|
|
28
|
+
* verification endpoint rather than accessing token storage directly.
|
|
29
|
+
*/
|
|
30
|
+
export declare class DcrTokenVerifier {
|
|
31
|
+
private verifyUrl;
|
|
32
|
+
/**
|
|
33
|
+
* @param verifyUrl - Authorization Server's /oauth/verify endpoint URL
|
|
34
|
+
*/
|
|
35
|
+
constructor(verifyUrl: string);
|
|
36
|
+
/**
|
|
37
|
+
* Verify an access token by calling the Authorization Server
|
|
38
|
+
*
|
|
39
|
+
* @param token - Bearer access token to validate
|
|
40
|
+
* @returns AuthInfo with token metadata and provider tokens
|
|
41
|
+
* @throws Error if token is invalid or verification fails
|
|
42
|
+
*/
|
|
43
|
+
verifyAccessToken(token: string): Promise<AuthInfo>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DCR Token Verifier
|
|
3
|
+
*
|
|
4
|
+
* Validates bearer tokens by calling Authorization Server's verification endpoint.
|
|
5
|
+
* Implements proper AS/RS separation - Resource Server doesn't access token storage.
|
|
6
|
+
*/ /**
|
|
7
|
+
* DCR Token Verifier validates access tokens via Authorization Server
|
|
8
|
+
*
|
|
9
|
+
* This implements proper OAuth 2.0 architecture where the Resource Server
|
|
10
|
+
* (MCP server) validates tokens by calling the Authorization Server's
|
|
11
|
+
* verification endpoint rather than accessing token storage directly.
|
|
12
|
+
*/ export class DcrTokenVerifier {
|
|
13
|
+
/**
|
|
14
|
+
* Verify an access token by calling the Authorization Server
|
|
15
|
+
*
|
|
16
|
+
* @param token - Bearer access token to validate
|
|
17
|
+
* @returns AuthInfo with token metadata and provider tokens
|
|
18
|
+
* @throws Error if token is invalid or verification fails
|
|
19
|
+
*/ async verifyAccessToken(token) {
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(this.verifyUrl, {
|
|
22
|
+
method: 'GET',
|
|
23
|
+
headers: {
|
|
24
|
+
Authorization: `Bearer ${token}`
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
let errorMessage = 'Unknown error';
|
|
29
|
+
try {
|
|
30
|
+
var _ref, _error_error_description;
|
|
31
|
+
const error = await response.json();
|
|
32
|
+
errorMessage = (_ref = (_error_error_description = error.error_description) !== null && _error_error_description !== void 0 ? _error_error_description : error.error) !== null && _ref !== void 0 ? _ref : errorMessage;
|
|
33
|
+
} catch {
|
|
34
|
+
// Failed to parse error JSON, use status text
|
|
35
|
+
errorMessage = response.statusText;
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`Token verification failed: ${errorMessage}`);
|
|
38
|
+
}
|
|
39
|
+
const authInfo = await response.json();
|
|
40
|
+
return authInfo;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error instanceof Error) {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Token verification failed: ${String(error)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* @param verifyUrl - Authorization Server's /oauth/verify endpoint URL
|
|
50
|
+
*/ constructor(verifyUrl){
|
|
51
|
+
this.verifyUrl = verifyUrl;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/Projects/ai/mcp-z/oauth/oauth-microsoft/src/lib/token-verifier.ts"],"sourcesContent":["/**\n * DCR Token Verifier\n *\n * Validates bearer tokens by calling Authorization Server's verification endpoint.\n * Implements proper AS/RS separation - Resource Server doesn't access token storage.\n */\n\nimport type { ProviderTokens } from '@mcp-z/oauth';\n\n/**\n * Authentication information from token verification\n */\nexport interface AuthInfo {\n /** Bearer access token */\n token: string;\n\n /** Client ID that owns the token */\n clientId: string;\n\n /** Granted scopes */\n scopes: string[];\n\n /** Token expiration timestamp (milliseconds since epoch) */\n expiresAt: number;\n\n /** Microsoft provider tokens (if available) */\n providerTokens?: ProviderTokens;\n}\n\n/**\n * DCR Token Verifier validates access tokens via Authorization Server\n *\n * This implements proper OAuth 2.0 architecture where the Resource Server\n * (MCP server) validates tokens by calling the Authorization Server's\n * verification endpoint rather than accessing token storage directly.\n */\nexport class DcrTokenVerifier {\n private verifyUrl: string;\n\n /**\n * @param verifyUrl - Authorization Server's /oauth/verify endpoint URL\n */\n constructor(verifyUrl: string) {\n this.verifyUrl = verifyUrl;\n }\n\n /**\n * Verify an access token by calling the Authorization Server\n *\n * @param token - Bearer access token to validate\n * @returns AuthInfo with token metadata and provider tokens\n * @throws Error if token is invalid or verification fails\n */\n async verifyAccessToken(token: string): Promise<AuthInfo> {\n try {\n const response = await fetch(this.verifyUrl, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n let errorMessage = 'Unknown error';\n try {\n const error = await response.json();\n errorMessage = (error as { error_description?: string; error?: string }).error_description ?? (error as { error?: string }).error ?? errorMessage;\n } catch {\n // Failed to parse error JSON, use status text\n errorMessage = response.statusText;\n }\n throw new Error(`Token verification failed: ${errorMessage}`);\n }\n\n const authInfo = (await response.json()) as AuthInfo;\n return authInfo;\n } catch (error) {\n if (error instanceof Error) {\n throw error;\n }\n throw new Error(`Token verification failed: ${String(error)}`);\n }\n }\n}\n"],"names":["DcrTokenVerifier","verifyAccessToken","token","response","fetch","verifyUrl","method","headers","Authorization","ok","errorMessage","error","json","error_description","statusText","Error","authInfo","String"],"mappings":"AAAA;;;;;CAKC,GAwBD;;;;;;CAMC,GACD,OAAO,MAAMA;IAUX;;;;;;GAMC,GACD,MAAMC,kBAAkBC,KAAa,EAAqB;QACxD,IAAI;YACF,MAAMC,WAAW,MAAMC,MAAM,IAAI,CAACC,SAAS,EAAE;gBAC3CC,QAAQ;gBACRC,SAAS;oBACPC,eAAe,CAAC,OAAO,EAAEN,OAAO;gBAClC;YACF;YAEA,IAAI,CAACC,SAASM,EAAE,EAAE;gBAChB,IAAIC,eAAe;gBACnB,IAAI;wBAEa,MAAA;oBADf,MAAMC,QAAQ,MAAMR,SAASS,IAAI;oBACjCF,gBAAe,QAAA,2BAAA,AAACC,MAAyDE,iBAAiB,cAA3E,sCAAA,2BAA+E,AAACF,MAA6BA,KAAK,cAAlH,kBAAA,OAAsHD;gBACvI,EAAE,OAAM;oBACN,8CAA8C;oBAC9CA,eAAeP,SAASW,UAAU;gBACpC;gBACA,MAAM,IAAIC,MAAM,CAAC,2BAA2B,EAAEL,cAAc;YAC9D;YAEA,MAAMM,WAAY,MAAMb,SAASS,IAAI;YACrC,OAAOI;QACT,EAAE,OAAOL,OAAO;YACd,IAAIA,iBAAiBI,OAAO;gBAC1B,MAAMJ;YACR;YACA,MAAM,IAAII,MAAM,CAAC,2BAA2B,EAAEE,OAAON,QAAQ;QAC/D;IACF;IA3CA;;GAEC,GACD,YAAYN,SAAiB,CAAE;QAC7B,IAAI,CAACA,SAAS,GAAGA;IACnB;AAuCF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "type": "module" }
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DCR Provider - Stateless Dynamic Client Registration Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements stateless provider pattern where provider tokens are received from
|
|
5
|
+
* token verification context rather than managed by the provider itself.
|
|
6
|
+
*
|
|
7
|
+
* Use case: MCP HTTP servers with DCR authentication where client manages tokens
|
|
8
|
+
* and provider only handles Microsoft Graph API calls with provided credentials.
|
|
9
|
+
*/
|
|
10
|
+
import type { ProviderTokens } from '@mcp-z/oauth';
|
|
11
|
+
import type { Logger, MicrosoftAuthProvider } from '../types.js';
|
|
12
|
+
/**
|
|
13
|
+
* DCR Provider configuration
|
|
14
|
+
*/
|
|
15
|
+
export interface DcrOAuthProviderConfig {
|
|
16
|
+
/** Microsoft application client ID */
|
|
17
|
+
clientId: string;
|
|
18
|
+
/** Microsoft application client secret (optional for public clients) */
|
|
19
|
+
clientSecret?: string;
|
|
20
|
+
/** Azure AD tenant ID */
|
|
21
|
+
tenantId: string;
|
|
22
|
+
/** OAuth scopes */
|
|
23
|
+
scope: string;
|
|
24
|
+
/** Custom token endpoint URL (for testing, defaults to Microsoft OAuth endpoint) */
|
|
25
|
+
tokenUrl?: string;
|
|
26
|
+
/** DCR token verification endpoint URL (e.g., http://localhost:3000/oauth/verify) */
|
|
27
|
+
verifyEndpoint: string;
|
|
28
|
+
/** Logger for auth operations */
|
|
29
|
+
logger: Logger;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* DCR Provider - Stateless OAuth provider for Dynamic Client Registration
|
|
33
|
+
*
|
|
34
|
+
* Unlike LoopbackOAuthProvider which manages token storage, DcrOAuthProvider is stateless:
|
|
35
|
+
* - Receives provider tokens from verification context (HTTP bearer auth)
|
|
36
|
+
* - Creates auth providers on-demand from tokens
|
|
37
|
+
* - Handles token refresh using Microsoft OAuth
|
|
38
|
+
* - No token storage dependency
|
|
39
|
+
*
|
|
40
|
+
* Pattern:
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const provider = new DcrOAuthProvider(config);
|
|
43
|
+
* const auth = provider.toAuthProvider(providerTokens);
|
|
44
|
+
* const accessToken = await auth.getAccessToken();
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare class DcrOAuthProvider {
|
|
48
|
+
private config;
|
|
49
|
+
private emailCache;
|
|
50
|
+
constructor(config: DcrOAuthProviderConfig);
|
|
51
|
+
/**
|
|
52
|
+
* Create Microsoft Graph auth provider from provider tokens
|
|
53
|
+
*
|
|
54
|
+
* This is the core stateless pattern - provider receives tokens from context
|
|
55
|
+
* (token verification, HTTP request) and creates auth provider on-demand.
|
|
56
|
+
*
|
|
57
|
+
* @param tokens - Provider tokens (Microsoft access/refresh tokens)
|
|
58
|
+
* @returns Microsoft Graph-compatible auth provider
|
|
59
|
+
*/
|
|
60
|
+
toAuthProvider(tokens: ProviderTokens): MicrosoftAuthProvider;
|
|
61
|
+
/**
|
|
62
|
+
* Check if token is still valid (with 1 minute buffer)
|
|
63
|
+
*/
|
|
64
|
+
private isTokenValid;
|
|
65
|
+
/**
|
|
66
|
+
* Refresh Microsoft access token using refresh token
|
|
67
|
+
*
|
|
68
|
+
* @param refreshToken - Microsoft refresh token
|
|
69
|
+
* @returns New provider tokens
|
|
70
|
+
*/
|
|
71
|
+
refreshAccessToken(refreshToken: string): Promise<ProviderTokens>;
|
|
72
|
+
/**
|
|
73
|
+
* Get user email from Microsoft Graph API (with caching)
|
|
74
|
+
*
|
|
75
|
+
* @param tokens - Provider tokens to use for API call
|
|
76
|
+
* @returns User's email address
|
|
77
|
+
*/
|
|
78
|
+
getUserEmail(tokens: ProviderTokens): Promise<string>;
|
|
79
|
+
/**
|
|
80
|
+
* Auth middleware for HTTP servers with DCR bearer auth
|
|
81
|
+
* Validates bearer tokens and enriches extra with provider tokens
|
|
82
|
+
*
|
|
83
|
+
* Pattern:
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const provider = new DcrOAuthProvider({ ..., verifyEndpoint: 'http://localhost:3000/oauth/verify' });
|
|
86
|
+
* const middleware = provider.authMiddleware();
|
|
87
|
+
* const tools = toolFactories.map(f => f()).map(middleware.withToolAuth);
|
|
88
|
+
* const resources = resourceFactories.map(f => f()).map(middleware.withResourceAuth);
|
|
89
|
+
* const prompts = promptFactories.map(f => f()).map(middleware.withPromptAuth);
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
authMiddleware(): {
|
|
93
|
+
withToolAuth: <T extends {
|
|
94
|
+
name: string;
|
|
95
|
+
config: unknown;
|
|
96
|
+
handler: unknown;
|
|
97
|
+
}>(module: T) => T;
|
|
98
|
+
withResourceAuth: <T extends {
|
|
99
|
+
name: string;
|
|
100
|
+
template?: unknown;
|
|
101
|
+
config?: unknown;
|
|
102
|
+
handler: unknown;
|
|
103
|
+
}>(module: T) => T;
|
|
104
|
+
withPromptAuth: <T extends {
|
|
105
|
+
name: string;
|
|
106
|
+
config: unknown;
|
|
107
|
+
handler: unknown;
|
|
108
|
+
}>(module: T) => T;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DCR Provider - Stateless Dynamic Client Registration Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements stateless provider pattern where provider tokens are received from
|
|
5
|
+
* token verification context rather than managed by the provider itself.
|
|
6
|
+
*
|
|
7
|
+
* Use case: MCP HTTP servers with DCR authentication where client manages tokens
|
|
8
|
+
* and provider only handles Microsoft Graph API calls with provided credentials.
|
|
9
|
+
*/ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
import { fetchWithTimeout } from '../lib/fetch-with-timeout.js';
|
|
11
|
+
/**
|
|
12
|
+
* DCR Provider - Stateless OAuth provider for Dynamic Client Registration
|
|
13
|
+
*
|
|
14
|
+
* Unlike LoopbackOAuthProvider which manages token storage, DcrOAuthProvider is stateless:
|
|
15
|
+
* - Receives provider tokens from verification context (HTTP bearer auth)
|
|
16
|
+
* - Creates auth providers on-demand from tokens
|
|
17
|
+
* - Handles token refresh using Microsoft OAuth
|
|
18
|
+
* - No token storage dependency
|
|
19
|
+
*
|
|
20
|
+
* Pattern:
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const provider = new DcrOAuthProvider(config);
|
|
23
|
+
* const auth = provider.toAuthProvider(providerTokens);
|
|
24
|
+
* const accessToken = await auth.getAccessToken();
|
|
25
|
+
* ```
|
|
26
|
+
*/ export class DcrOAuthProvider {
|
|
27
|
+
/**
|
|
28
|
+
* Create Microsoft Graph auth provider from provider tokens
|
|
29
|
+
*
|
|
30
|
+
* This is the core stateless pattern - provider receives tokens from context
|
|
31
|
+
* (token verification, HTTP request) and creates auth provider on-demand.
|
|
32
|
+
*
|
|
33
|
+
* @param tokens - Provider tokens (Microsoft access/refresh tokens)
|
|
34
|
+
* @returns Microsoft Graph-compatible auth provider
|
|
35
|
+
*/ toAuthProvider(tokens) {
|
|
36
|
+
// Capture tokens in closure for auth provider
|
|
37
|
+
let currentTokens = {
|
|
38
|
+
...tokens
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
getAccessToken: async ()=>{
|
|
42
|
+
// Check if token is still valid
|
|
43
|
+
if (this.isTokenValid(currentTokens)) {
|
|
44
|
+
return currentTokens.accessToken;
|
|
45
|
+
}
|
|
46
|
+
// Token expired - try refresh if available
|
|
47
|
+
if (currentTokens.refreshToken) {
|
|
48
|
+
try {
|
|
49
|
+
const refreshedTokens = await this.refreshAccessToken(currentTokens.refreshToken);
|
|
50
|
+
currentTokens = refreshedTokens;
|
|
51
|
+
return currentTokens.accessToken;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(`Token refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// No refresh token - token expired and cannot refresh
|
|
57
|
+
throw new Error('Access token expired and no refresh token available');
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Check if token is still valid (with 1 minute buffer)
|
|
63
|
+
*/ isTokenValid(tokens) {
|
|
64
|
+
if (!tokens.expiresAt) return true; // No expiry = assume valid
|
|
65
|
+
return Date.now() < tokens.expiresAt - 60000; // 1 minute buffer
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Refresh Microsoft access token using refresh token
|
|
69
|
+
*
|
|
70
|
+
* @param refreshToken - Microsoft refresh token
|
|
71
|
+
* @returns New provider tokens
|
|
72
|
+
*/ async refreshAccessToken(refreshToken) {
|
|
73
|
+
const { clientId, clientSecret, tenantId, scope, tokenUrl: customTokenUrl } = this.config;
|
|
74
|
+
const tokenUrl = customTokenUrl !== null && customTokenUrl !== void 0 ? customTokenUrl : `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
|
75
|
+
const params = {
|
|
76
|
+
refresh_token: refreshToken,
|
|
77
|
+
client_id: clientId,
|
|
78
|
+
grant_type: 'refresh_token',
|
|
79
|
+
scope
|
|
80
|
+
};
|
|
81
|
+
// Only include client_secret for confidential clients
|
|
82
|
+
if (clientSecret) {
|
|
83
|
+
params.client_secret = clientSecret;
|
|
84
|
+
}
|
|
85
|
+
const body = new URLSearchParams(params);
|
|
86
|
+
const response = await fetchWithTimeout(tokenUrl, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: {
|
|
89
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
90
|
+
},
|
|
91
|
+
body: body.toString()
|
|
92
|
+
});
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const errorText = await response.text();
|
|
95
|
+
throw new Error(`Token refresh failed: ${response.status} ${errorText}`);
|
|
96
|
+
}
|
|
97
|
+
const tokenResponse = await response.json();
|
|
98
|
+
return {
|
|
99
|
+
accessToken: tokenResponse.access_token,
|
|
100
|
+
refreshToken: refreshToken,
|
|
101
|
+
...tokenResponse.expires_in !== undefined && {
|
|
102
|
+
expiresAt: Date.now() + tokenResponse.expires_in * 1000
|
|
103
|
+
},
|
|
104
|
+
...tokenResponse.scope !== undefined && {
|
|
105
|
+
scope: tokenResponse.scope
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get user email from Microsoft Graph API (with caching)
|
|
111
|
+
*
|
|
112
|
+
* @param tokens - Provider tokens to use for API call
|
|
113
|
+
* @returns User's email address
|
|
114
|
+
*/ async getUserEmail(tokens) {
|
|
115
|
+
var _userInfo_mail, _tokens_expiresAt;
|
|
116
|
+
const cacheKey = tokens.accessToken;
|
|
117
|
+
const cached = this.emailCache.get(cacheKey);
|
|
118
|
+
// Check cache (with same expiry as access token)
|
|
119
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
120
|
+
return cached.email;
|
|
121
|
+
}
|
|
122
|
+
const auth = this.toAuthProvider(tokens);
|
|
123
|
+
const accessToken = await auth.getAccessToken();
|
|
124
|
+
const response = await fetchWithTimeout('https://graph.microsoft.com/v1.0/me', {
|
|
125
|
+
headers: {
|
|
126
|
+
Authorization: `Bearer ${accessToken}`
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
throw new Error(`Failed to get user info: ${response.status} ${await response.text()}`);
|
|
131
|
+
}
|
|
132
|
+
const userInfo = await response.json();
|
|
133
|
+
const email = (_userInfo_mail = userInfo.mail) !== null && _userInfo_mail !== void 0 ? _userInfo_mail : userInfo.userPrincipalName;
|
|
134
|
+
// Cache with token expiration (default 1 hour if not specified)
|
|
135
|
+
this.emailCache.set(cacheKey, {
|
|
136
|
+
email,
|
|
137
|
+
expiresAt: (_tokens_expiresAt = tokens.expiresAt) !== null && _tokens_expiresAt !== void 0 ? _tokens_expiresAt : Date.now() + 3600000
|
|
138
|
+
});
|
|
139
|
+
return email;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Auth middleware for HTTP servers with DCR bearer auth
|
|
143
|
+
* Validates bearer tokens and enriches extra with provider tokens
|
|
144
|
+
*
|
|
145
|
+
* Pattern:
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const provider = new DcrOAuthProvider({ ..., verifyEndpoint: 'http://localhost:3000/oauth/verify' });
|
|
148
|
+
* const middleware = provider.authMiddleware();
|
|
149
|
+
* const tools = toolFactories.map(f => f()).map(middleware.withToolAuth);
|
|
150
|
+
* const resources = resourceFactories.map(f => f()).map(middleware.withResourceAuth);
|
|
151
|
+
* const prompts = promptFactories.map(f => f()).map(middleware.withPromptAuth);
|
|
152
|
+
* ```
|
|
153
|
+
*/ authMiddleware() {
|
|
154
|
+
// Shared wrapper logic - extracts extra parameter from specified position
|
|
155
|
+
// Generic T captures the actual module type; handler is cast from unknown to callable
|
|
156
|
+
const wrapAtPosition = (module, extraPosition)=>{
|
|
157
|
+
const originalHandler = module.handler;
|
|
158
|
+
const wrappedHandler = async (...allArgs)=>{
|
|
159
|
+
var _extra_requestInfo;
|
|
160
|
+
// Extract extra from the correct position
|
|
161
|
+
const extra = allArgs[extraPosition];
|
|
162
|
+
// Extract DCR bearer token from SDK's authInfo (if present) or request headers
|
|
163
|
+
let bearerToken;
|
|
164
|
+
// Option 1: Token already verified by SDK's bearerAuth middleware
|
|
165
|
+
if (extra.authInfo && typeof extra.authInfo === 'object') {
|
|
166
|
+
var _ref;
|
|
167
|
+
// authInfo contains the validated token - extract it
|
|
168
|
+
// The SDK's bearerAuth middleware already validated it, but we need the raw token for /oauth/verify
|
|
169
|
+
// Check if authInfo has the token directly, otherwise extract from headers
|
|
170
|
+
const authInfo = extra.authInfo;
|
|
171
|
+
bearerToken = (_ref = typeof authInfo.accessToken === 'string' ? authInfo.accessToken : undefined) !== null && _ref !== void 0 ? _ref : typeof authInfo.token === 'string' ? authInfo.token : undefined;
|
|
172
|
+
}
|
|
173
|
+
// Option 2: Extract from Authorization header
|
|
174
|
+
if (!bearerToken && ((_extra_requestInfo = extra.requestInfo) === null || _extra_requestInfo === void 0 ? void 0 : _extra_requestInfo.headers)) {
|
|
175
|
+
const authHeader = extra.requestInfo.headers.authorization || extra.requestInfo.headers.Authorization;
|
|
176
|
+
if (authHeader) {
|
|
177
|
+
// Handle both string and string[] types
|
|
178
|
+
const headerValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
179
|
+
if (headerValue) {
|
|
180
|
+
const match = /^Bearer\s+(.+)$/i.exec(headerValue);
|
|
181
|
+
if (match) {
|
|
182
|
+
bearerToken = match[1];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!bearerToken) {
|
|
188
|
+
throw new McpError(ErrorCode.InvalidRequest, 'Missing Authorization header. DCR mode requires bearer token.');
|
|
189
|
+
}
|
|
190
|
+
// Call /oauth/verify to validate DCR token and get provider tokens
|
|
191
|
+
const verifyResponse = await fetchWithTimeout(this.config.verifyEndpoint, {
|
|
192
|
+
headers: {
|
|
193
|
+
Authorization: `Bearer ${bearerToken}`
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
if (!verifyResponse.ok) {
|
|
197
|
+
throw new McpError(ErrorCode.InvalidRequest, `Token verification failed: ${verifyResponse.status}`);
|
|
198
|
+
}
|
|
199
|
+
const verifyData = await verifyResponse.json();
|
|
200
|
+
// Fetch user email to use as accountId (with caching)
|
|
201
|
+
let accountId;
|
|
202
|
+
try {
|
|
203
|
+
accountId = await this.getUserEmail(verifyData.providerTokens);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get user email for DCR authentication: ${error instanceof Error ? error.message : String(error)}`);
|
|
206
|
+
}
|
|
207
|
+
// Create auth provider from provider tokens
|
|
208
|
+
const auth = this.toAuthProvider(verifyData.providerTokens);
|
|
209
|
+
// Inject authContext and logger into extra
|
|
210
|
+
extra.authContext = {
|
|
211
|
+
auth,
|
|
212
|
+
accountId
|
|
213
|
+
};
|
|
214
|
+
extra.logger = this.config.logger;
|
|
215
|
+
// Call original handler with all args
|
|
216
|
+
return await originalHandler(...allArgs);
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
...module,
|
|
220
|
+
handler: wrappedHandler
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
return {
|
|
224
|
+
// Use structural constraints to avoid contravariance check on handler type.
|
|
225
|
+
// wrapAtPosition is now generic and returns T directly.
|
|
226
|
+
withToolAuth: (module)=>wrapAtPosition(module, 1),
|
|
227
|
+
withResourceAuth: (module)=>wrapAtPosition(module, 2),
|
|
228
|
+
withPromptAuth: (module)=>wrapAtPosition(module, 0)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
constructor(config){
|
|
232
|
+
this.emailCache = new Map();
|
|
233
|
+
this.config = config;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/Projects/ai/mcp-z/oauth/oauth-microsoft/src/providers/dcr.ts"],"sourcesContent":["/**\n * DCR Provider - Stateless Dynamic Client Registration Provider\n *\n * Implements stateless provider pattern where provider tokens are received from\n * token verification context rather than managed by the provider itself.\n *\n * Use case: MCP HTTP servers with DCR authentication where client manages tokens\n * and provider only handles Microsoft Graph API calls with provided credentials.\n */\n\nimport type { ProviderTokens } from '@mcp-z/oauth';\nimport { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';\nimport { fetchWithTimeout } from '../lib/fetch-with-timeout.ts';\nimport type { AuthContext, EnrichedExtra, Logger, MicrosoftAuthProvider } from '../types.ts';\n\n/**\n * DCR Provider configuration\n */\nexport interface DcrOAuthProviderConfig {\n /** Microsoft application client ID */\n clientId: string;\n\n /** Microsoft application client secret (optional for public clients) */\n clientSecret?: string;\n\n /** Azure AD tenant ID */\n tenantId: string;\n\n /** OAuth scopes */\n scope: string;\n\n /** Custom token endpoint URL (for testing, defaults to Microsoft OAuth endpoint) */\n tokenUrl?: string;\n\n /** DCR token verification endpoint URL (e.g., http://localhost:3000/oauth/verify) */\n verifyEndpoint: string;\n\n /** Logger for auth operations */\n logger: Logger;\n}\n\n/**\n * Microsoft Graph TokenResponse\n */\ninterface TokenResponse {\n access_token: string;\n refresh_token?: string;\n expires_in?: number;\n scope?: string;\n token_type?: string;\n}\n\n/**\n * DCR Provider - Stateless OAuth provider for Dynamic Client Registration\n *\n * Unlike LoopbackOAuthProvider which manages token storage, DcrOAuthProvider is stateless:\n * - Receives provider tokens from verification context (HTTP bearer auth)\n * - Creates auth providers on-demand from tokens\n * - Handles token refresh using Microsoft OAuth\n * - No token storage dependency\n *\n * Pattern:\n * ```typescript\n * const provider = new DcrOAuthProvider(config);\n * const auth = provider.toAuthProvider(providerTokens);\n * const accessToken = await auth.getAccessToken();\n * ```\n */\nexport class DcrOAuthProvider {\n private config: DcrOAuthProviderConfig;\n private emailCache = new Map<string, { email: string; expiresAt: number }>();\n\n constructor(config: DcrOAuthProviderConfig) {\n this.config = config;\n }\n\n /**\n * Create Microsoft Graph auth provider from provider tokens\n *\n * This is the core stateless pattern - provider receives tokens from context\n * (token verification, HTTP request) and creates auth provider on-demand.\n *\n * @param tokens - Provider tokens (Microsoft access/refresh tokens)\n * @returns Microsoft Graph-compatible auth provider\n */\n toAuthProvider(tokens: ProviderTokens): MicrosoftAuthProvider {\n // Capture tokens in closure for auth provider\n let currentTokens = { ...tokens };\n\n return {\n getAccessToken: async (): Promise<string> => {\n // Check if token is still valid\n if (this.isTokenValid(currentTokens)) {\n return currentTokens.accessToken;\n }\n\n // Token expired - try refresh if available\n if (currentTokens.refreshToken) {\n try {\n const refreshedTokens = await this.refreshAccessToken(currentTokens.refreshToken);\n currentTokens = refreshedTokens;\n return currentTokens.accessToken;\n } catch (error) {\n throw new Error(`Token refresh failed: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n // No refresh token - token expired and cannot refresh\n throw new Error('Access token expired and no refresh token available');\n },\n };\n }\n\n /**\n * Check if token is still valid (with 1 minute buffer)\n */\n private isTokenValid(tokens: ProviderTokens): boolean {\n if (!tokens.expiresAt) return true; // No expiry = assume valid\n return Date.now() < tokens.expiresAt - 60000; // 1 minute buffer\n }\n\n /**\n * Refresh Microsoft access token using refresh token\n *\n * @param refreshToken - Microsoft refresh token\n * @returns New provider tokens\n */\n async refreshAccessToken(refreshToken: string): Promise<ProviderTokens> {\n const { clientId, clientSecret, tenantId, scope, tokenUrl: customTokenUrl } = this.config;\n\n const tokenUrl = customTokenUrl ?? `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;\n const params: Record<string, string> = {\n refresh_token: refreshToken,\n client_id: clientId,\n grant_type: 'refresh_token',\n scope,\n };\n\n // Only include client_secret for confidential clients\n if (clientSecret) {\n params.client_secret = clientSecret;\n }\n\n const body = new URLSearchParams(params);\n\n const response = await fetchWithTimeout(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed: ${response.status} ${errorText}`);\n }\n\n const tokenResponse = (await response.json()) as TokenResponse;\n\n return {\n accessToken: tokenResponse.access_token,\n refreshToken: refreshToken, // Keep original refresh token\n ...(tokenResponse.expires_in !== undefined && { expiresAt: Date.now() + tokenResponse.expires_in * 1000 }),\n ...(tokenResponse.scope !== undefined && { scope: tokenResponse.scope }),\n };\n }\n\n /**\n * Get user email from Microsoft Graph API (with caching)\n *\n * @param tokens - Provider tokens to use for API call\n * @returns User's email address\n */\n async getUserEmail(tokens: ProviderTokens): Promise<string> {\n const cacheKey = tokens.accessToken;\n const cached = this.emailCache.get(cacheKey);\n\n // Check cache (with same expiry as access token)\n if (cached && Date.now() < cached.expiresAt) {\n return cached.email;\n }\n\n const auth = this.toAuthProvider(tokens);\n const accessToken = await auth.getAccessToken();\n\n const response = await fetchWithTimeout('https://graph.microsoft.com/v1.0/me', {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to get user info: ${response.status} ${await response.text()}`);\n }\n\n const userInfo = (await response.json()) as { mail?: string; userPrincipalName: string };\n const email = userInfo.mail ?? userInfo.userPrincipalName;\n\n // Cache with token expiration (default 1 hour if not specified)\n this.emailCache.set(cacheKey, {\n email,\n expiresAt: tokens.expiresAt ?? Date.now() + 3600000,\n });\n\n return email;\n }\n\n /**\n * Auth middleware for HTTP servers with DCR bearer auth\n * Validates bearer tokens and enriches extra with provider tokens\n *\n * Pattern:\n * ```typescript\n * const provider = new DcrOAuthProvider({ ..., verifyEndpoint: 'http://localhost:3000/oauth/verify' });\n * const middleware = provider.authMiddleware();\n * const tools = toolFactories.map(f => f()).map(middleware.withToolAuth);\n * const resources = resourceFactories.map(f => f()).map(middleware.withResourceAuth);\n * const prompts = promptFactories.map(f => f()).map(middleware.withPromptAuth);\n * ```\n */\n authMiddleware() {\n // Shared wrapper logic - extracts extra parameter from specified position\n // Generic T captures the actual module type; handler is cast from unknown to callable\n const wrapAtPosition = <T extends { name: string; handler: unknown; [key: string]: unknown }>(module: T, extraPosition: number): T => {\n const originalHandler = module.handler as (...args: unknown[]) => Promise<unknown>;\n\n const wrappedHandler = async (...allArgs: unknown[]) => {\n // Extract extra from the correct position\n const extra = allArgs[extraPosition] as EnrichedExtra;\n\n // Extract DCR bearer token from SDK's authInfo (if present) or request headers\n let bearerToken: string | undefined;\n\n // Option 1: Token already verified by SDK's bearerAuth middleware\n if (extra.authInfo && typeof extra.authInfo === 'object') {\n // authInfo contains the validated token - extract it\n // The SDK's bearerAuth middleware already validated it, but we need the raw token for /oauth/verify\n // Check if authInfo has the token directly, otherwise extract from headers\n const authInfo = extra.authInfo as unknown as Record<string, unknown>;\n bearerToken = (typeof authInfo.accessToken === 'string' ? authInfo.accessToken : undefined) ?? (typeof authInfo.token === 'string' ? authInfo.token : undefined);\n }\n\n // Option 2: Extract from Authorization header\n if (!bearerToken && extra.requestInfo?.headers) {\n const authHeader = extra.requestInfo.headers.authorization || extra.requestInfo.headers.Authorization;\n if (authHeader) {\n // Handle both string and string[] types\n const headerValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;\n if (headerValue) {\n const match = /^Bearer\\s+(.+)$/i.exec(headerValue);\n if (match) {\n bearerToken = match[1];\n }\n }\n }\n }\n\n if (!bearerToken) {\n throw new McpError(ErrorCode.InvalidRequest, 'Missing Authorization header. DCR mode requires bearer token.');\n }\n\n // Call /oauth/verify to validate DCR token and get provider tokens\n const verifyResponse = await fetchWithTimeout(this.config.verifyEndpoint, {\n headers: { Authorization: `Bearer ${bearerToken}` },\n });\n\n if (!verifyResponse.ok) {\n throw new McpError(ErrorCode.InvalidRequest, `Token verification failed: ${verifyResponse.status}`);\n }\n\n const verifyData = (await verifyResponse.json()) as {\n providerTokens: ProviderTokens;\n };\n\n // Fetch user email to use as accountId (with caching)\n let accountId: string;\n try {\n accountId = await this.getUserEmail(verifyData.providerTokens);\n } catch (error) {\n throw new McpError(ErrorCode.InternalError, `Failed to get user email for DCR authentication: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n // Create auth provider from provider tokens\n const auth = this.toAuthProvider(verifyData.providerTokens);\n\n // Inject authContext and logger into extra\n (extra as { authContext?: AuthContext }).authContext = {\n auth,\n accountId, // User's email address\n };\n (extra as { logger?: unknown }).logger = this.config.logger;\n\n // Call original handler with all args\n return await originalHandler(...allArgs);\n };\n\n return {\n ...module,\n handler: wrappedHandler,\n } as T;\n };\n\n return {\n // Use structural constraints to avoid contravariance check on handler type.\n // wrapAtPosition is now generic and returns T directly.\n withToolAuth: <T extends { name: string; config: unknown; handler: unknown }>(module: T) => wrapAtPosition(module, 1),\n withResourceAuth: <T extends { name: string; template?: unknown; config?: unknown; handler: unknown }>(module: T) => wrapAtPosition(module, 2),\n withPromptAuth: <T extends { name: string; config: unknown; handler: unknown }>(module: T) => wrapAtPosition(module, 0),\n };\n }\n}\n"],"names":["ErrorCode","McpError","fetchWithTimeout","DcrOAuthProvider","toAuthProvider","tokens","currentTokens","getAccessToken","isTokenValid","accessToken","refreshToken","refreshedTokens","refreshAccessToken","error","Error","message","String","expiresAt","Date","now","clientId","clientSecret","tenantId","scope","tokenUrl","customTokenUrl","config","params","refresh_token","client_id","grant_type","client_secret","body","URLSearchParams","response","method","headers","toString","ok","errorText","text","status","tokenResponse","json","access_token","expires_in","undefined","getUserEmail","userInfo","cacheKey","cached","emailCache","get","email","auth","Authorization","mail","userPrincipalName","set","authMiddleware","wrapAtPosition","module","extraPosition","originalHandler","handler","wrappedHandler","allArgs","extra","bearerToken","authInfo","token","requestInfo","authHeader","authorization","headerValue","Array","isArray","match","exec","InvalidRequest","verifyResponse","verifyEndpoint","verifyData","accountId","providerTokens","InternalError","authContext","logger","withToolAuth","withResourceAuth","withPromptAuth","Map"],"mappings":"AAAA;;;;;;;;CAQC,GAGD,SAASA,SAAS,EAAEC,QAAQ,QAAQ,qCAAqC;AACzE,SAASC,gBAAgB,QAAQ,+BAA+B;AAwChE;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAMC;IAQX;;;;;;;;GAQC,GACDC,eAAeC,MAAsB,EAAyB;QAC5D,8CAA8C;QAC9C,IAAIC,gBAAgB;YAAE,GAAGD,MAAM;QAAC;QAEhC,OAAO;YACLE,gBAAgB;gBACd,gCAAgC;gBAChC,IAAI,IAAI,CAACC,YAAY,CAACF,gBAAgB;oBACpC,OAAOA,cAAcG,WAAW;gBAClC;gBAEA,2CAA2C;gBAC3C,IAAIH,cAAcI,YAAY,EAAE;oBAC9B,IAAI;wBACF,MAAMC,kBAAkB,MAAM,IAAI,CAACC,kBAAkB,CAACN,cAAcI,YAAY;wBAChFJ,gBAAgBK;wBAChB,OAAOL,cAAcG,WAAW;oBAClC,EAAE,OAAOI,OAAO;wBACd,MAAM,IAAIC,MAAM,CAAC,sBAAsB,EAAED,iBAAiBC,QAAQD,MAAME,OAAO,GAAGC,OAAOH,QAAQ;oBACnG;gBACF;gBAEA,sDAAsD;gBACtD,MAAM,IAAIC,MAAM;YAClB;QACF;IACF;IAEA;;GAEC,GACD,AAAQN,aAAaH,MAAsB,EAAW;QACpD,IAAI,CAACA,OAAOY,SAAS,EAAE,OAAO,MAAM,2BAA2B;QAC/D,OAAOC,KAAKC,GAAG,KAAKd,OAAOY,SAAS,GAAG,OAAO,kBAAkB;IAClE;IAEA;;;;;GAKC,GACD,MAAML,mBAAmBF,YAAoB,EAA2B;QACtE,MAAM,EAAEU,QAAQ,EAAEC,YAAY,EAAEC,QAAQ,EAAEC,KAAK,EAAEC,UAAUC,cAAc,EAAE,GAAG,IAAI,CAACC,MAAM;QAEzF,MAAMF,WAAWC,2BAAAA,4BAAAA,iBAAkB,CAAC,kCAAkC,EAAEH,SAAS,kBAAkB,CAAC;QACpG,MAAMK,SAAiC;YACrCC,eAAelB;YACfmB,WAAWT;YACXU,YAAY;YACZP;QACF;QAEA,sDAAsD;QACtD,IAAIF,cAAc;YAChBM,OAAOI,aAAa,GAAGV;QACzB;QAEA,MAAMW,OAAO,IAAIC,gBAAgBN;QAEjC,MAAMO,WAAW,MAAMhC,iBAAiBsB,UAAU;YAChDW,QAAQ;YACRC,SAAS;gBACP,gBAAgB;YAClB;YACAJ,MAAMA,KAAKK,QAAQ;QACrB;QAEA,IAAI,CAACH,SAASI,EAAE,EAAE;YAChB,MAAMC,YAAY,MAAML,SAASM,IAAI;YACrC,MAAM,IAAI1B,MAAM,CAAC,sBAAsB,EAAEoB,SAASO,MAAM,CAAC,CAAC,EAAEF,WAAW;QACzE;QAEA,MAAMG,gBAAiB,MAAMR,SAASS,IAAI;QAE1C,OAAO;YACLlC,aAAaiC,cAAcE,YAAY;YACvClC,cAAcA;YACd,GAAIgC,cAAcG,UAAU,KAAKC,aAAa;gBAAE7B,WAAWC,KAAKC,GAAG,KAAKuB,cAAcG,UAAU,GAAG;YAAK,CAAC;YACzG,GAAIH,cAAcnB,KAAK,KAAKuB,aAAa;gBAAEvB,OAAOmB,cAAcnB,KAAK;YAAC,CAAC;QACzE;IACF;IAEA;;;;;GAKC,GACD,MAAMwB,aAAa1C,MAAsB,EAAmB;YAuB5C2C,gBAKD3C;QA3Bb,MAAM4C,WAAW5C,OAAOI,WAAW;QACnC,MAAMyC,SAAS,IAAI,CAACC,UAAU,CAACC,GAAG,CAACH;QAEnC,iDAAiD;QACjD,IAAIC,UAAUhC,KAAKC,GAAG,KAAK+B,OAAOjC,SAAS,EAAE;YAC3C,OAAOiC,OAAOG,KAAK;QACrB;QAEA,MAAMC,OAAO,IAAI,CAAClD,cAAc,CAACC;QACjC,MAAMI,cAAc,MAAM6C,KAAK/C,cAAc;QAE7C,MAAM2B,WAAW,MAAMhC,iBAAiB,uCAAuC;YAC7EkC,SAAS;gBACPmB,eAAe,CAAC,OAAO,EAAE9C,aAAa;YACxC;QACF;QAEA,IAAI,CAACyB,SAASI,EAAE,EAAE;YAChB,MAAM,IAAIxB,MAAM,CAAC,yBAAyB,EAAEoB,SAASO,MAAM,CAAC,CAAC,EAAE,MAAMP,SAASM,IAAI,IAAI;QACxF;QAEA,MAAMQ,WAAY,MAAMd,SAASS,IAAI;QACrC,MAAMU,SAAQL,iBAAAA,SAASQ,IAAI,cAAbR,4BAAAA,iBAAiBA,SAASS,iBAAiB;QAEzD,gEAAgE;QAChE,IAAI,CAACN,UAAU,CAACO,GAAG,CAACT,UAAU;YAC5BI;YACApC,SAAS,GAAEZ,oBAAAA,OAAOY,SAAS,cAAhBZ,+BAAAA,oBAAoBa,KAAKC,GAAG,KAAK;QAC9C;QAEA,OAAOkC;IACT;IAEA;;;;;;;;;;;;GAYC,GACDM,iBAAiB;QACf,0EAA0E;QAC1E,sFAAsF;QACtF,MAAMC,iBAAiB,CAAuEC,QAAWC;YACvG,MAAMC,kBAAkBF,OAAOG,OAAO;YAEtC,MAAMC,iBAAiB,OAAO,GAAGC;oBAiBXC;gBAhBpB,0CAA0C;gBAC1C,MAAMA,QAAQD,OAAO,CAACJ,cAAc;gBAEpC,+EAA+E;gBAC/E,IAAIM;gBAEJ,kEAAkE;gBAClE,IAAID,MAAME,QAAQ,IAAI,OAAOF,MAAME,QAAQ,KAAK,UAAU;wBAKzC;oBAJf,qDAAqD;oBACrD,oGAAoG;oBACpG,2EAA2E;oBAC3E,MAAMA,WAAWF,MAAME,QAAQ;oBAC/BD,eAAe,OAAA,OAAOC,SAAS5D,WAAW,KAAK,WAAW4D,SAAS5D,WAAW,GAAGqC,uBAAlE,kBAAA,OAAiF,OAAOuB,SAASC,KAAK,KAAK,WAAWD,SAASC,KAAK,GAAGxB;gBACxJ;gBAEA,8CAA8C;gBAC9C,IAAI,CAACsB,iBAAeD,qBAAAA,MAAMI,WAAW,cAAjBJ,yCAAAA,mBAAmB/B,OAAO,GAAE;oBAC9C,MAAMoC,aAAaL,MAAMI,WAAW,CAACnC,OAAO,CAACqC,aAAa,IAAIN,MAAMI,WAAW,CAACnC,OAAO,CAACmB,aAAa;oBACrG,IAAIiB,YAAY;wBACd,wCAAwC;wBACxC,MAAME,cAAcC,MAAMC,OAAO,CAACJ,cAAcA,UAAU,CAAC,EAAE,GAAGA;wBAChE,IAAIE,aAAa;4BACf,MAAMG,QAAQ,mBAAmBC,IAAI,CAACJ;4BACtC,IAAIG,OAAO;gCACTT,cAAcS,KAAK,CAAC,EAAE;4BACxB;wBACF;oBACF;gBACF;gBAEA,IAAI,CAACT,aAAa;oBAChB,MAAM,IAAInE,SAASD,UAAU+E,cAAc,EAAE;gBAC/C;gBAEA,mEAAmE;gBACnE,MAAMC,iBAAiB,MAAM9E,iBAAiB,IAAI,CAACwB,MAAM,CAACuD,cAAc,EAAE;oBACxE7C,SAAS;wBAAEmB,eAAe,CAAC,OAAO,EAAEa,aAAa;oBAAC;gBACpD;gBAEA,IAAI,CAACY,eAAe1C,EAAE,EAAE;oBACtB,MAAM,IAAIrC,SAASD,UAAU+E,cAAc,EAAE,CAAC,2BAA2B,EAAEC,eAAevC,MAAM,EAAE;gBACpG;gBAEA,MAAMyC,aAAc,MAAMF,eAAerC,IAAI;gBAI7C,sDAAsD;gBACtD,IAAIwC;gBACJ,IAAI;oBACFA,YAAY,MAAM,IAAI,CAACpC,YAAY,CAACmC,WAAWE,cAAc;gBAC/D,EAAE,OAAOvE,OAAO;oBACd,MAAM,IAAIZ,SAASD,UAAUqF,aAAa,EAAE,CAAC,iDAAiD,EAAExE,iBAAiBC,QAAQD,MAAME,OAAO,GAAGC,OAAOH,QAAQ;gBAC1J;gBAEA,4CAA4C;gBAC5C,MAAMyC,OAAO,IAAI,CAAClD,cAAc,CAAC8E,WAAWE,cAAc;gBAE1D,2CAA2C;gBAC1CjB,MAAwCmB,WAAW,GAAG;oBACrDhC;oBACA6B;gBACF;gBACChB,MAA+BoB,MAAM,GAAG,IAAI,CAAC7D,MAAM,CAAC6D,MAAM;gBAE3D,sCAAsC;gBACtC,OAAO,MAAMxB,mBAAmBG;YAClC;YAEA,OAAO;gBACL,GAAGL,MAAM;gBACTG,SAASC;YACX;QACF;QAEA,OAAO;YACL,4EAA4E;YAC5E,wDAAwD;YACxDuB,cAAc,CAAgE3B,SAAcD,eAAeC,QAAQ;YACnH4B,kBAAkB,CAAqF5B,SAAcD,eAAeC,QAAQ;YAC5I6B,gBAAgB,CAAgE7B,SAAcD,eAAeC,QAAQ;QACvH;IACF;IA9OA,YAAYnC,MAA8B,CAAE;aAFpCyB,aAAa,IAAIwC;QAGvB,IAAI,CAACjE,MAAM,GAAGA;IAChB;AA6OF"}
|