Something went wrong. Try again.
Our Personal Data Server from scratch!
Something went wrong. Try again.
9.0 kB · 322 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323use chrono::{DateTime, Utc};use serde::{Deserialize, Serialize, de, ser};use std::fmt;use std::str::FromStr;use tranquil_types::{Did, Jti, Nsid};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum TokenType { Access, Refresh, Service,}
impl TokenType { pub fn as_str(&self) -> &'static str { match self { Self::Access => "at+jwt", Self::Refresh => "refresh+jwt", // RFC 7519 §5.1 recommends the uppercase "JWT". // and for atproto inter-service auth its a requirement. Self::Service => "JWT", } }}
impl fmt::Display for TokenType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) }}
impl FromStr for TokenType { type Err = TokenTypeParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> { match s.to_ascii_lowercase().as_str() { "at+jwt" => Ok(Self::Access), "refresh+jwt" => Ok(Self::Refresh), "jwt" => Ok(Self::Service), _ => Err(TokenTypeParseError(s.to_string())), } }}
#[derive(Debug, Clone)]pub struct TokenTypeParseError(pub String);
impl fmt::Display for TokenTypeParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "unknown token type: {}", self.0) }}
impl std::error::Error for TokenTypeParseError {}
impl Serialize for TokenType { fn serialize<S: ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { serializer.serialize_str(self.as_str()) }}
impl<'de> Deserialize<'de> for TokenType { fn deserialize<D: de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { let s = String::deserialize(deserializer)?; Self::from_str(&s).map_err(de::Error::custom) }}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum SigningAlgorithm { ES256K, HS256,}
impl SigningAlgorithm { pub fn as_str(&self) -> &'static str { match self { Self::ES256K => "ES256K", Self::HS256 => "HS256", } }}
impl fmt::Display for SigningAlgorithm { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) }}
impl FromStr for SigningAlgorithm { type Err = SigningAlgorithmParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> { match s.to_ascii_uppercase().as_str() { "ES256K" => Ok(Self::ES256K), "HS256" => Ok(Self::HS256), _ => Err(SigningAlgorithmParseError(s.to_string())), } }}
#[derive(Debug, Clone)]pub struct SigningAlgorithmParseError(pub String);
impl fmt::Display for SigningAlgorithmParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "unknown signing algorithm: {}", self.0) }}
impl std::error::Error for SigningAlgorithmParseError {}
impl Serialize for SigningAlgorithm { fn serialize<S: ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { serializer.serialize_str(self.as_str()) }}
impl<'de> Deserialize<'de> for SigningAlgorithm { fn deserialize<D: de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { let s = String::deserialize(deserializer)?; Self::from_str(&s).map_err(de::Error::custom) }}
#[derive(Debug, Clone, PartialEq, Eq)]pub enum TokenScope { Access, Refresh, AppPass, AppPassPrivileged, Custom(String),}
impl TokenScope { pub fn as_str(&self) -> &str { match self { Self::Access => "com.atproto.access", Self::Refresh => "com.atproto.refresh", Self::AppPass => "com.atproto.appPass", Self::AppPassPrivileged => "com.atproto.appPassPrivileged", Self::Custom(s) => s, } }
pub fn is_access_like(&self) -> bool { matches!(self, Self::Access | Self::AppPass | Self::AppPassPrivileged) }}
impl fmt::Display for TokenScope { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) }}
impl FromStr for TokenScope { type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> { Ok(match s { "com.atproto.access" => Self::Access, "com.atproto.refresh" => Self::Refresh, "com.atproto.appPass" => Self::AppPass, "com.atproto.appPassPrivileged" => Self::AppPassPrivileged, other => Self::Custom(other.to_string()), }) }}
impl Serialize for TokenScope { fn serialize<S: ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { serializer.serialize_str(self.as_str()) }}
impl<'de> Deserialize<'de> for TokenScope { fn deserialize<D: de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { let s = String::deserialize(deserializer)?; Ok(Self::from_str(&s).unwrap_or_else(|e| match e {})) }}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum TokenDecodeError { InvalidFormat, Base64DecodeFailed, JsonDecodeFailed, MissingClaim,}
impl fmt::Display for TokenDecodeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::InvalidFormat => write!(f, "Invalid token format"), Self::Base64DecodeFailed => write!(f, "Base64 decode failed"), Self::JsonDecodeFailed => write!(f, "JSON decode failed"), Self::MissingClaim => write!(f, "Missing required claim"), } }}
impl std::error::Error for TokenDecodeError {}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct ActClaim { pub sub: Did,}
#[derive(Debug, Serialize, Deserialize)]pub struct Claims { pub iss: Did, pub sub: Did, pub aud: String, pub exp: i64, pub iat: i64, #[serde(skip_serializing_if = "Option::is_none")] pub scope: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub lxm: Option<Nsid>, pub jti: Jti, #[serde(skip_serializing_if = "Option::is_none")] pub act: Option<ActClaim>,}
#[derive(Debug, Serialize, Deserialize)]pub struct Header { pub alg: SigningAlgorithm, pub typ: TokenType,}
#[derive(Debug, Serialize, Deserialize)]pub struct UnsafeClaims { pub iss: Did, pub sub: Option<Did>,}
pub struct TokenData<T> { pub claims: T,}
pub struct TokenWithMetadata { pub token: String, pub jti: Jti, pub expires_at: DateTime<Utc>,}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum TokenVerifyError { Expired, Invalid(&'static str),}
impl fmt::Display for TokenVerifyError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Expired => write!(f, "Token expired"), Self::Invalid(reason) => write!(f, "{}", reason), } }}
impl std::error::Error for TokenVerifyError {}
#[cfg(test)]mod tests { use super::*;
#[test] fn token_type_accepts_bluesky_uppercase_jwt() { let result: Result<Header, _> = serde_json::from_str(r#"{"alg":"ES256K","typ":"JWT"}"#); let header = result.expect("should parse uppercase JWT from bluesky reference pds"); assert_eq!(header.typ, TokenType::Service); assert_eq!(header.alg, SigningAlgorithm::ES256K); }
#[test] fn token_type_accepts_lowercase_jwt() { let result: Result<Header, _> = serde_json::from_str(r#"{"alg":"ES256K","typ":"jwt"}"#); let header = result.expect("should parse lowercase jwt"); assert_eq!(header.typ, TokenType::Service); }
#[test] fn token_type_accepts_mixed_case_access() { assert_eq!(TokenType::from_str("AT+JWT").unwrap(), TokenType::Access); assert_eq!(TokenType::from_str("at+jwt").unwrap(), TokenType::Access); assert_eq!(TokenType::from_str("At+Jwt").unwrap(), TokenType::Access); }
#[test] fn token_type_rejects_unknown() { assert!(TokenType::from_str("bearer").is_err()); }
#[test] fn service_token_header_serializes_typ_as_uppercase_jwt() { // RFC 7519 §5.1 recommends the JWT `typ` header value be uppercase "JWT". let header = Header { alg: SigningAlgorithm::ES256K, typ: TokenType::Service, }; let json = serde_json::to_string(&header).expect("serialize header"); assert!(json.contains(r#""typ":"JWT""#), "got {json}"); }
#[test] fn signing_algorithm_case_insensitive() { assert_eq!( SigningAlgorithm::from_str("ES256K").unwrap(), SigningAlgorithm::ES256K ); assert_eq!( SigningAlgorithm::from_str("es256k").unwrap(), SigningAlgorithm::ES256K ); assert_eq!( SigningAlgorithm::from_str("hs256").unwrap(), SigningAlgorithm::HS256 ); }}