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