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