@@ -79,6 +79,156 @@ export type OAuthTokens = z.infer<typeof OAuthTokensSchema>;
79
79
export type OAuthClientMetadata = z . infer < typeof OAuthClientMetadataSchema > ;
80
80
export type OAuthClientInformation = z . infer < typeof OAuthClientInformationSchema > ;
81
81
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
+
82
232
/**
83
233
* Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
84
234
*
0 commit comments