Compare commits

...
Author SHA1 Message Date
Brandon Thomas f1c22c7c47 leave runwayml unimplemented 2025-05-20 23:38:17 -04:00
Brandon Thomas 2275526684 request wip 2025-05-20 21:17:35 -04:00
Brandon Thomas a7db67900d error codes wip 2025-05-20 20:36:27 -04:00
Brandon Thomas 75be0c17a2 wip creds 2025-05-20 20:36:27 -04:00
Brandon Thomas 62ad9c484a jwt parsing 2025-05-20 20:36:27 -04:00
Brandon Thomas 48f9cd405b wip runway client, jwt handling library 2025-05-20 20:36:27 -04:00
Brandon Thomas 7febc6a1d8 errors 2025-05-20 20:36:27 -04:00
Brandon Thomas 30daca8462 runwayml crate 2025-05-20 20:36:27 -04:00
26 changed files with 806 additions and 1 deletions
Generated
+40
View File
@@ -6496,6 +6496,16 @@ dependencies = [
"zeroize",
]
[[package]]
name = "jwt_light"
version = "0.0.1"
dependencies = [
"base64 0.22.1",
"chrono",
"serde_json",
"workspace-build-acceleration",
]
[[package]]
name = "k256"
version = "0.13.3"
@@ -10301,6 +10311,36 @@ dependencies = [
"realfft",
]
[[package]]
name = "runwayml_client"
version = "0.0.1"
dependencies = [
"anyhow",
"base64 0.22.1",
"chrono",
"cookie 0.18.1",
"crockford",
"errors",
"filesys",
"hex-literal",
"idempotency",
"jwt_light",
"log",
"once_cell",
"rand 0.9.1",
"reqwest 0.12.15",
"serde",
"serde_derive",
"serde_json",
"sha3",
"subprocess_common",
"testing",
"thiserror 2.0.12",
"tokio",
"toml 0.8.22",
"workspace-build-acceleration",
]
[[package]]
name = "rust-embed"
version = "8.0.0"
+3
View File
@@ -34,6 +34,7 @@ members = [
"crates/lib/api_clients/google_drive_common",
"crates/lib/api_clients/google_sign_in",
"crates/lib/api_clients/openai_sora_client",
"crates/lib/api_clients/runwayml_client",
"crates/lib/api_clients/storyteller_client",
"crates/lib/api_clients/thumbnail_generator",
"crates/lib/bootstrap",
@@ -65,6 +66,7 @@ members = [
"crates/lib/idempotency",
"crates/lib/image_algorithms",
"crates/lib/jobs_common",
"crates/lib/jwt_light",
"crates/lib/logging",
"crates/lib/machine_learning/ml_models",
"crates/lib/machine_learning/ml_weights_registry",
@@ -191,6 +193,7 @@ fal_client = { path = "crates/lib/api_clients/fal_client" }
filesys = { path = "crates/lib/files/filesys" }
idempotency = { path = "crates/lib/idempotency" }
images = { path = "crates/lib/files/images" }
jwt_light = { path = "crates/lib/jwt_light" }
memory_caching = { path = "crates/lib/caching/memory_caching" }
memory_store = { path = "crates/lib/caching/memory_store" }
mimetypes = { path = "crates/lib/files/mimetypes" }
@@ -1,6 +1,6 @@
[package]
name = "openai_sora_client"
edition = "2021"
edition = "2024"
version = "0.0.1"
publish = false
@@ -0,0 +1,46 @@
[package]
name = "runwayml_client"
edition = "2021"
version = "0.0.1"
publish = false
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[lib]
name = "runwayml_client"
path = "src/lib.rs"
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
crockford = { path = "../../data/crockford" }
errors = { path = "../../errors" }
idempotency = { path = "../../idempotency" }
jwt_light = { workspace = true }
subprocess_common = { path = "../../subprocess_common" }
# External
anyhow = { workspace = true }
base64 = { workspace = true }
chrono = { workspace = true}
cookie = { workspace = true }
hex-literal = { workspace = true }
log = { workspace = true }
once_cell= { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true, features = ["multipart", "gzip"] }
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
sha3 = { workspace = true }
thiserror = { workspace = true }
tokio = { version = "1.38.0", features = ["macros"] }
toml = { workspace = true }
[dev-dependencies]
testing = { path = "../../testing" }
filesys = { path = "../../files/filesys" }
@@ -0,0 +1,16 @@
use crate::creds::jwt_bearer_token::JwtBearerToken;
use crate::error::api_error::ApiError;
#[derive(Clone)]
pub struct Credentials {
pub jwt_bearer_token: Option<JwtBearerToken>,
}
impl Credentials {
pub fn from_jwt_str(jwt_str: &str) -> Result<Self, ApiError> {
let jwt_bearer_token = JwtBearerToken::new(jwt_str.to_string())?;
Ok(Self {
jwt_bearer_token: Some(jwt_bearer_token),
})
}
}
@@ -0,0 +1,39 @@
use crate::creds::jwt_claims::JwtClaims;
use crate::error::api_error::ApiError;
use jwt_light::parse_jwt_claims_trait::ParseJwtClaims;
/// Sora bearer tokens are JWT tokens that can be minted with a valid cookie/session.
#[derive(Clone)]
pub struct JwtBearerToken {
raw_jwt: String,
jwt_claims: JwtClaims,
}
impl JwtBearerToken {
pub fn new(raw_jwt: String) -> Result<Self, ApiError> {
let jwt_claims = JwtClaims::parse_claims(&raw_jwt)?;
Ok(Self {
raw_jwt,
jwt_claims,
})
}
pub fn raw_jwt_str(&self) -> &str {
&self.raw_jwt
}
pub fn as_bytes(&self) -> &[u8] {
self.raw_jwt.as_bytes()
}
pub fn jwt_claims(&self) -> &JwtClaims {
&self.jwt_claims
}
pub fn to_authorization_header_value(&self) -> String {
match self.raw_jwt.get(0..6) {
Some("bearer") | Some("Bearer") => self.raw_jwt.clone(),
_ => "Bearer ".to_owned() + &self.raw_jwt,
}
}
}
@@ -0,0 +1,75 @@
use base64::Engine;
use chrono::{DateTime, Utc};
use jwt_light::common_claims::CommonClaims;
use jwt_light::error::JwtError;
use jwt_light::parse_jwt_claims_trait::ParseJwtClaims;
use serde_json::{Map, Value};
#[derive(Clone, Debug)]
pub struct JwtClaims {
/// From the "iat" field
pub created: DateTime<Utc>,
/// From the "exp" field
pub expiration: DateTime<Utc>,
/// From the "id" field
pub id: String,
/// From the "email" field
pub email: String,
/// From the "sso" field
pub sso: bool,
}
impl ParseJwtClaims for JwtClaims {
fn extract_claims(common_claims: CommonClaims, extra_claims: Map<String, Value>) -> Result<Self, JwtError> {
let id = extra_claims.get("id")
.ok_or_else(|| JwtError::CustomClaimsFieldError("no id claim".to_string()))?
.as_i64()
.ok_or_else(|| JwtError::CustomClaimsFieldError("id is not a string".to_string()))?
.to_string();
let email = extra_claims.get("email")
.ok_or_else(|| JwtError::CustomClaimsFieldError("no email claim".to_string()))?
.as_str()
.ok_or_else(|| JwtError::CustomClaimsFieldError("email is not a string".to_string()))?
.to_string();
let sso = extra_claims.get("sso")
.map(|val| val.as_bool())
.flatten()
.unwrap_or(false);
Ok(Self {
created: common_claims.created,
expiration: common_claims.expiration,
id,
email,
sso,
})
}
}
#[cfg(test)]
mod tests {
use crate::creds::jwt_claims::JwtClaims;
use jwt_light::parse_jwt_claims_trait::ParseJwtClaims;
#[test]
fn test_token() {
/*{
"id": 23129405,
"email": "echelon@gmail.com",
"exp": 1750201256.49,
"iat": 1747609256.49,
"sso": false
}*/
let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MjMxMjk0MDUsImVtYWlsIjoiZWNoZWxvbkBnbWFpbC5jb20iLCJleHAiOjE3NTAyMDEyNTYuNDksImlhdCI6MTc0NzYwOTI1Ni40OSwic3NvIjpmYWxzZX0.UxbJozIiHSApqI8_Vl7o2d7q7CzqpXIzsZoazCtY75s";
let claims = JwtClaims::parse_claims(jwt).unwrap();
assert_eq!(claims.id, "23129405");
assert_eq!(claims.email, "echelon@gmail.com");
assert_eq!(claims.sso, false);
assert_eq!(claims.created.timestamp(), 1747609256);
assert_eq!(claims.expiration.timestamp(), 1750201256);
}
}
@@ -0,0 +1,3 @@
pub mod credentials;
pub mod jwt_bearer_token;
pub mod jwt_claims;
@@ -0,0 +1,39 @@
use jwt_light::error::JwtError;
use reqwest::StatusCode;
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum ApiError {
/// Inability to parse JWT.
JwtError(JwtError),
/// An error that occurred as or after the request was sent.
ReqwestError(reqwest::Error),
/// Some other failure response.
UncategorizedBadResponse{
status_code: StatusCode,
message : String,
}
}
impl Display for ApiError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ApiError::JwtError(err) => write!(f, "JwtError: {}", err),
ApiError::ReqwestError(err) => write!(f, "ReqwestError: {}", err),
ApiError::UncategorizedBadResponse { status_code, message } => {
write!(f, "UncategorizedBadResponse: {:?} : {}", status_code, message)
}
}
}
}
impl Error for ApiError {}
impl From<JwtError> for ApiError {
fn from(err: JwtError) -> Self {
ApiError::JwtError(err)
}
}
@@ -0,0 +1,17 @@
use crate::error::api_error::ApiError;
use crate::error::runwayml_error::RunwayMlError;
/// This assumes the request failed and returned a non-200.
/// The caller should check.
pub async fn classify_http_error_response(response: reqwest::Response) -> RunwayMlError {
let status = response.status();
let message = match response.text().await {
Ok(text) => text,
Err(err) => return RunwayMlError::Api(ApiError::ReqwestError(err)),
};
// TODO: Handle various error messages from the API.
//anyhow::anyhow!("Upload failed with status {}: {}", status, message))
RunwayMlError::Api(ApiError::UncategorizedBadResponse { status_code: status, message })
}
@@ -0,0 +1,22 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum ClientError {
/// No JWT bearer token in the credentials
NoJwtBearerToken,
/// An error that occurred clientside before the request was sent.
ReqwestError(reqwest::Error),
}
impl Display for ClientError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ClientError::NoJwtBearerToken => write!(f, "No JWT bearer token in the credentials"),
ClientError::ReqwestError(err) => write!(f, "Reqwest error: {}", err),
}
}
}
impl Error for ClientError {}
@@ -0,0 +1,4 @@
pub mod api_error;
pub mod classify_http_error_response;
pub mod client_error;
pub mod runwayml_error;
@@ -0,0 +1,33 @@
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum RunwayMlError {
Api(ApiError),
Client(ClientError),
}
impl Display for RunwayMlError{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Api(err) => write!(f, "ApiError: {}", err),
Self::Client(err) => write!(f, "ClientError: {}", err),
}
}
}
impl Error for RunwayMlError {}
impl From<ApiError> for RunwayMlError {
fn from(err: ApiError) -> Self {
RunwayMlError::Api(err)
}
}
impl From<ClientError> for RunwayMlError {
fn from(err: ClientError) -> Self {
RunwayMlError::Client(err)
}
}
@@ -0,0 +1,5 @@
/// TODO DOCS
pub mod creds;
pub mod error;
pub mod requests;
@@ -0,0 +1,158 @@
use crate::creds::credentials::Credentials;
use crate::error::api_error::ApiError;
use crate::error::classify_http_error_response::classify_http_error_response;
use crate::error::client_error::ClientError;
use crate::error::runwayml_error::RunwayMlError;
use log::{debug, error, warn};
use reqwest::Client;
use serde_derive::{Deserialize, Serialize};
const GENERATE_URL: &str = "https://api.runwayml.com/v1/tasks";
// TODO: This endpoint is not implemented yet.
// TODO: This endpoint is not implemented yet.
// TODO: This endpoint is not implemented yet.
#[derive(Serialize)]
struct TaskRequest {
/// eg. "gen4"
#[serde(rename = "taskType")]
task_type: String,
/// eg. false
internal: bool,
/// inner request payload
options: TaskRequestOptions,
/// eg. 23129405
#[serde(rename = "asTeamId")]
as_team_id: u64,
/// eg. fc66957b-2651-4362-aca0-5468e4ee61b0
#[serde(rename = "sessionId")]
session_id: String,
}
#[derive(Serialize)]
struct TaskRequestOptions {
/// eg. Gen-4 3438043024
name: String,
/// eg. 3438043024
seed: u64,
/// Probably whether the request is free (exploreMode = unlimited queue)
#[serde(rename = "exploreMode")]
explore_mode: bool,
/// eg. 5
seconds: u8,
/// eg. false
watermark: bool,
/// This can be blank / empty string!
text_prompt: String,
/// eg. "i2v"
route: String,
/// A URL, eg. https://d2jqrm6oza8nb6.cloudfront.net/datasets/586d77ff-b33b-4100-b4d3-c04f8b046b00.jpg?_jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXlIYXNoIjoiNTJhMTY0N2RkZjFjMGU3NCIsImJ1Y2tldCI6InJ1bndheS1kYXRhc2V0cyIsInN0YWdlIjoicHJvZCIsImV4cCI6MTc0Nzc4NTYwMH0.N_TztqlU2pzPegOq7e8tn_6zb16OLmDmerlTtfr_-FI
init_image: String,
/// eg. 1280
width: u16,
/// eg. 720
height: u16,
/// eg. b5c42f29-d63d-4124-88c0-019ab1299829
#[serde(rename = "assetGroupId")]
asset_group_id: String,
}
pub async fn gen4_image_to_video(
credentials: &Credentials,
) -> Result<(), RunwayMlError> {
let jwt = match credentials.jwt_bearer_token.as_ref() {
Some(token) => token,
None => {
error!("Failed to generate bearer token. No JWT bearer token found.");
return Err(ClientError::NoJwtBearerToken.into());
}
};
let endpoint_request = TaskRequest {
task_type: "gen4".to_string(),
internal: false,
options: TaskRequestOptions {
name: "Gen-4 3438043025".to_string(),
seed: 3438043025,
explore_mode: true,
seconds: 5,
watermark: false,
text_prompt: "".to_string(),
route: "i2v".to_string(),
init_image: "https://d2jqrm6oza8nb6.cloudfront.net/datasets/586d77ff-b33b-4100-b4d3-c04f8b046b00.jpg?_jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXlIYXNoIjoiNTJhMTY0N2RkZjFjMGU3NCIsImJ1Y2tldCI6InJ1bndheS1kYXRhc2V0cyIsInN0YWdlIjoicHJvZCIsImV4cCI6MTc0Nzc4NTYwMH0.N_TztqlU2pzPegOq7e8tn_6zb16OLmDmerlTtfr_-FI".to_string(),
width: 1280,
height: 720,
asset_group_id: "b5c42f29-d63d-4124-88c0-019ab1299829".to_string(),
},
as_team_id: 23129405,
session_id: "fc66957b-2651-4362-aca0-5468e4ee61b0".to_string(),
};
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| {
error!("Failed to create HTTP client: {}", err);
RunwayMlError::Client(ClientError::ReqwestError(err))
})?;
let http_request = client.post(GENERATE_URL)
.header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0")
.header("Accept", "application/json")
.header("Accept-Encoding", "gzip, deflate, br")
.header("Accept-Language", "en-US,en;q=0.5")
.header("Content-Type", "application/json")
.header("Authorization", jwt.to_authorization_header_value());
let http_request = http_request.json(&endpoint_request)
.build()
.map_err(|err| RunwayMlError::Client(ClientError::ReqwestError(err)))?;
let response = client.execute(http_request)
.await
.map_err(|err| RunwayMlError::Api(ApiError::ReqwestError(err)))?;
let status = response.status();
if !status.is_success() {
warn!("Gen4 image to video failure. Status: {:?}", status);
let error = classify_http_error_response(response).await;
return Err(error);
}
debug!("Gen4 I2V returned a 200.");
let response_body = &response
.text()
.await
.map_err(|err| RunwayMlError::Api(ApiError::ReqwestError(err)))?;
debug!("Response: {}", response_body);
//if &response_body == "null" {
// error!("Failed to generate bearer token. Response was `null`.");
// return Err(SoraError::FailedToGenerateBearer)
//}
//let auth_response: SoraAuthResponse = serde_json::from_str(&response_body)?;
// TODO: This endpoint is not implemented yet.
// TODO: This endpoint is not implemented yet.
// TODO: This endpoint is not implemented yet.
todo!("This is not yet implemented")
}
#[cfg(test)]
mod tests {
use errors::AnyhowResult;
#[tokio::test]
#[ignore]
async fn send_test_request() -> AnyhowResult<()> {
Ok(())
}
}
@@ -0,0 +1 @@
pub mod gen4_image_to_video;
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "jwt_light"
edition = "2024"
version = "0.0.1"
publish = false
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[lib]
name = "jwt_light"
path = "src/lib.rs"
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
# (None)
# External
base64 = { workspace = true }
chrono = { workspace = true}
serde_json = { workspace = true }
[dev-dependencies]
# (None)
+109
View File
@@ -0,0 +1,109 @@
use crate::error::JwtError;
use crate::utils::json_number_to_datetime::json_number_to_datetime;
use chrono::{DateTime, Utc};
use serde_json::Value;
pub struct CommonClaims {
/// From the "iat" field
pub created: DateTime<Utc>,
/// From the "exp" field
pub expiration: DateTime<Utc>,
}
impl CommonClaims {
pub fn from_json(raw_json_claims: &str) -> Result<Self, JwtError> {
let claims : serde_json::Map<String, Value> = serde_json::from_str(&raw_json_claims)
.map_err(|err| JwtError::CommonFieldError(
format!("failure to parse claims json: {:?}", err)))?;
// NB: Some JWTs have "iat" and "exp" as integer numbers, some have them as floats
let iat = {
let iat = claims.get("iat")
.map(|val| val.as_number())
.flatten()
.ok_or_else(|| JwtError::CommonFieldError("no iat claim".to_string()))?;
json_number_to_datetime(iat, "iat")?
};
let exp = {
let exp = claims.get("exp")
.map(|val| val.as_number())
.flatten()
.ok_or_else(|| JwtError::CommonFieldError("no exp claim".to_string()))?;
json_number_to_datetime(exp, "exp")?
};
Ok(Self {
created: iat,
expiration: exp,
})
}
}
#[cfg(test)]
mod tests {
use crate::common_claims::CommonClaims;
use crate::utils::raw_jwt_to_raw_json::raw_jwt_to_raw_json;
use chrono::DateTime;
#[test]
fn test_runwayml() {
let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MjMxMjk0MDUsImVtYWlsIjoiZWNoZWxvbkBnbWFpbC5jb20iLCJleHAiOjE3NTAyMDEyNTYuNDksImlhdCI6MTc0NzYwOTI1Ni40OSwic3NvIjpmYWxzZX0.UxbJozIiHSApqI8_Vl7o2d7q7CzqpXIzsZoazCtY75s";
let json = raw_jwt_to_raw_json(jwt).unwrap();
let claims = CommonClaims::from_json(&json).unwrap();
// jwt payload:
// {
// "id": 23129405,
// "email": "echelon@gmail.com",
// "exp": 1750201256.49,
// "iat": 1747609256.49,
// "sso": false
// }
assert_eq!(claims.created, DateTime::from_timestamp(1747609256, 0).unwrap());
assert_eq!(claims.expiration, DateTime::from_timestamp(1750201256, 0).unwrap());
}
#[test]
fn test_sora() {
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";
let json = raw_jwt_to_raw_json(token).unwrap();
// 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 claims = CommonClaims::from_json(&json).unwrap();
assert_eq!(claims.created, DateTime::from_timestamp(1744936471, 0).unwrap());
assert_eq!(claims.expiration, DateTime::from_timestamp(1745800472, 0).unwrap());
}
}
+51
View File
@@ -0,0 +1,51 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum JwtError {
/// Generic error parsing the JWT
ParseError(String),
/// Error parsing the JWT JSON payload specifically
JsonParseError(serde_json::Error),
/// Error decoding the base64 payload
Base64DecodeError(base64::DecodeError),
/// Error decoding from UTF-8 (should be exceedingly rare)
Utf8Error(std::string::FromUtf8Error),
/// Error parsing a common field (iat, exp, etc.)
CommonFieldError(String),
/// Error parsing a custom claims field.
CustomClaimsFieldError(String),
}
impl Error for JwtError {}
impl Display for JwtError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::ParseError(reason) => { write!(f, "JwtError::ParseError : {}", reason) }
Self::JsonParseError(err) => { write!(f, "JwtError::JsonParseError : {:?}", err) }
Self::Base64DecodeError(err) => { write!(f, "JwtError::Base64DecodeError : {:?}", err) }
Self::Utf8Error(err) => { write!(f, "JwtError::Utf8Error : {:?}", err) }
Self::CommonFieldError(reason) => { write!(f, "JwtError::CommonFieldError : {}", reason) }
Self::CustomClaimsFieldError(reason) => { write!(f, "JwtError::CustomClaimsFieldError : {}", reason) }
}
}
}
impl From<serde_json::Error> for JwtError {
fn from(err: serde_json::Error) -> Self {
JwtError::JsonParseError(err)
}
}
impl From<base64::DecodeError> for JwtError {
fn from(err: base64::DecodeError) -> Self {
JwtError::Base64DecodeError(err)
}
}
impl From<std::string::FromUtf8Error> for JwtError {
fn from(err: std::string::FromUtf8Error) -> Self {
JwtError::Utf8Error(err)
}
}
+12
View File
@@ -0,0 +1,12 @@
//! Lightweight JWT library for Rust.
//! No cryptographic verification, no OpenSSL, etc.
//!
//! DO NOT USE THIS FOR OUR OWN JWT VERIFICATION !!!
//! It's okay to parse external vendors since we're not validating, but
//! do not ever use this to secure our own server.
pub (crate) mod utils;
pub mod common_claims;
pub mod error;
pub mod parse_jwt_claims_trait;
@@ -0,0 +1,27 @@
use crate::common_claims::CommonClaims;
use crate::error::JwtError;
use crate::utils::decode_base64::decode_base64;
use crate::utils::split_components::split_components;
use serde_json::Value;
/// Trait for simple JWT claims parsing
pub trait ParseJwtClaims : Sized {
/// Parse any special fields relevant to the struct
/// Implement this method to complete the parsing.
fn extract_claims(
common_claims: CommonClaims,
extra_claims: serde_json::Map<String, Value>,
) -> Result<Self, JwtError>;
fn parse_claims(raw_jwt: &str) -> Result<Self, JwtError> {
let [_header_str, claims_str, _signature_str] = split_components(raw_jwt)?;
let raw_json = decode_base64(claims_str)?;
let common_claims = CommonClaims::from_json(&raw_json)?;
let extra_claims : serde_json::Map<String, Value> = serde_json::from_str(&raw_json)?;
Self::extract_claims(common_claims, extra_claims)
}
}
@@ -0,0 +1,9 @@
use crate::error::JwtError;
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
use base64::Engine;
pub fn decode_base64(raw: &str) -> Result<String, JwtError> {
let json_bytes = BASE64_URL_SAFE_NO_PAD.decode(raw)?;
let json_str = String::from_utf8(json_bytes.clone())?;
Ok(json_str)
}
@@ -0,0 +1,30 @@
use crate::error::JwtError;
use chrono::{DateTime, Utc};
use serde_json::Number;
// NB: Some JWTs have "iat" and "exp" as integer numbers, some have them as floats
pub (crate) fn json_number_to_datetime(timestamp: &Number, field_name: &str) -> Result<DateTime<Utc>, JwtError> {
let result;
if timestamp.is_i64() {
// Integer case
let timestamp = timestamp.as_i64()
.ok_or_else(|| JwtError::CommonFieldError(format!("{} is not an integer", field_name)))?;
result = DateTime::from_timestamp(timestamp, 0)
.ok_or_else(|| JwtError::CommonFieldError(format!("{} is not a valid timestamp", field_name)))?;
} else if timestamp.is_f64() {
// Float case
let timestamp = timestamp.as_f64()
.ok_or_else(|| JwtError::CommonFieldError(format!("{} is not an integer", field_name)))?;
let timestamp = timestamp.round() as i64;
result = DateTime::from_timestamp(timestamp, 0)
.ok_or_else(|| JwtError::CommonFieldError(format!("{} is not a valid timestamp", field_name)))?;
} else {
return Err(JwtError::CommonFieldError(format!("{} is not a number", field_name)));
}
Ok(result)
}
+4
View File
@@ -0,0 +1,4 @@
pub (crate) mod decode_base64;
pub (crate) mod json_number_to_datetime;
pub (crate) mod raw_jwt_to_raw_json;
pub (crate) mod split_components;
@@ -0,0 +1,9 @@
use crate::error::JwtError;
use crate::utils::decode_base64::decode_base64;
use crate::utils::split_components::split_components;
pub (crate) fn raw_jwt_to_raw_json(raw_jwt: &str) -> Result<String, JwtError> {
let [_header_str, claims_str, _signature_str] = split_components(raw_jwt)?;
let raw_json = decode_base64(claims_str)?;
Ok(raw_json)
}
@@ -0,0 +1,25 @@
use crate::error::JwtError;
const SEPARATOR: &str = ".";
pub fn split_components(token: &str) -> Result<[&str; 3], JwtError> {
let mut components = token.split(SEPARATOR);
let header = components.next()
.ok_or_else(|| jwt_error("no header component"))?;
let claims = components.next()
.ok_or_else(|| jwt_error("no claims component"))?;
let signature = components.next()
.ok_or_else(|| jwt_error("no signature component"))?;
if components.next().is_some() {
return Err(jwt_error("too many components"));
}
Ok([header, claims, signature])
}
fn jwt_error(reason: &str) -> JwtError {
JwtError::ParseError(reason.to_string())
}