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<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
);
}
}