Files
cdxs/src/jwt.rs
T

56 lines
1.7 KiB
Rust

//! Minimal JWT helpers used for local token inspection.
//!
//! cdxs only decodes the payload to extract account metadata and expiry. It
//! does not verify signatures because validation is performed by OpenAI when
//! the token is used against the network APIs.
use anyhow::{anyhow, Context, Result};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct JwtPayload {
#[serde(default)]
pub email: Option<String>,
#[serde(default)]
pub sub: Option<String>,
#[serde(default)]
pub exp: Option<i64>,
#[serde(rename = "https://api.openai.com/auth", default)]
pub auth: Option<OpenAiAuth>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct OpenAiAuth {
#[serde(default)]
pub chatgpt_plan_type: Option<String>,
#[serde(default)]
pub account_id: Option<String>,
#[serde(default)]
pub organization_id: Option<String>,
}
pub fn decode_payload(token: &str) -> Result<JwtPayload> {
// JWT shape is header.payload.signature; only the base64url payload is
// needed for cdxs metadata.
let payload = token
.split('.')
.nth(1)
.ok_or_else(|| anyhow!("JWT 格式无效"))?;
let bytes = URL_SAFE_NO_PAD
.decode(payload)
.context("JWT payload base64 解码失败")?;
serde_json::from_slice(&bytes).context("JWT payload JSON 解析失败")
}
pub fn token_expired(token: &str, skew_seconds: i64) -> bool {
// Treat malformed tokens as expired so callers attempt refresh or reauth.
let Ok(payload) = decode_payload(token) else {
return true;
};
match payload.exp {
Some(exp) => chrono::Utc::now().timestamp() + skew_seconds >= exp,
None => false,
}
}