@@ -58,6 +58,99 @@ app.get("/api/health", (_req: Request, res: Response) => {
5858 res . json ( { status : "ok" , uptime : process . uptime ( ) } ) ;
5959} ) ;
6060
61+ // OAuth token exchange endpoint - proxies token requests to hide client secrets from browser
62+ app . post ( "/api/auth/exchange-token" , express . json ( ) , async ( req : Request , res : Response ) => {
63+ try {
64+ const { code, codeVerifier, environment } = req . body ;
65+
66+ // Validate required parameters
67+ if ( ! code || ! codeVerifier || ! environment ) {
68+ return res . status ( 400 ) . json ( {
69+ error : 'missing_parameters' ,
70+ message : 'code, codeVerifier, and environment are required'
71+ } ) ;
72+ }
73+
74+ // Validate environment
75+ if ( environment !== 'dev' && environment !== 'prod' ) {
76+ return res . status ( 400 ) . json ( {
77+ error : 'invalid_environment' ,
78+ message : 'environment must be "dev" or "prod"'
79+ } ) ;
80+ }
81+
82+ // Get environment-specific configuration from server environment variables
83+ let loginBaseUrl : string ;
84+ let clientId : string ;
85+ let clientSecret : string ;
86+ let redirectUri : string ;
87+
88+ if ( environment === 'dev' ) {
89+ loginBaseUrl = process . env . VITE_DEV_LOGIN_BASE_URL || '' ;
90+ clientId = process . env . VITE_DEV_OAUTH_WEB_CLIENT_ID || '' ;
91+ clientSecret = process . env . VITE_DEV_OAUTH_WEB_CLIENT_SECRET || '' ;
92+ } else {
93+ loginBaseUrl = process . env . VITE_PROD_LOGIN_BASE_URL || '' ;
94+ clientId = process . env . VITE_PROD_OAUTH_WEB_CLIENT_ID || '' ;
95+ clientSecret = process . env . VITE_PROD_OAUTH_WEB_CLIENT_SECRET || '' ;
96+ }
97+
98+ redirectUri = process . env . VITE_OAUTH_REDIRECT_URI ||
99+ `${ req . protocol } ://${ req . get ( 'host' ) } /account/callback` ;
100+
101+ // Validate configuration
102+ if ( ! loginBaseUrl || ! clientId || ! clientSecret ) {
103+ console . error ( `❌ Missing OAuth configuration for environment: ${ environment } ` ) ;
104+ return res . status ( 500 ) . json ( {
105+ error : 'server_configuration_error' ,
106+ message : 'OAuth credentials not configured on server'
107+ } ) ;
108+ }
109+
110+ console . log ( `🔑 [token-exchange] Exchanging token for environment: ${ environment } ` ) ;
111+ console . log ( `🔑 [token-exchange] OAuth server: ${ loginBaseUrl } ` ) ;
112+
113+ // Build token exchange request
114+ const tokenUrl = `${ loginBaseUrl } /connect/token` ;
115+ const body = new URLSearchParams ( {
116+ grant_type : 'authorization_code' ,
117+ code : code ,
118+ redirect_uri : redirectUri ,
119+ code_verifier : codeVerifier ,
120+ client_id : clientId ,
121+ client_secret : clientSecret
122+ } ) ;
123+
124+ // Exchange code for token with OAuth server
125+ const response = await fetch ( tokenUrl , {
126+ method : 'POST' ,
127+ headers : {
128+ 'Content-Type' : 'application/x-www-form-urlencoded' ,
129+ } ,
130+ body : body . toString ( )
131+ } ) ;
132+
133+ const data = await response . json ( ) ;
134+
135+ if ( ! response . ok ) {
136+ console . error ( `❌ [token-exchange] OAuth server error:` , data ) ;
137+ return res . status ( response . status ) . json ( data ) ;
138+ }
139+
140+ console . log ( `✅ [token-exchange] Token exchange successful for environment: ${ environment } ` ) ;
141+
142+ // Return tokens to frontend
143+ res . json ( data ) ;
144+
145+ } catch ( error ) {
146+ console . error ( '❌ [token-exchange] Unexpected error:' , error ) ;
147+ res . status ( 500 ) . json ( {
148+ error : 'server_error' ,
149+ message : 'An unexpected error occurred during token exchange'
150+ } ) ;
151+ }
152+ } ) ;
153+
61154
62155// Register webhook-related routes (event store, SSE, diagnostics)
63156registerWebhookRoutes ( app ) ;
@@ -110,26 +203,27 @@ if (fs.existsSync(staticPath)) {
110203 }
111204
112205 // Build runtime config from VITE_ env vars (fall back to empty strings)
206+ // Note: Client secrets are intentionally hidden - token exchange happens on backend
113207 const runtime = {
114208 // Legacy environment variables (default/current environment)
115209 VITE_BACKEND_BASE_URL : process . env . VITE_BACKEND_BASE_URL || '' ,
116210 VITE_LOGIN_BASE_URL : process . env . VITE_LOGIN_BASE_URL || '' ,
117211 VITE_NODE_ENV : process . env . VITE_NODE_ENV || 'production' ,
118212 VITE_OAUTH_WEB_CLIENT_ID : process . env . VITE_OAUTH_WEB_CLIENT_ID || '' ,
119- VITE_OAUTH_WEB_CLIENT_SECRET : process . env . VITE_OAUTH_WEB_CLIENT_SECRET || '' ,
213+ VITE_OAUTH_WEB_CLIENT_SECRET : '' , // Hidden - not needed in browser
120214 VITE_OAUTH_REDIRECT_URI : process . env . VITE_OAUTH_REDIRECT_URI || '' ,
121215
122216 // Development environment variables
123217 VITE_DEV_BACKEND_BASE_URL : process . env . VITE_DEV_BACKEND_BASE_URL || '' ,
124218 VITE_DEV_LOGIN_BASE_URL : process . env . VITE_DEV_LOGIN_BASE_URL || '' ,
125219 VITE_DEV_OAUTH_WEB_CLIENT_ID : process . env . VITE_DEV_OAUTH_WEB_CLIENT_ID || '' ,
126- VITE_DEV_OAUTH_WEB_CLIENT_SECRET : process . env . VITE_DEV_OAUTH_WEB_CLIENT_SECRET || '' ,
220+ VITE_DEV_OAUTH_WEB_CLIENT_SECRET : '' , // Hidden - not needed in browser
127221
128222 // Production environment variables
129223 VITE_PROD_BACKEND_BASE_URL : process . env . VITE_PROD_BACKEND_BASE_URL || '' ,
130224 VITE_PROD_LOGIN_BASE_URL : process . env . VITE_PROD_LOGIN_BASE_URL || '' ,
131225 VITE_PROD_OAUTH_WEB_CLIENT_ID : process . env . VITE_PROD_OAUTH_WEB_CLIENT_ID || '' ,
132- VITE_PROD_OAUTH_WEB_CLIENT_SECRET : process . env . VITE_PROD_OAUTH_WEB_CLIENT_SECRET || '' ,
226+ VITE_PROD_OAUTH_WEB_CLIENT_SECRET : '' , // Hidden - not needed in browser
133227
134228 _generated : new Date ( ) . toISOString ( ) ,
135229 } as Record < string , any > ;
@@ -198,26 +292,27 @@ if (fs.existsSync(staticPath)) {
198292 // Provide the same runtime-config.js fallback as we do when static files exist
199293 app . get ( '/runtime-config.js' , ( _req : Request , res : Response ) => {
200294 res . type ( 'application/javascript' ) ;
295+ // Note: Client secrets are intentionally hidden - token exchange happens on backend
201296 const runtime = {
202297 // Legacy environment variables (default/current environment)
203298 VITE_BACKEND_BASE_URL : process . env . VITE_BACKEND_BASE_URL || '' ,
204299 VITE_LOGIN_BASE_URL : process . env . VITE_LOGIN_BASE_URL || '' ,
205300 VITE_NODE_ENV : process . env . VITE_NODE_ENV || 'production' ,
206301 VITE_OAUTH_WEB_CLIENT_ID : process . env . VITE_OAUTH_WEB_CLIENT_ID || '' ,
207- VITE_OAUTH_WEB_CLIENT_SECRET : process . env . VITE_OAUTH_WEB_CLIENT_SECRET || '' ,
302+ VITE_OAUTH_WEB_CLIENT_SECRET : '' , // Hidden - not needed in browser
208303 VITE_OAUTH_REDIRECT_URI : process . env . VITE_OAUTH_REDIRECT_URI || '' ,
209304
210305 // Development environment variables
211306 VITE_DEV_BACKEND_BASE_URL : process . env . VITE_DEV_BACKEND_BASE_URL || '' ,
212307 VITE_DEV_LOGIN_BASE_URL : process . env . VITE_DEV_LOGIN_BASE_URL || '' ,
213308 VITE_DEV_OAUTH_WEB_CLIENT_ID : process . env . VITE_DEV_OAUTH_WEB_CLIENT_ID || '' ,
214- VITE_DEV_OAUTH_WEB_CLIENT_SECRET : process . env . VITE_DEV_OAUTH_WEB_CLIENT_SECRET || '' ,
309+ VITE_DEV_OAUTH_WEB_CLIENT_SECRET : '' , // Hidden - not needed in browser
215310
216311 // Production environment variables
217312 VITE_PROD_BACKEND_BASE_URL : process . env . VITE_PROD_BACKEND_BASE_URL || '' ,
218313 VITE_PROD_LOGIN_BASE_URL : process . env . VITE_PROD_LOGIN_BASE_URL || '' ,
219314 VITE_PROD_OAUTH_WEB_CLIENT_ID : process . env . VITE_PROD_OAUTH_WEB_CLIENT_ID || '' ,
220- VITE_PROD_OAUTH_WEB_CLIENT_SECRET : process . env . VITE_PROD_OAUTH_WEB_CLIENT_SECRET || '' ,
315+ VITE_PROD_OAUTH_WEB_CLIENT_SECRET : '' , // Hidden - not needed in browser
221316
222317 _generated : new Date ( ) . toISOString ( ) ,
223318 } as Record < string , any > ;
0 commit comments