@@ -57,6 +57,68 @@ impl AuthStrategy for ApiKeyStrategy {
5757 }
5858}
5959
60+ /// Extract the ChatGPT account ID from a JWT token's claims.
61+ ///
62+ /// Checks `chatgpt_account_id`, `https://api.openai.com/auth.chatgpt_account_id`,
63+ /// and `organizations[0].id` in that order, matching the opencode
64+ /// implementation.
65+ fn extract_chatgpt_account_id ( token : & str ) -> Option < String > {
66+ let parts: Vec < & str > = token. split ( '.' ) . collect ( ) ;
67+ if parts. len ( ) != 3 {
68+ return None ;
69+ }
70+ use base64:: Engine ;
71+ let payload = base64:: engine:: general_purpose:: URL_SAFE_NO_PAD
72+ . decode ( parts[ 1 ] )
73+ . ok ( ) ?;
74+ let claims: serde_json:: Value = serde_json:: from_slice ( & payload) . ok ( ) ?;
75+
76+ // Try chatgpt_account_id first
77+ if let Some ( id) = claims[ "chatgpt_account_id" ] . as_str ( ) {
78+ return Some ( id. to_string ( ) ) ;
79+ }
80+ // Try nested auth claim
81+ if let Some ( id) = claims[ "https://api.openai.com/auth" ] [ "chatgpt_account_id" ] . as_str ( ) {
82+ return Some ( id. to_string ( ) ) ;
83+ }
84+ // Fall back to organizations[0].id
85+ if let Some ( id) = claims[ "organizations" ]
86+ . as_array ( )
87+ . and_then ( |orgs| orgs. first ( ) )
88+ . and_then ( |org| org[ "id" ] . as_str ( ) )
89+ {
90+ return Some ( id. to_string ( ) ) ;
91+ }
92+ None
93+ }
94+
95+ /// Adds Codex-specific credential metadata derived from OAuth tokens.
96+ ///
97+ /// Tries to extract the account ID from the `id_token` first (which typically
98+ /// contains the user identity claims in OpenID Connect flows), then falls back
99+ /// to the `access_token` if needed.
100+ fn enrich_codex_oauth_credential (
101+ provider_id : & ProviderId ,
102+ credential : & mut AuthCredential ,
103+ id_token : Option < & str > ,
104+ access_token : & str ,
105+ ) {
106+ if * provider_id != ProviderId :: CODEX {
107+ return ;
108+ }
109+
110+ // Try id_token first (preferred for user identity claims)
111+ let account_id = id_token
112+ . and_then ( extract_chatgpt_account_id)
113+ . or_else ( || extract_chatgpt_account_id ( access_token) ) ;
114+
115+ if let Some ( account_id) = account_id {
116+ credential
117+ . url_params
118+ . insert ( "chatgpt_account_id" . to_string ( ) . into ( ) , account_id. into ( ) ) ;
119+ }
120+ }
121+
60122/// OAuth Code Strategy - Browser redirect flow
61123pub struct OAuthCodeStrategy < T > {
62124 provider_id : ProviderId ,
@@ -96,7 +158,7 @@ impl<T: OAuthHttpProvider> AuthStrategy for OAuthCodeStrategy<T> {
96158 let token_response = self
97159 . adapter
98160 . exchange_code (
99- & self . config ,
161+ & ctx . request . oauth_config ,
100162 ctx. response . code . as_str ( ) ,
101163 ctx. request . pkce_verifier . as_ref ( ) . map ( |v| v. as_str ( ) ) ,
102164 )
@@ -107,12 +169,21 @@ impl<T: OAuthHttpProvider> AuthStrategy for OAuthCodeStrategy<T> {
107169 ) )
108170 } ) ?;
109171
110- build_oauth_credential (
172+ let access_token = token_response. access_token . clone ( ) ;
173+ let id_token = token_response. id_token . clone ( ) ;
174+ let mut credential = build_oauth_credential (
111175 self . provider_id . clone ( ) ,
112176 token_response,
113- & self . config ,
177+ & ctx . request . oauth_config ,
114178 chrono:: Duration :: hours ( 1 ) , // Code flow default
115- )
179+ ) ?;
180+ enrich_codex_oauth_credential (
181+ & self . provider_id ,
182+ & mut credential,
183+ id_token. as_deref ( ) ,
184+ & access_token,
185+ ) ;
186+ Ok ( credential)
116187 }
117188 _ => Err ( AuthError :: InvalidContext ( "Expected Code context" . to_string ( ) ) . into ( ) ) ,
118189 }
@@ -479,41 +550,6 @@ struct CodexDeviceTokenResponse {
479550 code_verifier : String ,
480551}
481552
482- /// Extract the ChatGPT account ID from a JWT token's claims.
483- ///
484- /// Checks `chatgpt_account_id`, `https://api.openai.com/auth.chatgpt_account_id`,
485- /// and `organizations[0].id` in that order, matching the opencode
486- /// implementation.
487- fn extract_chatgpt_account_id ( token : & str ) -> Option < String > {
488- let parts: Vec < & str > = token. split ( '.' ) . collect ( ) ;
489- if parts. len ( ) != 3 {
490- return None ;
491- }
492- use base64:: Engine ;
493- let payload = base64:: engine:: general_purpose:: URL_SAFE_NO_PAD
494- . decode ( parts[ 1 ] )
495- . ok ( ) ?;
496- let claims: serde_json:: Value = serde_json:: from_slice ( & payload) . ok ( ) ?;
497-
498- // Try chatgpt_account_id first
499- if let Some ( id) = claims[ "chatgpt_account_id" ] . as_str ( ) {
500- return Some ( id. to_string ( ) ) ;
501- }
502- // Try nested auth claim
503- if let Some ( id) = claims[ "https://api.openai.com/auth" ] [ "chatgpt_account_id" ] . as_str ( ) {
504- return Some ( id. to_string ( ) ) ;
505- }
506- // Fall back to organizations[0].id
507- if let Some ( id) = claims[ "organizations" ]
508- . as_array ( )
509- . and_then ( |orgs| orgs. first ( ) )
510- . and_then ( |org| org[ "id" ] . as_str ( ) )
511- {
512- return Some ( id. to_string ( ) ) ;
513- }
514- None
515- }
516-
517553#[ async_trait:: async_trait]
518554impl AuthStrategy for CodexDeviceStrategy {
519555 async fn init ( & self ) -> anyhow:: Result < AuthContextRequest > {
@@ -570,11 +606,8 @@ impl AuthStrategy for CodexDeviceStrategy {
570606 // Poll for authorization code using the custom OpenAI endpoint
571607 let token_response = codex_poll_for_tokens ( & ctx. request , & self . config ) . await ?;
572608
573- // Extract ChatGPT account ID from the access token JWT.
574- // This is used for the optional `ChatGPT-Account-Id` request
575- // header when available.
576- let account_id = extract_chatgpt_account_id ( & token_response. access_token ) ;
577-
609+ let access_token = token_response. access_token . clone ( ) ;
610+ let id_token = token_response. id_token . clone ( ) ;
578611 let mut credential = build_oauth_credential (
579612 self . provider_id . clone ( ) ,
580613 token_response,
@@ -583,12 +616,13 @@ impl AuthStrategy for CodexDeviceStrategy {
583616 ) ?;
584617
585618 // Store account_id in url_params so it's persisted and available
586- // for chat request headers
587- if let Some ( id) = account_id {
588- credential
589- . url_params
590- . insert ( "chatgpt_account_id" . to_string ( ) . into ( ) , id. into ( ) ) ;
591- }
619+ // for chat request headers.
620+ enrich_codex_oauth_credential (
621+ & self . provider_id ,
622+ & mut credential,
623+ id_token. as_deref ( ) ,
624+ & access_token,
625+ ) ;
592626
593627 Ok ( credential)
594628 }
0 commit comments