JWT claims from Sora parsed
This commit is contained in:
1 parent
878763c987
commit
64948e82a6
8 files changed
+204
-3
No files matched your search
@@ -40,4 +40,4 @@ rand = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
testing = { path = "../../testing" }
|
||||
filesys = { path = "../../files/filesys" }
|
||||
filesys = { path = "../../files/filesys" }
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod sora_jwt_bearer_token;
|
||||
pub mod sora_cookies;
|
||||
pub mod sora_sentinel;
|
||||
@@ -0,0 +1,15 @@
|
||||
/// Cookies are the credential that are always required.
|
||||
/// You can mint a JWT bearer token with just the cookies.
|
||||
pub struct SoraCookies {
|
||||
cookies: String,
|
||||
}
|
||||
|
||||
impl SoraCookies {
|
||||
pub fn new(cookies: String) -> Self {
|
||||
SoraCookies { cookies }
|
||||
}
|
||||
|
||||
pub fn get_cookies(&self) -> &str {
|
||||
&self.cookies
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use crate::utils::lightweight_jwt_parse::{lightweight_jwt_parse, SoraJwtClaims};
|
||||
use errors::AnyhowResult;
|
||||
|
||||
/// Sora bearer tokens are JWT tokens that can be minted with a valid cookie/session.
|
||||
pub struct SoraJwtBearerToken {
|
||||
token: String,
|
||||
jwt_claims: SoraJwtClaims,
|
||||
}
|
||||
|
||||
impl SoraJwtBearerToken {
|
||||
pub fn new(token: String) -> AnyhowResult<Self> {
|
||||
let jwt_claims = lightweight_jwt_parse(&token)?;
|
||||
Ok(SoraJwtBearerToken {
|
||||
token,
|
||||
jwt_claims,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn token_str(&self) -> &str {
|
||||
&self.token
|
||||
}
|
||||
|
||||
pub fn jwt_claims(&self) -> &SoraJwtClaims {
|
||||
&self.jwt_claims
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
/// Sora Sentinel tokens can be generated without cookies or JWTs, and are needed for some API calls.
|
||||
pub struct SoraSentinel {
|
||||
sentinel: String,
|
||||
}
|
||||
|
||||
impl SoraSentinel {
|
||||
pub fn new(sentinel: String) -> Self {
|
||||
SoraSentinel { sentinel }
|
||||
}
|
||||
|
||||
pub fn get_sentinel(&self) -> &str {
|
||||
&self.sentinel
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
pub mod bearer;
|
||||
pub mod credentials;
|
||||
pub mod creds;
|
||||
pub mod image_gen;
|
||||
pub mod job;
|
||||
pub mod upload;
|
||||
pub mod sentinel_refresh;
|
||||
mod sora_error;
|
||||
pub mod sora_error;
|
||||
pub mod upload;
|
||||
pub mod utils;
|
||||
@@ -0,0 +1,139 @@
|
||||
//! Adapted from 'jwt' crate
|
||||
|
||||
use anyhow::anyhow;
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::warn;
|
||||
use serde_json::Value;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
const SEPARATOR: &str = ".";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SoraJwtClaims {
|
||||
pub created: DateTime<Utc>,
|
||||
pub expiration: DateTime<Utc>,
|
||||
pub user_id: String,
|
||||
pub email: String,
|
||||
pub email_verified: bool,
|
||||
}
|
||||
|
||||
pub fn lightweight_jwt_parse(token: &str) -> AnyhowResult<SoraJwtClaims> {
|
||||
let [_header_str, claims_str, _signature_str] = split_components(token)?;
|
||||
|
||||
let claims = decode_base64(claims_str)?;
|
||||
let claims : serde_json::Map<String, Value> = serde_json::from_str(&claims)?;
|
||||
|
||||
let iat = claims.get("iat")
|
||||
.map(|val| val.as_number())
|
||||
.flatten()
|
||||
.ok_or(anyhow!("no iat claim"))?;
|
||||
|
||||
let iat = iat.as_i64()
|
||||
.ok_or(anyhow!("iat is not a number"))?;
|
||||
|
||||
let iat = DateTime::from_timestamp(iat, 0)
|
||||
.ok_or(anyhow!("iat is not a valid timestamp"))?;
|
||||
|
||||
let exp = claims.get("exp")
|
||||
.map(|val| val.as_number())
|
||||
.flatten()
|
||||
.ok_or(anyhow!("no exp claim"))?;
|
||||
|
||||
let exp = exp.as_i64()
|
||||
.ok_or(anyhow!("exp is not a number"))?;
|
||||
|
||||
let exp = DateTime::from_timestamp(exp, 0)
|
||||
.ok_or(anyhow!("iat is not a valid timestamp"))?;
|
||||
|
||||
let user_id = claims.get("https://api.openai.com/auth")
|
||||
.map(|val| val.get("user_id"))
|
||||
.flatten()
|
||||
.map(|val| val.as_str())
|
||||
.flatten()
|
||||
.ok_or(anyhow!("no user_id claim"))?;
|
||||
|
||||
let profile = claims.get("https://api.openai.com/profile")
|
||||
.ok_or(anyhow!("no user_id claim"))?;
|
||||
|
||||
let email = profile.get("email")
|
||||
.map(|val| val.as_str())
|
||||
.flatten()
|
||||
.ok_or(anyhow!("no email claim"))?;
|
||||
|
||||
let email_verified = profile.get("email_verified")
|
||||
.map(|val| val.as_bool())
|
||||
.flatten()
|
||||
.unwrap_or_else(|| {
|
||||
warn!("no email_verified claim");
|
||||
false
|
||||
});
|
||||
|
||||
Ok(SoraJwtClaims {
|
||||
created: iat,
|
||||
expiration: exp,
|
||||
user_id: user_id.to_string(),
|
||||
email: email.to_string(),
|
||||
email_verified,
|
||||
})
|
||||
}
|
||||
|
||||
fn split_components(token: &str) -> AnyhowResult<[&str; 3]> {
|
||||
let mut components = token.split(SEPARATOR);
|
||||
let header = components.next().ok_or(anyhow!("no header component"))?;
|
||||
let claims = components.next().ok_or(anyhow!("no claims component"))?;
|
||||
let signature = components.next().ok_or(anyhow!("no signature component"))?;
|
||||
|
||||
if components.next().is_some() {
|
||||
return Err(anyhow!("too many components"));
|
||||
}
|
||||
|
||||
Ok([header, claims, signature])
|
||||
}
|
||||
|
||||
fn decode_base64(raw: &str) -> AnyhowResult<String> {
|
||||
let json_bytes = BASE64_URL_SAFE_NO_PAD.decode(raw)?;
|
||||
let json_str = String::from_utf8(json_bytes.clone())?;
|
||||
Ok(json_str)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::lightweight_jwt_parse::lightweight_jwt_parse;
|
||||
|
||||
#[test]
|
||||
fn test_sora_jwt_bearer_token() {
|
||||
let token =
|
||||
"eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6Ik\
|
||||
pXVCJ9.eyJhdWQiOlsiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS92MSJdLCJjbGllbnRfaWQiOiJhcHBfTTFuUTN0UjV2\
|
||||
VzdYOWpMMnBFNmdIOGRLNCIsImV4cCI6MTc0NTgwMDQ3MiwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9hdXRoIjp7InV\
|
||||
zZXJfaWQiOiJ1c2VyLTZOeEpmbEFIb0VCREp6Wmw5aVhocWJERyJ9LCJodHRwczovL2FwaS5vcGVuYWkuY29tL3Byb2\
|
||||
ZpbGUiOnsiZW1haWwiOiJ2b2NvZGVzMjAyMEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZX0sImlhdCI6M\
|
||||
Tc0NDkzNjQ3MSwiaXNzIjoiaHR0cHM6Ly9hdXRoLm9wZW5haS5jb20iLCJqdGkiOiI0NTZmZjY5Yi0xZTZhLTQzOWYt\
|
||||
YTYzMC1jNmFjYzI3NzU1YjAiLCJuYmYiOjE3NDQ5MzY0NzEsInB3ZF9hdXRoX3RpbWUiOjE3NDQwNTkzMTc3NTAsInN\
|
||||
jcCI6WyJvcGVuaWQiLCJlbWFpbCIsInByb2ZpbGUiLCJvZmZsaW5lX2FjY2VzcyJdLCJzZXNzaW9uX2lkIjoiYXV0aH\
|
||||
Nlc3NfcEE4UGl3V0trNmJsSE1qQnVCQnVSRmh2Iiwic3ViIjoiZ29vZ2xlLW9hdXRoMnwxMTMxMDE5Njc2MTIzOTY3O\
|
||||
TM3NzcifQ.JOxYfrQSIsHdf-VN1zcOqDOS3tF1HpUYnRVfPKgi65am3sNiniOM1kWTNrKE5-sh4DaZ33G_XniGkhg-9\
|
||||
QKF8tspXpZ6BnTNkf-QdGqR_v89AIO8pZgQ7oECLXhnmg-31G-LAHlGfwWedJ6qPVy4dL5zoR0wVyoQ8gYv5QzQC-zd\
|
||||
IF7uY-umKKloAAYP_tkKLlQHackjTynN-bpu2mKv55-h7a3hSEPfxvgX0SsvL69xtNsZkwb43bqyP1x_c2ErfYoPyzg\
|
||||
rqgMIRZjCw9TI33vISKM_BfQXBiZT_BHkX4s0Jng3ph4vrwvnBsa0HpNxBhQYKb1u9gZXs7XjLZhjZJZsDTQI1V4Wov\
|
||||
iAL7ihvZkLxJRtDCOmX7-5BTEsMInFgXUAhCDhQUMwSfLLJuinBS96NGSX5_TfIVMkbCl-HWzdQx7KZAsahNe_CmuYT\
|
||||
lZH32NuRn78ohY4eViRyfC8OXVLwoLxxpen2CWlV2dxV5wwXeij_kHt5wOXSGYpSfxz37cK0CaK8K2pwudasxQ0lv2V\
|
||||
_Xx_z4UaEOr5wqT_BK1A1HO4HRjvFX2soF-qm4GCSWAbmFZs7DVEClrotxRx2romifRVb8XRia2M6YTmDyIT0YphFgG\
|
||||
nnrDIZNKO23uDYT60LVMVYpCCY2RiwxCR9bn7AeGbmi7cpL9aYZ5uJRM";
|
||||
|
||||
// jwt payload :
|
||||
// - "https://api.openai.com/auth".user_id = "user-6NxJflAHoEBDJzZl9iXhqbDG"
|
||||
// - "https://api.openai.com/profile".email = "vocodes2020@gmail.com"
|
||||
// - "https://api.openai.com/profile".email_verified = true
|
||||
// - "iat": 1744936471, - issued at claim (5 days ago)
|
||||
// - "nbf": 1744936471, - not before, don't use before date
|
||||
// - "exp": 1745800472, - in 5 days, expiry
|
||||
|
||||
let result = lightweight_jwt_parse(token).expect("claim should parse");
|
||||
|
||||
assert_eq!(result.email, "vocodes2020@gmail.com");
|
||||
assert_eq!(result.email_verified, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod lightweight_jwt_parse;
|
||||
Reference in new issue
Block a user