Skip to content

Commit 47fd6df

Browse files
committed
Higher-level auth flow
1 parent fa50204 commit 47fd6df

File tree

1 file changed

+150
-0
lines changed

1 file changed

+150
-0
lines changed

src/client/auth.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,156 @@ export type OAuthTokens = z.infer<typeof OAuthTokensSchema>;
7979
export type OAuthClientMetadata = z.infer<typeof OAuthClientMetadataSchema>;
8080
export type OAuthClientInformation = z.infer<typeof OAuthClientInformationSchema>;
8181

82+
/**
83+
* Implements an end-to-end OAuth client to be used with one MCP server.
84+
*
85+
* This client relies upon a concept of an authorized "session," the exact
86+
* meaning of which is application-defined. Tokens, authorization codes, and
87+
* code verifiers should not cross different sessions.
88+
*/
89+
export interface OAuthClientProvider {
90+
/**
91+
* The URL to redirect the user agent to after authorization.
92+
*
93+
* If the client is not redirecting to localhost, `clientInformation` must be
94+
* implemented.
95+
*/
96+
get redirectUrl(): string | URL;
97+
98+
/**
99+
* Metadata about this OAuth client.
100+
*/
101+
get clientMetadata(): OAuthClientMetadata;
102+
103+
/**
104+
* Loads information about this OAuth client, as registered already with the
105+
* server, or returns `undefined` if the client is not registered with the
106+
* server.
107+
*
108+
* This method must be implemented _unless_ redirecting to `localhost`.
109+
*/
110+
clientInformation?(): OAuthClientInformation | undefined | Promise<OAuthClientInformation | undefined>;
111+
112+
/**
113+
* If implemented, this permits the OAuth client to dynamically register with
114+
* the server. Client information saved this way should later be read via
115+
* `clientInformation()`.
116+
*
117+
* This method is not required to be implemented if redirecting to
118+
* `localhost`, or if client information is statically known (e.g.,
119+
* pre-registered).
120+
*/
121+
saveClientInformation?(clientInformation: OAuthClientInformation): void | Promise<void>;
122+
123+
/**
124+
* Loads any existing OAuth tokens for the current session, or returns
125+
* `undefined` if there are no saved tokens.
126+
*/
127+
tokens(): OAuthTokens | undefined | Promise<OAuthTokens | undefined>;
128+
129+
/**
130+
* Stores new OAuth tokens for the current session, after a successful
131+
* authorization.
132+
*/
133+
saveTokens(tokens: OAuthTokens): void | Promise<void>;
134+
135+
/**
136+
* Invoked to redirect the user agent to the given URL to begin the authorization flow.
137+
*/
138+
redirectToAuthorization(authorizationUrl: URL): void | Promise<void>;
139+
140+
/**
141+
* Saves a PKCE code verifier for the current session, before redirecting to
142+
* the authorization flow.
143+
*/
144+
saveCodeVerifier(codeVerifier: string): void | Promise<void>;
145+
146+
/**
147+
* Loads the PKCE code verifier for the current session, necessary to validate
148+
* the authorization result.
149+
*/
150+
codeVerifier(): string | Promise<string>;
151+
}
152+
153+
export type AuthResult = "AUTHORIZED" | "REDIRECT";
154+
155+
/**
156+
* Orchestrates the full auth flow with a server.
157+
*
158+
* This can be used as a single entry point for all authorization functionality,
159+
* instead of linking together the other lower-level functions in this module.
160+
*/
161+
export async function auth(
162+
provider: OAuthClientProvider,
163+
{ serverUrl, authorizationCode }: { serverUrl: string | URL, authorizationCode?: string }): Promise<AuthResult> {
164+
const metadata = await discoverOAuthMetadata(serverUrl);
165+
166+
// Handle client registration if needed
167+
const hostname = new URL(provider.redirectUrl).hostname;
168+
if (hostname !== "localhost" && hostname !== "127.0.0.1") {
169+
if (!provider.clientInformation) {
170+
throw new Error("OAuth client information is required when not redirecting to localhost")
171+
}
172+
173+
let clientInformation = await Promise.resolve(provider.clientInformation());
174+
if (!clientInformation) {
175+
if (authorizationCode !== undefined) {
176+
throw new Error("Existing OAuth client information is required when exchanging an authorization code");
177+
}
178+
179+
if (!provider.saveClientInformation) {
180+
throw new Error("OAuth client information must be saveable when not provided and not redirecting to localhost");
181+
}
182+
183+
clientInformation = await registerClient(serverUrl, {
184+
metadata,
185+
clientMetadata: provider.clientMetadata,
186+
});
187+
188+
await provider.saveClientInformation(clientInformation);
189+
}
190+
191+
// TODO: Send clientInformation into auth flow
192+
}
193+
194+
// Exchange authorization code for tokens
195+
if (authorizationCode !== undefined) {
196+
const codeVerifier = await provider.codeVerifier();
197+
const tokens = await exchangeAuthorization(serverUrl, {
198+
metadata,
199+
authorizationCode,
200+
codeVerifier,
201+
});
202+
203+
await provider.saveTokens(tokens);
204+
return "AUTHORIZED";
205+
}
206+
207+
const tokens = await provider.tokens();
208+
209+
// Handle token refresh or new authorization
210+
if (tokens?.refresh_token) {
211+
try {
212+
// Attempt to refresh the token
213+
const newTokens = await refreshAuthorization(serverUrl, {
214+
metadata,
215+
refreshToken: tokens.refresh_token,
216+
});
217+
218+
await provider.saveTokens(newTokens);
219+
return "AUTHORIZED";
220+
} catch (error) {
221+
console.error("Could not refresh OAuth tokens:", error);
222+
}
223+
}
224+
225+
// Start new authorization flow
226+
const { authorizationUrl, codeVerifier } = await startAuthorization(serverUrl, { metadata, redirectUrl: provider.redirectUrl });
227+
await provider.saveCodeVerifier(codeVerifier);
228+
await provider.redirectToAuthorization(authorizationUrl);
229+
return "REDIRECT";
230+
}
231+
82232
/**
83233
* Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
84234
*

0 commit comments

Comments
 (0)