codex_login/
token_data.rs1use base64::Engine;
2use chrono::DateTime;
3use chrono::Utc;
4use codex_protocol::auth::PlanType;
5use serde::Deserialize;
6use serde::Serialize;
7use serde::de::DeserializeOwned;
8use thiserror::Error;
9
10#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Default)]
11pub struct TokenData {
12 #[serde(
14 deserialize_with = "deserialize_id_token",
15 serialize_with = "serialize_id_token"
16 )]
17 pub id_token: IdTokenInfo,
18
19 pub access_token: String,
21
22 pub refresh_token: String,
23
24 pub account_id: Option<String>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
29pub struct IdTokenInfo {
30 pub email: Option<String>,
31 pub chatgpt_plan_type: Option<PlanType>,
35 pub chatgpt_user_id: Option<String>,
37 pub chatgpt_account_id: Option<String>,
39 pub chatgpt_account_is_fedramp: bool,
41 pub raw_jwt: String,
42}
43
44impl IdTokenInfo {
45 pub fn get_chatgpt_plan_type(&self) -> Option<String> {
46 self.chatgpt_plan_type.as_ref().map(|t| match t {
47 PlanType::Known(plan) => plan.display_name().to_string(),
48 PlanType::Unknown(s) => s.clone(),
49 })
50 }
51
52 pub fn get_chatgpt_plan_type_raw(&self) -> Option<String> {
53 self.chatgpt_plan_type.as_ref().map(|t| match t {
54 PlanType::Known(plan) => plan.raw_value().to_string(),
55 PlanType::Unknown(s) => s.clone(),
56 })
57 }
58
59 pub fn is_workspace_account(&self) -> bool {
60 matches!(
61 self.chatgpt_plan_type,
62 Some(PlanType::Known(plan)) if plan.is_workspace_account()
63 )
64 }
65
66 pub fn is_fedramp_account(&self) -> bool {
67 self.chatgpt_account_is_fedramp
68 }
69}
70
71#[derive(Deserialize)]
72struct IdClaims {
73 #[serde(default)]
74 email: Option<String>,
75 #[serde(rename = "https://api.openai.com/profile", default)]
76 profile: Option<ProfileClaims>,
77 #[serde(rename = "https://api.openai.com/auth", default)]
78 auth: Option<AuthClaims>,
79}
80
81#[derive(Deserialize)]
82struct ProfileClaims {
83 #[serde(default)]
84 email: Option<String>,
85}
86
87#[derive(Deserialize)]
88struct AuthClaims {
89 #[serde(default)]
90 chatgpt_plan_type: Option<PlanType>,
91 #[serde(default)]
92 chatgpt_user_id: Option<String>,
93 #[serde(default)]
94 user_id: Option<String>,
95 #[serde(default)]
96 chatgpt_account_id: Option<String>,
97 #[serde(default)]
98 chatgpt_account_is_fedramp: bool,
99}
100
101#[derive(Deserialize)]
102struct StandardJwtClaims {
103 #[serde(default)]
104 exp: Option<i64>,
105}
106
107#[derive(Debug, Error)]
108pub enum IdTokenInfoError {
109 #[error("invalid ID token format")]
110 InvalidFormat,
111 #[error(transparent)]
112 Base64(#[from] base64::DecodeError),
113 #[error(transparent)]
114 Json(#[from] serde_json::Error),
115}
116
117fn decode_jwt_payload<T: DeserializeOwned>(jwt: &str) -> Result<T, IdTokenInfoError> {
118 let mut parts = jwt.split('.');
120 let (_header_b64, payload_b64, _sig_b64) = match (parts.next(), parts.next(), parts.next()) {
121 (Some(h), Some(p), Some(s)) if !h.is_empty() && !p.is_empty() && !s.is_empty() => (h, p, s),
122 _ => return Err(IdTokenInfoError::InvalidFormat),
123 };
124
125 let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(payload_b64)?;
126 let claims = serde_json::from_slice(&payload_bytes)?;
127 Ok(claims)
128}
129
130pub fn parse_jwt_expiration(jwt: &str) -> Result<Option<DateTime<Utc>>, IdTokenInfoError> {
131 let claims: StandardJwtClaims = decode_jwt_payload(jwt)?;
132 Ok(claims
133 .exp
134 .and_then(|exp| DateTime::<Utc>::from_timestamp(exp, 0)))
135}
136
137pub fn parse_chatgpt_jwt_claims(jwt: &str) -> Result<IdTokenInfo, IdTokenInfoError> {
138 let claims: IdClaims = decode_jwt_payload(jwt)?;
139 let email = claims
140 .email
141 .or_else(|| claims.profile.and_then(|profile| profile.email));
142
143 match claims.auth {
144 Some(auth) => Ok(IdTokenInfo {
145 email,
146 raw_jwt: jwt.to_string(),
147 chatgpt_plan_type: auth.chatgpt_plan_type,
148 chatgpt_user_id: auth.chatgpt_user_id.or(auth.user_id),
149 chatgpt_account_id: auth.chatgpt_account_id,
150 chatgpt_account_is_fedramp: auth.chatgpt_account_is_fedramp,
151 }),
152 None => Ok(IdTokenInfo {
153 email,
154 raw_jwt: jwt.to_string(),
155 chatgpt_plan_type: None,
156 chatgpt_user_id: None,
157 chatgpt_account_id: None,
158 chatgpt_account_is_fedramp: false,
159 }),
160 }
161}
162
163fn deserialize_id_token<'de, D>(deserializer: D) -> Result<IdTokenInfo, D::Error>
164where
165 D: serde::Deserializer<'de>,
166{
167 let s = String::deserialize(deserializer)?;
168 parse_chatgpt_jwt_claims(&s).map_err(serde::de::Error::custom)
169}
170
171fn serialize_id_token<S>(id_token: &IdTokenInfo, serializer: S) -> Result<S::Ok, S::Error>
172where
173 S: serde::Serializer,
174{
175 serializer.serialize_str(&id_token.raw_jwt)
176}
177
178#[cfg(test)]
179#[path = "token_data_tests.rs"]
180mod tests;