use 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 { 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(&self, serializer: S) -> Result { serializer.serialize_str(self.as_str()) } } impl<'de> Deserialize<'de> for TokenType { fn deserialize>(deserializer: D) -> Result { 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 { 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(&self, serializer: S) -> Result { serializer.serialize_str(self.as_str()) } } impl<'de> Deserialize<'de> for SigningAlgorithm { fn deserialize>(deserializer: D) -> Result { 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 { 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(&self, serializer: S) -> Result { serializer.serialize_str(self.as_str()) } } impl<'de> Deserialize<'de> for TokenScope { fn deserialize>(deserializer: D) -> Result { 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, #[serde(skip_serializing_if = "Option::is_none")] pub lxm: Option, pub jti: Jti, #[serde(skip_serializing_if = "Option::is_none")] pub act: Option, } #[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, } pub struct TokenData { pub claims: T, } pub struct TokenWithMetadata { pub token: String, pub jti: Jti, pub expires_at: DateTime, } #[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 = 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 = 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 ); } }