56 lines
1.7 KiB
Rust
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,
|
|
}
|
|
}
|