@@ -32,7 +32,18 @@ export const OAuthMetadataSchema = z
3232 } )
3333 . passthrough ( ) ;
3434
35+ export const OAuthTokensSchema = z
36+ . object ( {
37+ access_token : z . string ( ) ,
38+ token_type : z . string ( ) ,
39+ expires_in : z . number ( ) . optional ( ) ,
40+ scope : z . string ( ) . optional ( ) ,
41+ refresh_token : z . string ( ) . optional ( ) ,
42+ } )
43+ . strip ( ) ;
44+
3545export type OAuthMetadata = z . infer < typeof OAuthMetadataSchema > ;
46+ export type OAuthTokens = z . infer < typeof OAuthTokensSchema > ;
3647
3748/**
3849 * Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
@@ -58,18 +69,16 @@ export async function discoverOAuthMetadata(
5869 return OAuthMetadataSchema . parse ( await response . json ( ) ) ;
5970}
6071
72+ /**
73+ * Begins the authorization flow with the given server, by generating a PKCE challenge and constructing the authorization URL.
74+ */
6175export async function startAuthorization (
6276 serverUrl : string | URL ,
6377 {
6478 metadata,
6579 redirectUrl,
6680 } : { metadata : OAuthMetadata ; redirectUrl : string | URL } ,
6781) : Promise < { authorizationUrl : URL ; codeVerifier : string } > {
68- // Generate PKCE challenge
69- const challenge = await pkceChallenge ( ) ;
70- const codeVerifier = challenge . code_verifier ;
71- const codeChallenge = challenge . code_challenge ;
72-
7382 const responseType = "code" ;
7483 const codeChallengeMethod = "S256" ;
7584
@@ -95,6 +104,11 @@ export async function startAuthorization(
95104 authorizationUrl = new URL ( "/authorize" , serverUrl ) ;
96105 }
97106
107+ // Generate PKCE challenge
108+ const challenge = await pkceChallenge ( ) ;
109+ const codeVerifier = challenge . code_verifier ;
110+ const codeChallenge = challenge . code_challenge ;
111+
98112 authorizationUrl . searchParams . set ( "response_type" , responseType ) ;
99113 authorizationUrl . searchParams . set ( "code_challenge" , codeChallenge ) ;
100114 authorizationUrl . searchParams . set (
@@ -105,3 +119,59 @@ export async function startAuthorization(
105119
106120 return { authorizationUrl, codeVerifier } ;
107121}
122+
123+ /**
124+ * Exchanges an authorization code for an access token with the given server.
125+ */
126+ export async function exchangeAuthorization (
127+ serverUrl : string | URL ,
128+ {
129+ metadata,
130+ authorizationCode,
131+ codeVerifier,
132+ redirectUrl,
133+ } : {
134+ metadata : OAuthMetadata ;
135+ authorizationCode : string ;
136+ codeVerifier : string ;
137+ redirectUrl : string | URL ;
138+ } ,
139+ ) : Promise < OAuthTokens > {
140+ const grantType = "authorization_code" ;
141+
142+ let tokenUrl : URL ;
143+ if ( metadata ) {
144+ tokenUrl = new URL ( metadata . token_endpoint ) ;
145+
146+ if (
147+ metadata . grant_types_supported &&
148+ ! ( grantType in metadata . grant_types_supported )
149+ ) {
150+ throw new Error (
151+ `Incompatible auth server: does not support grant type ${ grantType } ` ,
152+ ) ;
153+ }
154+ } else {
155+ tokenUrl = new URL ( "/token" , serverUrl ) ;
156+ }
157+
158+ // Exchange code for tokens
159+ const response = await fetch ( tokenUrl , {
160+ method : "POST" ,
161+ headers : {
162+ "Content-Type" : "application/x-www-form-urlencoded" ,
163+ } ,
164+ body : new URLSearchParams ( {
165+ grant_type : grantType ,
166+ code : authorizationCode ,
167+ code_verifier : codeVerifier ,
168+ redirect_uri : String ( redirectUrl ) ,
169+ } ) ,
170+ } ) ;
171+
172+ if ( ! response . ok ) {
173+ throw new Error ( `Token exchange failed: HTTP ${ response . status } ` ) ;
174+ }
175+
176+ return OAuthTokensSchema . parse ( await response . json ( ) ) ;
177+ }
0 commit comments