@@ -6,10 +6,6 @@ import { randomUUID } from 'crypto';
66import jwt from 'jsonwebtoken' ;
77import axios from 'axios' ;
88
9- // Debug mode for verbose auth logging - OFF by default for security
10- // Set AUTH_DEBUG=true to enable detailed logging (includes sensitive data)
11- const AUTH_DEBUG = process . env . AUTH_DEBUG === 'true' ;
12-
139// ============================================================================
1410// Scope Challenge Types and Parser (MCP Authorization Best Practices)
1511// ============================================================================
@@ -148,12 +144,6 @@ export class TokenExchangeHandler {
148144 formData . append ( 'client_assertion_type' , 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' ) ;
149145 formData . append ( 'client_assertion' , clientAssertion ) ;
150146
151- const requestedScopes = scopes || this . config . agentScopes ;
152- console . log ( `🔄 Step 1: Exchanging ID token for ID-JAG token...` ) ;
153- console . log ( `🎯 Requested scopes: ${ requestedScopes } ` ) ;
154- console . log ( `📍 Audience: ${ this . config . authorizationServer } ` ) ;
155- console . log ( `🆔 Client ID: ${ this . config . clientId } ` ) ;
156-
157147 const response = await axios . post (
158148 `https://${ this . config . oktaDomain } /oauth2/v1/token` ,
159149 formData ,
@@ -164,12 +154,89 @@ export class TokenExchangeHandler {
164154 }
165155 ) ;
166156
167- console . log ( `✅ ID-JAG token obtained` ) ;
168- console . log ( `🎯 Issued token type: ${ response . data . issued_token_type } ` ) ;
169-
170157 return response . data . access_token ; // This is actually the ID-JAG token
171158 }
172159
160+ // ============================================================================
161+ // Exchange ID Token for Vaulted Secret (PAM)
162+ // ============================================================================
163+
164+ /**
165+ * Exchange ID token for a vaulted secret from Okta PAM
166+ * @param idToken - The user's ID token
167+ * @param resourceOrn - The ORN of the secret (e.g., orn:okta:pam:{orgId}:secrets:{secretId})
168+ * @param secretName - Optional name for logging purposes
169+ * @returns The secret value
170+ */
171+ async exchangeIdTokenForVaultedSecret (
172+ idToken : string ,
173+ resourceOrn : string ,
174+ secretName ?: string
175+ ) : Promise < string > {
176+ if ( ! this . privateKey ) {
177+ throw new Error ( 'Private key not loaded for token exchange' ) ;
178+ }
179+
180+ const tokenEndpoint = `https://${ this . config . oktaDomain } /oauth2/v1/token` ;
181+ const clientAssertion = this . createClientAssertion ( tokenEndpoint ) ;
182+
183+ const formData = new URLSearchParams ( ) ;
184+ formData . append ( 'grant_type' , 'urn:ietf:params:oauth:grant-type:token-exchange' ) ;
185+ formData . append ( 'requested_token_type' , 'urn:okta:params:oauth:token-type:vaulted-secret' ) ;
186+ formData . append ( 'subject_token' , idToken ) ;
187+ formData . append ( 'subject_token_type' , 'urn:ietf:params:oauth:token-type:id_token' ) ;
188+ formData . append ( 'resource' , resourceOrn ) ;
189+ formData . append ( 'client_assertion_type' , 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' ) ;
190+ formData . append ( 'client_assertion' , clientAssertion ) ;
191+
192+ try {
193+ const response = await axios . post ( tokenEndpoint , formData , {
194+ headers : {
195+ 'Content-Type' : 'application/x-www-form-urlencoded' ,
196+ } ,
197+ } ) ;
198+
199+ // Extract the actual secret value from the response
200+ const vaultedSecret = response . data . vaulted_secret ;
201+ let secretValue : string ;
202+
203+ if ( typeof vaultedSecret === 'string' ) {
204+ secretValue = vaultedSecret ;
205+ } else if ( typeof vaultedSecret === 'object' && vaultedSecret !== null ) {
206+ // Try common key names for secrets
207+ secretValue = vaultedSecret . secret
208+ || vaultedSecret . password
209+ || vaultedSecret . api_key
210+ || vaultedSecret . apiKey
211+ || vaultedSecret . token
212+ || vaultedSecret . value
213+ || Object . values ( vaultedSecret ) [ 0 ] as string ;
214+ } else {
215+ throw new Error ( `Invalid secret format received for ${ secretName || resourceOrn } ` ) ;
216+ }
217+
218+ // Validate non-empty
219+ if ( ! secretValue || ( typeof secretValue === 'string' && secretValue . trim ( ) === '' ) ) {
220+ throw new Error ( `Empty secret value received for ${ secretName || resourceOrn } ` ) ;
221+ }
222+
223+ return secretValue ;
224+ } catch ( error : any ) {
225+ const errorData = error . response ?. data ;
226+ console . error ( `❌ Failed to retrieve secret${ secretName ? ` (${ secretName } )` : '' } :` , errorData || error . message ) ;
227+
228+ if ( errorData ?. error === 'invalid_grant' ) {
229+ throw new Error ( `Secret access denied: ${ errorData . error_description || 'Invalid grant' } ` ) ;
230+ } else if ( errorData ?. error === 'invalid_target' ) {
231+ throw new Error ( `Invalid secret resource: ${ resourceOrn } ` ) ;
232+ }
233+
234+ throw new Error (
235+ `Failed to retrieve vaulted secret: ${ errorData ?. error_description || error . message } `
236+ ) ;
237+ }
238+ }
239+
173240 // ============================================================================
174241 // Step 2: Exchange ID-JAG for Access Token
175242 // ============================================================================
@@ -183,9 +250,6 @@ export class TokenExchangeHandler {
183250 const authorizationServer = this . config . authorizationServer ;
184251 const authorizationServerTokenEndpoint = this . config . authorizationServerTokenEndpoint ;
185252
186- console . log ( `🔄 Step 2: Exchanging ID-JAG for Access Token at Resource Server...` ) ;
187- console . log ( `📍 MCP authorization server: ${ authorizationServer } ` ) ;
188-
189253 const clientAssertion = this . createClientAssertion ( authorizationServerTokenEndpoint ) ;
190254
191255 const resourceTokenForm = new URLSearchParams ( ) ;
@@ -204,10 +268,6 @@ export class TokenExchangeHandler {
204268 }
205269 ) ;
206270
207- console . log ( `✅ Access Token obtained from Resource Server` ) ;
208- console . log ( `🎯 Token type: ${ response . data . token_type } ` ) ;
209- console . log ( `⏰ Expires in: ${ response . data . expires_in } s` ) ;
210-
211271 return response . data ;
212272 }
213273
@@ -235,14 +295,6 @@ export class TokenExchangeHandler {
235295 throw new Error ( 'Cross-app access not configured properly. Private key not loaded.' ) ;
236296 }
237297
238- // Only log tokens in debug mode - contains sensitive credentials
239- if ( AUTH_DEBUG ) {
240- console . log ( `👻 Subject token: ${ idToken } ` ) ;
241- }
242- if ( requestedScopes ) {
243- console . log ( `🔄 Step-up authorization requested with scopes: ${ requestedScopes } ` ) ;
244- }
245-
246298 try {
247299 // Step 1: Exchange ID token for ID-JAG
248300 const idJag = await this . exchangeIdTokenForIdJag ( idToken , requestedScopes ) ;
@@ -253,8 +305,6 @@ export class TokenExchangeHandler {
253305
254306 // Return the access token
255307 const accessToken = accessTokenResponse . access_token ;
256- console . log ( '✅ Access token obtained successfully' ) ;
257- console . log ( '💡 Token can be used for MCP tool calls' ) ;
258308
259309 return {
260310 success : true ,
0 commit comments