1
+ import { Response } from "express" ;
2
+ import { OAuthRegisteredClientsStore } from "./clients.js" ;
3
+ import {
4
+ OAuthClientInformationFull ,
5
+ OAuthTokenRevocationRequest ,
6
+ OAuthTokens ,
7
+ OAuthTokensSchema ,
8
+ } from "./../../shared/auth.js" ;
9
+ import { AuthInfo } from "./types.js" ;
10
+ import { AuthorizationParams , OAuthServerProvider } from "./provider.js" ;
11
+ import { ServerError } from "./errors.js" ;
12
+
13
+ export type ProxyEndpoints = {
14
+ authorizationUrl ?: string ;
15
+ tokenUrl ?: string ;
16
+ revocationUrl ?: string ;
17
+ registrationUrl ?: string ;
18
+ } ;
19
+
20
+ export type ProxyOptions = {
21
+ /**
22
+ * Individual endpoint URLs for proxying specific OAuth operations
23
+ */
24
+ endpoints : ProxyEndpoints ;
25
+
26
+ /**
27
+ * Function to verify access tokens and return auth info
28
+ */
29
+ verifyToken : ( token : string ) => Promise < AuthInfo > ;
30
+
31
+ /**
32
+ * Function to fetch client information from the upstream server
33
+ */
34
+ getClient : ( clientId : string ) => Promise < OAuthClientInformationFull | undefined > ;
35
+
36
+ } ;
37
+
38
+ /**
39
+ * Implements an OAuth server that proxies requests to another OAuth server.
40
+ */
41
+ export class ProxyOAuthServerProvider implements OAuthServerProvider {
42
+ private readonly _endpoints : ProxyEndpoints ;
43
+ private readonly _verifyToken : ( token : string ) => Promise < AuthInfo > ;
44
+ private readonly _getClient : ( clientId : string ) => Promise < OAuthClientInformationFull | undefined > ;
45
+
46
+ public revokeToken ?: (
47
+ client : OAuthClientInformationFull ,
48
+ request : OAuthTokenRevocationRequest
49
+ ) => Promise < void > ;
50
+
51
+ constructor ( options : ProxyOptions ) {
52
+ this . _endpoints = options . endpoints ;
53
+ this . _verifyToken = options . verifyToken ;
54
+ this . _getClient = options . getClient ;
55
+ if ( options . endpoints ?. revocationUrl ) {
56
+ this . revokeToken = async (
57
+ client : OAuthClientInformationFull ,
58
+ request : OAuthTokenRevocationRequest
59
+ ) => {
60
+ const revocationUrl = this . _endpoints . revocationUrl ;
61
+
62
+ if ( ! revocationUrl ) {
63
+ throw new Error ( "No revocation endpoint configured" ) ;
64
+ }
65
+
66
+ const params = new URLSearchParams ( ) ;
67
+ params . set ( "token" , request . token ) ;
68
+ params . set ( "client_id" , client . client_id ) ;
69
+ params . set ( "client_secret" , client . client_secret || "" ) ;
70
+ if ( request . token_type_hint ) {
71
+ params . set ( "token_type_hint" , request . token_type_hint ) ;
72
+ }
73
+
74
+ const response = await fetch ( revocationUrl , {
75
+ method : "POST" ,
76
+ headers : {
77
+ "Content-Type" : "application/x-www-form-urlencoded" ,
78
+ } ,
79
+ body : params ,
80
+ } ) ;
81
+
82
+ if ( ! response . ok ) {
83
+ throw new ServerError ( `Token revocation failed: ${ response . status } ` ) ;
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ get clientsStore ( ) : OAuthRegisteredClientsStore {
90
+ const registrationUrl = this . _endpoints . registrationUrl ;
91
+ return {
92
+ getClient : this . _getClient ,
93
+ ...( registrationUrl && {
94
+ registerClient : async ( client : OAuthClientInformationFull ) => {
95
+ const response = await fetch ( registrationUrl , {
96
+ method : "POST" ,
97
+ headers : {
98
+ "Content-Type" : "application/json" ,
99
+ } ,
100
+ body : JSON . stringify ( client ) ,
101
+ } ) ;
102
+
103
+ if ( ! response . ok ) {
104
+ throw new ServerError ( `Client registration failed: ${ response . status } ` ) ;
105
+ }
106
+
107
+ return response . json ( ) ;
108
+ }
109
+ } )
110
+ }
111
+ }
112
+
113
+ async authorize (
114
+ client : OAuthClientInformationFull ,
115
+ params : AuthorizationParams ,
116
+ res : Response
117
+ ) : Promise < void > {
118
+ const authorizationUrl = this . _endpoints . authorizationUrl ;
119
+
120
+ if ( ! authorizationUrl ) {
121
+ throw new Error ( "No authorization endpoint configured" ) ;
122
+ }
123
+
124
+ // Start with required OAuth parameters
125
+ const targetUrl = new URL ( authorizationUrl ) ;
126
+ const searchParams = new URLSearchParams ( {
127
+ client_id : client . client_id ,
128
+ response_type : "code" ,
129
+ redirect_uri : params . redirectUri ,
130
+ code_challenge : params . codeChallenge ,
131
+ code_challenge_method : "S256"
132
+ } ) ;
133
+
134
+ // Add optional standard OAuth parameters
135
+ if ( params . state ) searchParams . set ( "state" , params . state ) ;
136
+ if ( params . scopes ?. length ) searchParams . set ( "scope" , params . scopes . join ( " " ) ) ;
137
+
138
+ targetUrl . search = searchParams . toString ( ) ;
139
+ res . redirect ( targetUrl . toString ( ) ) ;
140
+ }
141
+
142
+ async challengeForAuthorizationCode (
143
+ _client : OAuthClientInformationFull ,
144
+ _authorizationCode : string
145
+ ) : Promise < string > {
146
+ // In a proxy setup, we don't store the code challenge ourselves
147
+ // Instead, we proxy the token request and let the upstream server validate it
148
+ return "" ;
149
+ }
150
+
151
+ async exchangeAuthorizationCode (
152
+ client : OAuthClientInformationFull ,
153
+ authorizationCode : string ,
154
+ codeVerifier ?: string
155
+ ) : Promise < OAuthTokens > {
156
+ const tokenUrl = this . _endpoints . tokenUrl ;
157
+
158
+ if ( ! tokenUrl ) {
159
+ throw new Error ( "No token endpoint configured" ) ;
160
+ }
161
+ const response = await fetch ( tokenUrl , {
162
+ method : "POST" ,
163
+ headers : {
164
+ "Content-Type" : "application/x-www-form-urlencoded" ,
165
+ } ,
166
+ body : new URLSearchParams ( {
167
+ grant_type : "authorization_code" ,
168
+ client_id : client . client_id ,
169
+ client_secret : client . client_secret || "" ,
170
+ code : authorizationCode ,
171
+ code_verifier : codeVerifier || "" ,
172
+ } ) ,
173
+ } ) ;
174
+
175
+ if ( ! response . ok ) {
176
+ throw new ServerError ( `Token exchange failed: ${ response . status } ` ) ;
177
+ }
178
+
179
+ const data = await response . json ( ) ;
180
+ return OAuthTokensSchema . parse ( data ) ;
181
+ }
182
+
183
+ async exchangeRefreshToken (
184
+ client : OAuthClientInformationFull ,
185
+ refreshToken : string ,
186
+ scopes ?: string [ ]
187
+ ) : Promise < OAuthTokens > {
188
+ const tokenUrl = this . _endpoints . tokenUrl ;
189
+
190
+ if ( ! tokenUrl ) {
191
+ throw new Error ( "No token endpoint configured" ) ;
192
+ }
193
+
194
+ const params = new URLSearchParams ( {
195
+ grant_type : "refresh_token" ,
196
+ client_id : client . client_id ,
197
+ client_secret : client . client_secret || "" ,
198
+ refresh_token : refreshToken ,
199
+ } ) ;
200
+
201
+ if ( scopes ?. length ) {
202
+ params . set ( "scope" , scopes . join ( " " ) ) ;
203
+ }
204
+
205
+ const response = await fetch ( tokenUrl , {
206
+ method : "POST" ,
207
+ headers : {
208
+ "Content-Type" : "application/x-www-form-urlencoded" ,
209
+ } ,
210
+ body : params ,
211
+ } ) ;
212
+
213
+ if ( ! response . ok ) {
214
+ throw new ServerError ( `Token refresh failed: ${ response . status } ` ) ;
215
+ }
216
+
217
+ const data = await response . json ( ) ;
218
+ return OAuthTokensSchema . parse ( data ) ;
219
+ }
220
+
221
+ async verifyAccessToken ( token : string ) : Promise < AuthInfo > {
222
+ return this . _verifyToken ( token ) ;
223
+ }
224
+ }
0 commit comments