this repo has no description
1use super::{Claims, Header}; 2use anyhow::Result; 3use base64::Engine as _; 4use base64::engine::general_purpose::URL_SAFE_NO_PAD; 5use chrono::{DateTime, Duration, Utc}; 6use hmac::{Hmac, Mac}; 7use k256::ecdsa::{Signature, SigningKey, signature::Signer}; 8use sha2::Sha256; 9use uuid; 10 11type HmacSha256 = Hmac<Sha256>; 12 13pub const TOKEN_TYPE_ACCESS: &str = "at+jwt"; 14pub const TOKEN_TYPE_REFRESH: &str = "refresh+jwt"; 15pub const TOKEN_TYPE_SERVICE: &str = "jwt"; 16pub const SCOPE_ACCESS: &str = "com.atproto.access"; 17pub const SCOPE_REFRESH: &str = "com.atproto.refresh"; 18pub const SCOPE_APP_PASS: &str = "com.atproto.appPass"; 19pub const SCOPE_APP_PASS_PRIVILEGED: &str = "com.atproto.appPassPrivileged"; 20 21pub struct TokenWithMetadata { 22 pub token: String, 23 pub jti: String, 24 pub expires_at: DateTime<Utc>, 25} 26 27pub fn create_access_token(did: &str, key_bytes: &[u8]) -> Result<String> { 28 Ok(create_access_token_with_metadata(did, key_bytes)?.token) 29} 30 31pub fn create_refresh_token(did: &str, key_bytes: &[u8]) -> Result<String> { 32 Ok(create_refresh_token_with_metadata(did, key_bytes)?.token) 33} 34 35pub fn create_access_token_with_metadata(did: &str, key_bytes: &[u8]) -> Result<TokenWithMetadata> { 36 create_access_token_with_scope_metadata(did, key_bytes, None) 37} 38 39pub fn create_access_token_with_scope_metadata( 40 did: &str, 41 key_bytes: &[u8], 42 scopes: Option<&str>, 43) -> Result<TokenWithMetadata> { 44 let scope = scopes.unwrap_or(SCOPE_ACCESS); 45 create_signed_token_with_metadata( 46 did, 47 scope, 48 TOKEN_TYPE_ACCESS, 49 key_bytes, 50 Duration::minutes(15), 51 ) 52} 53 54pub fn create_refresh_token_with_metadata( 55 did: &str, 56 key_bytes: &[u8], 57) -> Result<TokenWithMetadata> { 58 create_signed_token_with_metadata( 59 did, 60 SCOPE_REFRESH, 61 TOKEN_TYPE_REFRESH, 62 key_bytes, 63 Duration::days(14), 64 ) 65} 66 67pub fn create_service_token(did: &str, aud: &str, lxm: &str, key_bytes: &[u8]) -> Result<String> { 68 let signing_key = SigningKey::from_slice(key_bytes)?; 69 70 let expiration = Utc::now() 71 .checked_add_signed(Duration::seconds(60)) 72 .expect("valid timestamp") 73 .timestamp(); 74 75 let claims = Claims { 76 iss: did.to_owned(), 77 sub: did.to_owned(), 78 aud: aud.to_owned(), 79 exp: expiration as usize, 80 iat: Utc::now().timestamp() as usize, 81 scope: None, 82 lxm: Some(lxm.to_string()), 83 jti: uuid::Uuid::new_v4().to_string(), 84 }; 85 86 sign_claims(claims, &signing_key) 87} 88 89fn create_signed_token_with_metadata( 90 did: &str, 91 scope: &str, 92 typ: &str, 93 key_bytes: &[u8], 94 duration: Duration, 95) -> Result<TokenWithMetadata> { 96 let signing_key = SigningKey::from_slice(key_bytes)?; 97 98 let expires_at = Utc::now() 99 .checked_add_signed(duration) 100 .expect("valid timestamp"); 101 102 let expiration = expires_at.timestamp(); 103 let jti = uuid::Uuid::new_v4().to_string(); 104 105 let claims = Claims { 106 iss: did.to_owned(), 107 sub: did.to_owned(), 108 aud: format!( 109 "did:web:{}", 110 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()) 111 ), 112 exp: expiration as usize, 113 iat: Utc::now().timestamp() as usize, 114 scope: Some(scope.to_string()), 115 lxm: None, 116 jti: jti.clone(), 117 }; 118 119 let token = sign_claims_with_type(claims, &signing_key, typ)?; 120 121 Ok(TokenWithMetadata { 122 token, 123 jti, 124 expires_at, 125 }) 126} 127 128fn sign_claims(claims: Claims, key: &SigningKey) -> Result<String> { 129 sign_claims_with_type(claims, key, TOKEN_TYPE_SERVICE) 130} 131 132fn sign_claims_with_type(claims: Claims, key: &SigningKey, typ: &str) -> Result<String> { 133 let header = Header { 134 alg: "ES256K".to_string(), 135 typ: typ.to_string(), 136 }; 137 138 let header_json = serde_json::to_string(&header)?; 139 let claims_json = serde_json::to_string(&claims)?; 140 141 let header_b64 = URL_SAFE_NO_PAD.encode(header_json); 142 let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json); 143 144 let message = format!("{}.{}", header_b64, claims_b64); 145 let signature: Signature = key.sign(message.as_bytes()); 146 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes()); 147 148 Ok(format!("{}.{}", message, signature_b64)) 149} 150 151pub fn create_access_token_hs256(did: &str, secret: &[u8]) -> Result<String> { 152 Ok(create_access_token_hs256_with_metadata(did, secret)?.token) 153} 154 155pub fn create_refresh_token_hs256(did: &str, secret: &[u8]) -> Result<String> { 156 Ok(create_refresh_token_hs256_with_metadata(did, secret)?.token) 157} 158 159pub fn create_access_token_hs256_with_metadata( 160 did: &str, 161 secret: &[u8], 162) -> Result<TokenWithMetadata> { 163 create_hs256_token_with_metadata( 164 did, 165 SCOPE_ACCESS, 166 TOKEN_TYPE_ACCESS, 167 secret, 168 Duration::minutes(15), 169 ) 170} 171 172pub fn create_refresh_token_hs256_with_metadata( 173 did: &str, 174 secret: &[u8], 175) -> Result<TokenWithMetadata> { 176 create_hs256_token_with_metadata( 177 did, 178 SCOPE_REFRESH, 179 TOKEN_TYPE_REFRESH, 180 secret, 181 Duration::days(14), 182 ) 183} 184 185pub fn create_service_token_hs256( 186 did: &str, 187 aud: &str, 188 lxm: &str, 189 secret: &[u8], 190) -> Result<String> { 191 let expiration = Utc::now() 192 .checked_add_signed(Duration::seconds(60)) 193 .expect("valid timestamp") 194 .timestamp(); 195 196 let claims = Claims { 197 iss: did.to_owned(), 198 sub: did.to_owned(), 199 aud: aud.to_owned(), 200 exp: expiration as usize, 201 iat: Utc::now().timestamp() as usize, 202 scope: None, 203 lxm: Some(lxm.to_string()), 204 jti: uuid::Uuid::new_v4().to_string(), 205 }; 206 207 sign_claims_hs256(claims, TOKEN_TYPE_SERVICE, secret) 208} 209 210fn create_hs256_token_with_metadata( 211 did: &str, 212 scope: &str, 213 typ: &str, 214 secret: &[u8], 215 duration: Duration, 216) -> Result<TokenWithMetadata> { 217 let expires_at = Utc::now() 218 .checked_add_signed(duration) 219 .expect("valid timestamp"); 220 221 let expiration = expires_at.timestamp(); 222 let jti = uuid::Uuid::new_v4().to_string(); 223 224 let claims = Claims { 225 iss: did.to_owned(), 226 sub: did.to_owned(), 227 aud: format!( 228 "did:web:{}", 229 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()) 230 ), 231 exp: expiration as usize, 232 iat: Utc::now().timestamp() as usize, 233 scope: Some(scope.to_string()), 234 lxm: None, 235 jti: jti.clone(), 236 }; 237 238 let token = sign_claims_hs256(claims, typ, secret)?; 239 240 Ok(TokenWithMetadata { 241 token, 242 jti, 243 expires_at, 244 }) 245} 246 247fn sign_claims_hs256(claims: Claims, typ: &str, secret: &[u8]) -> Result<String> { 248 let header = Header { 249 alg: "HS256".to_string(), 250 typ: typ.to_string(), 251 }; 252 253 let header_json = serde_json::to_string(&header)?; 254 let claims_json = serde_json::to_string(&claims)?; 255 256 let header_b64 = URL_SAFE_NO_PAD.encode(header_json); 257 let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json); 258 259 let message = format!("{}.{}", header_b64, claims_b64); 260 261 let mut mac = HmacSha256::new_from_slice(secret) 262 .map_err(|e| anyhow::anyhow!("Invalid secret length: {}", e))?; 263 mac.update(message.as_bytes()); 264 265 let signature = mac.finalize().into_bytes(); 266 let signature_b64 = URL_SAFE_NO_PAD.encode(signature); 267 268 Ok(format!("{}.{}", message, signature_b64)) 269}