@@ -32,7 +32,18 @@ export const OAuthMetadataSchema = z
32
32
} )
33
33
. passthrough ( ) ;
34
34
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
+
35
45
export type OAuthMetadata = z . infer < typeof OAuthMetadataSchema > ;
46
+ export type OAuthTokens = z . infer < typeof OAuthTokensSchema > ;
36
47
37
48
/**
38
49
* Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
@@ -58,18 +69,16 @@ export async function discoverOAuthMetadata(
58
69
return OAuthMetadataSchema . parse ( await response . json ( ) ) ;
59
70
}
60
71
72
+ /**
73
+ * Begins the authorization flow with the given server, by generating a PKCE challenge and constructing the authorization URL.
74
+ */
61
75
export async function startAuthorization (
62
76
serverUrl : string | URL ,
63
77
{
64
78
metadata,
65
79
redirectUrl,
66
80
} : { metadata : OAuthMetadata ; redirectUrl : string | URL } ,
67
81
) : 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
-
73
82
const responseType = "code" ;
74
83
const codeChallengeMethod = "S256" ;
75
84
@@ -95,6 +104,11 @@ export async function startAuthorization(
95
104
authorizationUrl = new URL ( "/authorize" , serverUrl ) ;
96
105
}
97
106
107
+ // Generate PKCE challenge
108
+ const challenge = await pkceChallenge ( ) ;
109
+ const codeVerifier = challenge . code_verifier ;
110
+ const codeChallenge = challenge . code_challenge ;
111
+
98
112
authorizationUrl . searchParams . set ( "response_type" , responseType ) ;
99
113
authorizationUrl . searchParams . set ( "code_challenge" , codeChallenge ) ;
100
114
authorizationUrl . searchParams . set (
@@ -105,3 +119,59 @@ export async function startAuthorization(
105
119
106
120
return { authorizationUrl, codeVerifier } ;
107
121
}
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