@mcp-z/oauth-google 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 +93 -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 +1189 -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 +107 -0
- package/dist/cjs/providers/dcr.d.ts +107 -0
- package/dist/cjs/providers/dcr.js +584 -0
- package/dist/cjs/providers/dcr.js.map +1 -0
- package/dist/cjs/providers/loopback-oauth.d.cts +119 -0
- package/dist/cjs/providers/loopback-oauth.d.ts +119 -0
- package/dist/cjs/providers/loopback-oauth.js +1334 -0
- package/dist/cjs/providers/loopback-oauth.js.map +1 -0
- package/dist/cjs/providers/service-account.d.cts +131 -0
- package/dist/cjs/providers/service-account.d.ts +131 -0
- package/dist/cjs/providers/service-account.js +800 -0
- package/dist/cjs/providers/service-account.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 +112 -0
- package/dist/cjs/setup/config.d.ts +112 -0
- package/dist/cjs/setup/config.js +236 -0
- package/dist/cjs/setup/config.js.map +1 -0
- package/dist/cjs/types.d.cts +173 -0
- package/dist/cjs/types.d.ts +173 -0
- package/dist/cjs/types.js +16 -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 +515 -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 +107 -0
- package/dist/esm/providers/dcr.js +242 -0
- package/dist/esm/providers/dcr.js.map +1 -0
- package/dist/esm/providers/loopback-oauth.d.ts +119 -0
- package/dist/esm/providers/loopback-oauth.js +639 -0
- package/dist/esm/providers/loopback-oauth.js.map +1 -0
- package/dist/esm/providers/service-account.d.ts +131 -0
- package/dist/esm/providers/service-account.js +353 -0
- package/dist/esm/providers/service-account.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 +112 -0
- package/dist/esm/setup/config.js +258 -0
- package/dist/esm/setup/config.js.map +1 -0
- package/dist/esm/types.d.ts +173 -0
- package/dist/esm/types.js +6 -0
- package/dist/esm/types.js.map +1 -0
- package/package.json +89 -0
|
@@ -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
|
+
/** Google 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-google/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 /** Google 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,107 @@
|
|
|
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 Google API calls with provided credentials.
|
|
9
|
+
*/
|
|
10
|
+
import type { ProviderTokens } from '@mcp-z/oauth';
|
|
11
|
+
import { OAuth2Client } from 'google-auth-library';
|
|
12
|
+
import type { Logger } from '../types.js';
|
|
13
|
+
/**
|
|
14
|
+
* DCR Provider configuration
|
|
15
|
+
*/
|
|
16
|
+
export interface DcrOAuthProviderConfig {
|
|
17
|
+
/** Google application client ID */
|
|
18
|
+
clientId: string;
|
|
19
|
+
/** Google application client secret (optional for public clients) */
|
|
20
|
+
clientSecret?: string;
|
|
21
|
+
/** OAuth scopes */
|
|
22
|
+
scope: string;
|
|
23
|
+
/** DCR token verification endpoint URL (e.g., http://localhost:3000/oauth/verify) */
|
|
24
|
+
verifyEndpoint: string;
|
|
25
|
+
/** Logger for auth operations */
|
|
26
|
+
logger: Logger;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* DCR Provider - Stateless OAuth provider for Dynamic Client Registration
|
|
30
|
+
*
|
|
31
|
+
* Unlike LoopbackOAuthProvider which manages token storage, DcrOAuthProvider is stateless:
|
|
32
|
+
* - Receives provider tokens from verification context (HTTP bearer auth)
|
|
33
|
+
* - Creates auth providers on-demand from tokens
|
|
34
|
+
* - Handles token refresh using Google OAuth 2.0
|
|
35
|
+
* - No token storage dependency
|
|
36
|
+
*
|
|
37
|
+
* Pattern:
|
|
38
|
+
* ```typescript
|
|
39
|
+
* const provider = new DcrOAuthProvider(config);
|
|
40
|
+
* const auth = provider.toAuth(providerTokens);
|
|
41
|
+
* const accessToken = await getAccessToken(auth);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare class DcrOAuthProvider {
|
|
45
|
+
private config;
|
|
46
|
+
private emailCache;
|
|
47
|
+
constructor(config: DcrOAuthProviderConfig);
|
|
48
|
+
/**
|
|
49
|
+
* Create Google OAuth2Client from provider tokens
|
|
50
|
+
*
|
|
51
|
+
* This is the core stateless pattern - provider receives tokens from context
|
|
52
|
+
* (token verification, HTTP request) and creates OAuth2Client on-demand.
|
|
53
|
+
*
|
|
54
|
+
* @param tokens - Provider tokens (Google access/refresh tokens)
|
|
55
|
+
* @returns Google OAuth2Client configured with credentials
|
|
56
|
+
*/
|
|
57
|
+
toAuth(tokens: ProviderTokens): OAuth2Client;
|
|
58
|
+
/**
|
|
59
|
+
* Check if token needs refresh (with 1 minute buffer)
|
|
60
|
+
*/
|
|
61
|
+
private needsRefresh;
|
|
62
|
+
/**
|
|
63
|
+
* Refresh Google access token using refresh token
|
|
64
|
+
*
|
|
65
|
+
* @param refreshToken - Google refresh token
|
|
66
|
+
* @returns New provider tokens
|
|
67
|
+
*/
|
|
68
|
+
refreshAccessToken(refreshToken: string): Promise<ProviderTokens>;
|
|
69
|
+
/**
|
|
70
|
+
* Get user email from Google userinfo API (with caching)
|
|
71
|
+
*
|
|
72
|
+
* @param tokens - Provider tokens to use for API call
|
|
73
|
+
* @returns User's email address
|
|
74
|
+
*/
|
|
75
|
+
getUserEmail(tokens: ProviderTokens): Promise<string>;
|
|
76
|
+
/**
|
|
77
|
+
* Auth middleware for HTTP servers with DCR bearer auth
|
|
78
|
+
* Validates bearer tokens and enriches extra with provider tokens
|
|
79
|
+
*
|
|
80
|
+
* Pattern:
|
|
81
|
+
* ```typescript
|
|
82
|
+
* const provider = new DcrOAuthProvider({ ..., verifyEndpoint: 'http://localhost:3000/oauth/verify' });
|
|
83
|
+
* const authMiddleware = provider.authMiddleware();
|
|
84
|
+
* const tools = toolFactories.map(f => f()).map(authMiddleware.withToolAuth);
|
|
85
|
+
* const resources = resourceFactories.map(f => f()).map(authMiddleware.withResourceAuth);
|
|
86
|
+
* const prompts = promptFactories.map(f => f()).map(authMiddleware.withPromptAuth);
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
authMiddleware(): {
|
|
90
|
+
withToolAuth: <T extends {
|
|
91
|
+
name: string;
|
|
92
|
+
config: unknown;
|
|
93
|
+
handler: unknown;
|
|
94
|
+
}>(module: T) => T;
|
|
95
|
+
withResourceAuth: <T extends {
|
|
96
|
+
name: string;
|
|
97
|
+
template?: unknown;
|
|
98
|
+
config?: unknown;
|
|
99
|
+
handler: unknown;
|
|
100
|
+
}>(module: T) => T;
|
|
101
|
+
withPromptAuth: <T extends {
|
|
102
|
+
name: string;
|
|
103
|
+
config: unknown;
|
|
104
|
+
handler: unknown;
|
|
105
|
+
}>(module: T) => T;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
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 Google API calls with provided credentials.
|
|
9
|
+
*/ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
import { OAuth2Client } from 'google-auth-library';
|
|
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 Google OAuth 2.0
|
|
18
|
+
* - No token storage dependency
|
|
19
|
+
*
|
|
20
|
+
* Pattern:
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const provider = new DcrOAuthProvider(config);
|
|
23
|
+
* const auth = provider.toAuth(providerTokens);
|
|
24
|
+
* const accessToken = await getAccessToken(auth);
|
|
25
|
+
* ```
|
|
26
|
+
*/ export class DcrOAuthProvider {
|
|
27
|
+
/**
|
|
28
|
+
* Create Google OAuth2Client from provider tokens
|
|
29
|
+
*
|
|
30
|
+
* This is the core stateless pattern - provider receives tokens from context
|
|
31
|
+
* (token verification, HTTP request) and creates OAuth2Client on-demand.
|
|
32
|
+
*
|
|
33
|
+
* @param tokens - Provider tokens (Google access/refresh tokens)
|
|
34
|
+
* @returns Google OAuth2Client configured with credentials
|
|
35
|
+
*/ toAuth(tokens) {
|
|
36
|
+
var _tokens_refreshToken, _tokens_expiresAt;
|
|
37
|
+
const { clientId, clientSecret } = this.config;
|
|
38
|
+
// Create OAuth2Client with credentials
|
|
39
|
+
const client = new OAuth2Client({
|
|
40
|
+
clientId,
|
|
41
|
+
...clientSecret && {
|
|
42
|
+
clientSecret
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
// Set initial credentials (convert undefined to null for Google's Credentials type)
|
|
46
|
+
client.credentials = {
|
|
47
|
+
access_token: tokens.accessToken,
|
|
48
|
+
refresh_token: (_tokens_refreshToken = tokens.refreshToken) !== null && _tokens_refreshToken !== void 0 ? _tokens_refreshToken : null,
|
|
49
|
+
expiry_date: (_tokens_expiresAt = tokens.expiresAt) !== null && _tokens_expiresAt !== void 0 ? _tokens_expiresAt : null,
|
|
50
|
+
token_type: 'Bearer'
|
|
51
|
+
};
|
|
52
|
+
// Override getRequestMetadataAsync to handle token refresh
|
|
53
|
+
// @ts-expect-error - Access protected method for token refresh
|
|
54
|
+
const originalGetMetadata = client.getRequestMetadataAsync.bind(client);
|
|
55
|
+
// @ts-expect-error - Override protected method for token refresh
|
|
56
|
+
client.getRequestMetadataAsync = async (url)=>{
|
|
57
|
+
// Check if token needs refresh
|
|
58
|
+
if (this.needsRefresh(client.credentials.expiry_date)) {
|
|
59
|
+
try {
|
|
60
|
+
// Use built-in refresh mechanism
|
|
61
|
+
const refreshedTokens = await client.refreshAccessToken();
|
|
62
|
+
client.credentials = refreshedTokens.credentials;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(`Token refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return originalGetMetadata(url);
|
|
68
|
+
};
|
|
69
|
+
return client;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Check if token needs refresh (with 1 minute buffer)
|
|
73
|
+
*/ needsRefresh(expiryDate) {
|
|
74
|
+
if (!expiryDate) return false; // No expiry = no refresh needed
|
|
75
|
+
return Date.now() >= expiryDate - 60000; // 1 minute buffer
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Refresh Google access token using refresh token
|
|
79
|
+
*
|
|
80
|
+
* @param refreshToken - Google refresh token
|
|
81
|
+
* @returns New provider tokens
|
|
82
|
+
*/ async refreshAccessToken(refreshToken) {
|
|
83
|
+
const { clientId, clientSecret } = this.config;
|
|
84
|
+
const tokenUrl = 'https://oauth2.googleapis.com/token';
|
|
85
|
+
const params = {
|
|
86
|
+
refresh_token: refreshToken,
|
|
87
|
+
client_id: clientId,
|
|
88
|
+
grant_type: 'refresh_token'
|
|
89
|
+
};
|
|
90
|
+
// Only include client_secret for confidential clients
|
|
91
|
+
if (clientSecret) {
|
|
92
|
+
params.client_secret = clientSecret;
|
|
93
|
+
}
|
|
94
|
+
const body = new URLSearchParams(params);
|
|
95
|
+
const response = await fetch(tokenUrl, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: {
|
|
98
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
99
|
+
},
|
|
100
|
+
body: body.toString()
|
|
101
|
+
});
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
const errorText = await response.text();
|
|
104
|
+
throw new Error(`Token refresh failed: ${response.status} ${errorText}`);
|
|
105
|
+
}
|
|
106
|
+
const tokenResponse = await response.json();
|
|
107
|
+
const result = {
|
|
108
|
+
accessToken: tokenResponse.access_token,
|
|
109
|
+
refreshToken: refreshToken
|
|
110
|
+
};
|
|
111
|
+
// Only add optional fields if they have values
|
|
112
|
+
if (tokenResponse.expires_in !== undefined) {
|
|
113
|
+
result.expiresAt = Date.now() + tokenResponse.expires_in * 1000;
|
|
114
|
+
}
|
|
115
|
+
if (tokenResponse.scope !== undefined) {
|
|
116
|
+
result.scope = tokenResponse.scope;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get user email from Google userinfo API (with caching)
|
|
122
|
+
*
|
|
123
|
+
* @param tokens - Provider tokens to use for API call
|
|
124
|
+
* @returns User's email address
|
|
125
|
+
*/ async getUserEmail(tokens) {
|
|
126
|
+
var _tokens_expiresAt;
|
|
127
|
+
const cacheKey = tokens.accessToken;
|
|
128
|
+
const cached = this.emailCache.get(cacheKey);
|
|
129
|
+
// Check cache (with same expiry as access token)
|
|
130
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
131
|
+
return cached.email;
|
|
132
|
+
}
|
|
133
|
+
const auth = this.toAuth(tokens);
|
|
134
|
+
// Use OAuth2Client to make authenticated request
|
|
135
|
+
const response = await auth.request({
|
|
136
|
+
url: 'https://www.googleapis.com/oauth2/v2/userinfo',
|
|
137
|
+
method: 'GET'
|
|
138
|
+
});
|
|
139
|
+
const userInfo = response.data;
|
|
140
|
+
const email = userInfo.email;
|
|
141
|
+
// Cache with token expiration (default 1 hour if not specified)
|
|
142
|
+
this.emailCache.set(cacheKey, {
|
|
143
|
+
email,
|
|
144
|
+
expiresAt: (_tokens_expiresAt = tokens.expiresAt) !== null && _tokens_expiresAt !== void 0 ? _tokens_expiresAt : Date.now() + 3600000
|
|
145
|
+
});
|
|
146
|
+
return email;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Auth middleware for HTTP servers with DCR bearer auth
|
|
150
|
+
* Validates bearer tokens and enriches extra with provider tokens
|
|
151
|
+
*
|
|
152
|
+
* Pattern:
|
|
153
|
+
* ```typescript
|
|
154
|
+
* const provider = new DcrOAuthProvider({ ..., verifyEndpoint: 'http://localhost:3000/oauth/verify' });
|
|
155
|
+
* const authMiddleware = provider.authMiddleware();
|
|
156
|
+
* const tools = toolFactories.map(f => f()).map(authMiddleware.withToolAuth);
|
|
157
|
+
* const resources = resourceFactories.map(f => f()).map(authMiddleware.withResourceAuth);
|
|
158
|
+
* const prompts = promptFactories.map(f => f()).map(authMiddleware.withPromptAuth);
|
|
159
|
+
* ```
|
|
160
|
+
*/ authMiddleware() {
|
|
161
|
+
// Shared wrapper logic - extracts extra parameter from specified position
|
|
162
|
+
// Generic T captures the actual module type; handler is cast from unknown to callable
|
|
163
|
+
const wrapAtPosition = (module, extraPosition)=>{
|
|
164
|
+
const originalHandler = module.handler;
|
|
165
|
+
const wrappedHandler = async (...allArgs)=>{
|
|
166
|
+
var _extra_requestInfo;
|
|
167
|
+
// Extract extra from the correct position
|
|
168
|
+
const extra = allArgs[extraPosition];
|
|
169
|
+
// Extract DCR bearer token from SDK's authInfo (if present) or request headers
|
|
170
|
+
let bearerToken;
|
|
171
|
+
// Option 1: Token already verified by SDK's bearerAuth middleware
|
|
172
|
+
if (extra.authInfo && typeof extra.authInfo === 'object') {
|
|
173
|
+
var _ref;
|
|
174
|
+
// authInfo contains the validated token - extract it
|
|
175
|
+
// The SDK's bearerAuth middleware already validated it, but we need the raw token for /oauth/verify
|
|
176
|
+
// Check if authInfo has the token directly, otherwise extract from headers
|
|
177
|
+
const authInfo = extra.authInfo;
|
|
178
|
+
bearerToken = (_ref = typeof authInfo.accessToken === 'string' ? authInfo.accessToken : undefined) !== null && _ref !== void 0 ? _ref : typeof authInfo.token === 'string' ? authInfo.token : undefined;
|
|
179
|
+
}
|
|
180
|
+
// Option 2: Extract from Authorization header
|
|
181
|
+
if (!bearerToken && ((_extra_requestInfo = extra.requestInfo) === null || _extra_requestInfo === void 0 ? void 0 : _extra_requestInfo.headers)) {
|
|
182
|
+
const authHeader = extra.requestInfo.headers.authorization || extra.requestInfo.headers.Authorization;
|
|
183
|
+
if (authHeader) {
|
|
184
|
+
// Handle both string and string[] types
|
|
185
|
+
const headerValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
186
|
+
if (headerValue) {
|
|
187
|
+
const match = /^Bearer\s+(.+)$/i.exec(headerValue);
|
|
188
|
+
if (match) {
|
|
189
|
+
bearerToken = match[1];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!bearerToken) {
|
|
195
|
+
throw new McpError(ErrorCode.InvalidRequest, 'Missing Authorization header. DCR mode requires bearer token.');
|
|
196
|
+
}
|
|
197
|
+
// Call /oauth/verify to validate DCR token and get provider tokens
|
|
198
|
+
const verifyResponse = await fetch(this.config.verifyEndpoint, {
|
|
199
|
+
headers: {
|
|
200
|
+
Authorization: `Bearer ${bearerToken}`
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
if (!verifyResponse.ok) {
|
|
204
|
+
throw new McpError(ErrorCode.InvalidRequest, `Token verification failed: ${verifyResponse.status}`);
|
|
205
|
+
}
|
|
206
|
+
const verifyData = await verifyResponse.json();
|
|
207
|
+
// Fetch user email to use as accountId (with caching)
|
|
208
|
+
let accountId;
|
|
209
|
+
try {
|
|
210
|
+
accountId = await this.getUserEmail(verifyData.providerTokens);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get user email for DCR authentication: ${error instanceof Error ? error.message : String(error)}`);
|
|
213
|
+
}
|
|
214
|
+
// Create auth client from provider tokens
|
|
215
|
+
const auth = this.toAuth(verifyData.providerTokens);
|
|
216
|
+
// Inject authContext and logger into extra
|
|
217
|
+
extra.authContext = {
|
|
218
|
+
auth,
|
|
219
|
+
accountId
|
|
220
|
+
};
|
|
221
|
+
extra.logger = this.config.logger;
|
|
222
|
+
// Call original handler with all args
|
|
223
|
+
return await originalHandler(...allArgs);
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
...module,
|
|
227
|
+
handler: wrappedHandler
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
// Use structural constraints to avoid contravariance check on handler type.
|
|
232
|
+
// wrapAtPosition is now generic and returns T directly.
|
|
233
|
+
withToolAuth: (module)=>wrapAtPosition(module, 1),
|
|
234
|
+
withResourceAuth: (module)=>wrapAtPosition(module, 2),
|
|
235
|
+
withPromptAuth: (module)=>wrapAtPosition(module, 0)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
constructor(config){
|
|
239
|
+
this.emailCache = new Map();
|
|
240
|
+
this.config = config;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/Projects/ai/mcp-z/oauth/oauth-google/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 Google API calls with provided credentials.\n */\n\nimport type { ProviderTokens } from '@mcp-z/oauth';\nimport { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';\nimport { OAuth2Client } from 'google-auth-library';\nimport type { AuthContext, EnrichedExtra, Logger } from '../types.ts';\n\n/**\n * DCR Provider configuration\n */\nexport interface DcrOAuthProviderConfig {\n /** Google application client ID */\n clientId: string;\n\n /** Google application client secret (optional for public clients) */\n clientSecret?: string;\n\n /** OAuth scopes */\n scope: 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 * Google 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 Google OAuth 2.0\n * - No token storage dependency\n *\n * Pattern:\n * ```typescript\n * const provider = new DcrOAuthProvider(config);\n * const auth = provider.toAuth(providerTokens);\n * const accessToken = await getAccessToken(auth);\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 Google OAuth2Client from provider tokens\n *\n * This is the core stateless pattern - provider receives tokens from context\n * (token verification, HTTP request) and creates OAuth2Client on-demand.\n *\n * @param tokens - Provider tokens (Google access/refresh tokens)\n * @returns Google OAuth2Client configured with credentials\n */\n toAuth(tokens: ProviderTokens): OAuth2Client {\n const { clientId, clientSecret } = this.config;\n\n // Create OAuth2Client with credentials\n const client = new OAuth2Client({\n clientId,\n ...(clientSecret && { clientSecret }),\n });\n\n // Set initial credentials (convert undefined to null for Google's Credentials type)\n client.credentials = {\n access_token: tokens.accessToken,\n refresh_token: tokens.refreshToken ?? null,\n expiry_date: tokens.expiresAt ?? null,\n token_type: 'Bearer',\n };\n\n // Override getRequestMetadataAsync to handle token refresh\n // @ts-expect-error - Access protected method for token refresh\n const originalGetMetadata = client.getRequestMetadataAsync.bind(client);\n\n // @ts-expect-error - Override protected method for token refresh\n client.getRequestMetadataAsync = async (url?: string) => {\n // Check if token needs refresh\n if (this.needsRefresh(client.credentials.expiry_date)) {\n try {\n // Use built-in refresh mechanism\n const refreshedTokens = await client.refreshAccessToken();\n client.credentials = refreshedTokens.credentials;\n } catch (error) {\n throw new Error(`Token refresh failed: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n return originalGetMetadata(url);\n };\n\n return client;\n }\n\n /**\n * Check if token needs refresh (with 1 minute buffer)\n */\n private needsRefresh(expiryDate: number | null | undefined): boolean {\n if (!expiryDate) return false; // No expiry = no refresh needed\n return Date.now() >= expiryDate - 60000; // 1 minute buffer\n }\n\n /**\n * Refresh Google access token using refresh token\n *\n * @param refreshToken - Google refresh token\n * @returns New provider tokens\n */\n async refreshAccessToken(refreshToken: string): Promise<ProviderTokens> {\n const { clientId, clientSecret } = this.config;\n\n const tokenUrl = 'https://oauth2.googleapis.com/token';\n const params: Record<string, string> = {\n refresh_token: refreshToken,\n client_id: clientId,\n grant_type: 'refresh_token',\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 fetch(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 const result: ProviderTokens = {\n accessToken: tokenResponse.access_token,\n refreshToken: refreshToken, // Keep original refresh token\n };\n\n // Only add optional fields if they have values\n if (tokenResponse.expires_in !== undefined) {\n result.expiresAt = Date.now() + tokenResponse.expires_in * 1000;\n }\n if (tokenResponse.scope !== undefined) {\n result.scope = tokenResponse.scope;\n }\n\n return result;\n }\n\n /**\n * Get user email from Google userinfo 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.toAuth(tokens);\n\n // Use OAuth2Client to make authenticated request\n const response = await auth.request({\n url: 'https://www.googleapis.com/oauth2/v2/userinfo',\n method: 'GET',\n });\n\n const userInfo = response.data as { email: string };\n const email = userInfo.email;\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 authMiddleware = provider.authMiddleware();\n * const tools = toolFactories.map(f => f()).map(authMiddleware.withToolAuth);\n * const resources = resourceFactories.map(f => f()).map(authMiddleware.withResourceAuth);\n * const prompts = promptFactories.map(f => f()).map(authMiddleware.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 fetch(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 client from provider tokens\n const auth = this.toAuth(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","OAuth2Client","DcrOAuthProvider","toAuth","tokens","clientId","clientSecret","config","client","credentials","access_token","accessToken","refresh_token","refreshToken","expiry_date","expiresAt","token_type","originalGetMetadata","getRequestMetadataAsync","bind","url","needsRefresh","refreshedTokens","refreshAccessToken","error","Error","message","String","expiryDate","Date","now","tokenUrl","params","client_id","grant_type","client_secret","body","URLSearchParams","response","fetch","method","headers","toString","ok","errorText","text","status","tokenResponse","json","result","expires_in","undefined","scope","getUserEmail","cacheKey","cached","emailCache","get","email","auth","request","userInfo","data","set","authMiddleware","wrapAtPosition","module","extraPosition","originalHandler","handler","wrappedHandler","allArgs","extra","bearerToken","authInfo","token","requestInfo","authHeader","authorization","Authorization","headerValue","Array","isArray","match","exec","InvalidRequest","verifyResponse","verifyEndpoint","verifyData","accountId","providerTokens","InternalError","authContext","logger","withToolAuth","withResourceAuth","withPromptAuth","Map"],"mappings":"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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loopback OAuth Implementation (RFC 8252)
|
|
3
|
+
*
|
|
4
|
+
* Implements OAuth 2.0 Authorization Code Flow with PKCE using loopback interface redirection.
|
|
5
|
+
* Uses ephemeral local server with OS-assigned port (RFC 8252 Section 8.3).
|
|
6
|
+
*/
|
|
7
|
+
import { type OAuth2TokenStorageProvider } from '@mcp-z/oauth';
|
|
8
|
+
import { OAuth2Client } from 'google-auth-library';
|
|
9
|
+
import { type LoopbackOAuthConfig } from '../types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Loopback OAuth Client (RFC 8252 Section 7.3)
|
|
12
|
+
*
|
|
13
|
+
* Implements OAuth 2.0 Authorization Code Flow with PKCE for native applications
|
|
14
|
+
* using loopback interface redirection. Manages ephemeral OAuth flows and token persistence
|
|
15
|
+
* with Keyv for key-based token storage using compound keys.
|
|
16
|
+
*
|
|
17
|
+
* Token key format: {accountId}:{service}:token (e.g., "user@example.com:gmail:token")
|
|
18
|
+
*/
|
|
19
|
+
export declare class LoopbackOAuthProvider implements OAuth2TokenStorageProvider {
|
|
20
|
+
private config;
|
|
21
|
+
constructor(config: LoopbackOAuthConfig);
|
|
22
|
+
/**
|
|
23
|
+
* Get access token from Keyv using compound key
|
|
24
|
+
*
|
|
25
|
+
* @param accountId - Account identifier (email address). Required for loopback OAuth.
|
|
26
|
+
* @returns Access token for API requests
|
|
27
|
+
*/
|
|
28
|
+
getAccessToken(accountId?: string): Promise<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Convert to googleapis-compatible OAuth2Client
|
|
31
|
+
*
|
|
32
|
+
* @param accountId - Account identifier for multi-account support (e.g., 'user@example.com')
|
|
33
|
+
* @returns OAuth2Client configured for the specified account
|
|
34
|
+
*/
|
|
35
|
+
toAuth(accountId?: string): OAuth2Client;
|
|
36
|
+
/**
|
|
37
|
+
* Authenticate new account with OAuth flow
|
|
38
|
+
* Triggers account selection, stores token, registers account
|
|
39
|
+
*
|
|
40
|
+
* @returns Email address of newly authenticated account
|
|
41
|
+
* @throws Error in headless mode (cannot open browser for OAuth)
|
|
42
|
+
*/
|
|
43
|
+
authenticateNewAccount(): Promise<string>;
|
|
44
|
+
/**
|
|
45
|
+
* Get user email from Google's userinfo endpoint (pure query)
|
|
46
|
+
* Used to query email for existing authenticated account
|
|
47
|
+
*
|
|
48
|
+
* @param accountId - Account identifier to get email for
|
|
49
|
+
* @returns User's email address
|
|
50
|
+
*/
|
|
51
|
+
getUserEmail(accountId?: string): Promise<string>;
|
|
52
|
+
/**
|
|
53
|
+
* Check for existing accounts in token storage (incremental OAuth detection)
|
|
54
|
+
*
|
|
55
|
+
* Uses key-utils helper for forward compatibility with key format changes.
|
|
56
|
+
*
|
|
57
|
+
* @returns Array of account IDs that have tokens for this service
|
|
58
|
+
*/
|
|
59
|
+
private getExistingAccounts;
|
|
60
|
+
private isTokenValid;
|
|
61
|
+
/**
|
|
62
|
+
* Fetch user email from Google OAuth2 userinfo endpoint
|
|
63
|
+
* Called during OAuth flow to get email for accountId
|
|
64
|
+
*
|
|
65
|
+
* @param accessToken - Fresh access token from OAuth exchange
|
|
66
|
+
* @returns User's email address
|
|
67
|
+
*/
|
|
68
|
+
private fetchUserEmailFromToken;
|
|
69
|
+
private performEphemeralOAuthFlow;
|
|
70
|
+
private exchangeCodeForToken;
|
|
71
|
+
private refreshAccessToken;
|
|
72
|
+
/**
|
|
73
|
+
* Create authentication middleware for MCP tools, resources, and prompts
|
|
74
|
+
*
|
|
75
|
+
* Returns position-aware middleware wrappers that enrich handlers with authentication context.
|
|
76
|
+
* The middleware handles token retrieval, refresh, and AuthRequiredError automatically.
|
|
77
|
+
*
|
|
78
|
+
* Single-user middleware for desktop/CLI apps where ONE user runs the entire process:
|
|
79
|
+
* - Desktop applications (Claude Desktop)
|
|
80
|
+
* - CLI tools (Gmail CLI)
|
|
81
|
+
* - Personal automation scripts
|
|
82
|
+
*
|
|
83
|
+
* All requests use token lookups based on the active account or account override.
|
|
84
|
+
*
|
|
85
|
+
* @returns Object with withToolAuth, withResourceAuth, withPromptAuth methods
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const loopback = new LoopbackOAuthProvider({ service: 'gmail', ... });
|
|
90
|
+
* const authMiddleware = loopback.authMiddleware();
|
|
91
|
+
* const tools = toolFactories.map(f => f()).map(authMiddleware.withToolAuth);
|
|
92
|
+
* const resources = resourceFactories.map(f => f()).map(authMiddleware.withResourceAuth);
|
|
93
|
+
* const prompts = promptFactories.map(f => f()).map(authMiddleware.withPromptAuth);
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
authMiddleware(): {
|
|
97
|
+
withToolAuth: <T extends {
|
|
98
|
+
name: string;
|
|
99
|
+
config: unknown;
|
|
100
|
+
handler: unknown;
|
|
101
|
+
}>(module: T) => T;
|
|
102
|
+
withResourceAuth: <T extends {
|
|
103
|
+
name: string;
|
|
104
|
+
template?: unknown;
|
|
105
|
+
config?: unknown;
|
|
106
|
+
handler: unknown;
|
|
107
|
+
}>(module: T) => T;
|
|
108
|
+
withPromptAuth: <T extends {
|
|
109
|
+
name: string;
|
|
110
|
+
config: unknown;
|
|
111
|
+
handler: unknown;
|
|
112
|
+
}>(module: T) => T;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Create a loopback OAuth client for Google services
|
|
117
|
+
* Works for both stdio and HTTP transports
|
|
118
|
+
*/
|
|
119
|
+
export declare function createGoogleFileAuth(config: LoopbackOAuthConfig): OAuth2TokenStorageProvider;
|