A library for ATProtocol identities.
at main 10 kB view raw
1//! JWT authorization extractors for XRPC services. 2//! 3//! Axum extractors for JWT validation against DID documents resolved 4//! via an identity resolver. 5 6use anyhow::Result; 7use atproto_identity::key::identify_key; 8use atproto_identity::traits::IdentityResolver; 9use atproto_oauth::jwt::{Claims, Header}; 10use axum::extract::{FromRef, OptionalFromRequestParts}; 11use axum::http::request::Parts; 12use base64::Engine as _; 13use base64::engine::general_purpose; 14use std::convert::Infallible; 15use std::sync::Arc; 16 17use crate::errors::AuthorizationError; 18 19/// JWT authorization extractor that validates tokens against DID documents. 20/// 21/// Contains JWT header, validated claims, original token, and validation status. 22/// Resolves DID documents via the configured identity resolver. 23#[derive(Clone)] 24pub struct Authorization(pub Header, pub Claims, pub String, pub bool); 25 26impl Authorization { 27 /// identity returns the optional issuer claim of the authorization structure. 28 pub fn identity(&self) -> Option<&str> { 29 if self.3 { 30 return self.1.jose.issuer.as_deref(); 31 } 32 None 33 } 34} 35 36impl<S> OptionalFromRequestParts<S> for Authorization 37where 38 S: Send + Sync, 39 Arc<dyn IdentityResolver>: FromRef<S>, 40{ 41 type Rejection = Infallible; 42 43 async fn from_request_parts( 44 parts: &mut Parts, 45 state: &S, 46 ) -> Result<Option<Self>, Self::Rejection> { 47 let auth_header = parts 48 .headers 49 .get("authorization") 50 .and_then(|value| value.to_str().ok()) 51 .and_then(|s| s.strip_prefix("Bearer ")); 52 53 let token = match auth_header { 54 Some(token) => token.to_string(), 55 None => { 56 return Ok(None); 57 } 58 }; 59 60 let identity_resolver = Arc::<dyn IdentityResolver>::from_ref(state); 61 62 match validate_jwt(&token, identity_resolver).await { 63 Ok((header, claims)) => Ok(Some(Authorization(header, claims, token, true))), 64 Err(_) => { 65 // Return unvalidated authorization so the handler can decide what to do 66 let header = Header::default(); 67 let claims = Claims::default(); 68 Ok(Some(Authorization(header, claims, token, false))) 69 } 70 } 71 } 72} 73 74async fn validate_jwt( 75 token: &str, 76 identity_resolver: Arc<dyn IdentityResolver>, 77) -> Result<(Header, Claims)> { 78 // Split and decode JWT 79 let parts: Vec<&str> = token.split('.').collect(); 80 if parts.len() != 3 { 81 return Err(AuthorizationError::InvalidJWTFormat.into()); 82 } 83 84 // Decode claims to get issuer 85 let encoded_claims = parts[1]; 86 let claims_bytes = general_purpose::URL_SAFE_NO_PAD 87 .decode(encoded_claims) 88 .map_err(|e| AuthorizationError::ClaimsDecodeError { error: e })?; 89 90 let claims: Claims = serde_json::from_slice(&claims_bytes) 91 .map_err(|e| AuthorizationError::ClaimsParseError { error: e })?; 92 93 // Get issuer from claims 94 let issuer = claims 95 .jose 96 .issuer 97 .as_ref() 98 .ok_or_else(|| AuthorizationError::NoIssuerInClaims)?; 99 100 // Resolve the DID document via identity resolver 101 let did_document = identity_resolver.resolve(issuer).await.map_err(|err| { 102 AuthorizationError::SubjectResolutionFailed { 103 issuer: issuer.to_string(), 104 error: err, 105 } 106 })?; 107 108 // Extract keys from DID document 109 let did_keys = did_document.did_keys(); 110 if did_keys.is_empty() { 111 return Err(AuthorizationError::NoVerificationKeys.into()); 112 } 113 114 // Try to validate with each key 115 for key_multibase in did_keys { 116 match identify_key(key_multibase) { 117 Ok(key_data) => { 118 match atproto_oauth::jwt::verify(token, &key_data) { 119 Ok(validated_claims) => { 120 // Decode header for return 121 let encoded_header = parts[0]; 122 let header_bytes = general_purpose::URL_SAFE_NO_PAD 123 .decode(encoded_header) 124 .map_err(|e| AuthorizationError::HeaderDecodeError { error: e })?; 125 let header: Header = serde_json::from_slice(&header_bytes) 126 .map_err(|e| AuthorizationError::HeaderParseError { error: e })?; 127 return Ok((header, validated_claims)); 128 } 129 Err(_e) => { 130 continue; 131 } 132 } 133 } 134 Err(_e) => { 135 continue; 136 } 137 } 138 } 139 140 Err(AuthorizationError::ValidationFailedAllKeys.into()) 141} 142 143// Example JWT: 144// eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE3NDg5ODg0OTcsImlzcyI6ImRpZDpwbGM6Y2Jrank1bjdiazNheDJ3cGxtdGpvZnEyIiwiYXVkIjoiZGlkOndlYjpuZ2VyYWtpbmVzLnR1bm4uZGV2IiwiZXhwIjoxNzQ4OTg4NTU3LCJseG0iOiJnYXJkZW4ubGV4aWNvbi5uZ2VyYWtpbmVzLmhlbGxvd29ybGQuSGVsbG8iLCJqdGkiOiI0ODQ2YjQ1OWMyMDFiMDNjZjBlZGMzYmE3NjQxNTk0MiJ9.sj74PPS97z81LSay6EyDOu3IQcF-bd4xGqK5u6qruhhWWiQR2IW89YMJ1s0H-P25xaTM1Zacp-pa4RlVsrH2uA 145 146#[cfg(test)] 147mod tests { 148 use super::*; 149 use atproto_identity::model::{Document, VerificationMethod}; 150 use axum::extract::FromRef; 151 use axum::http::{Method, Request}; 152 use std::collections::HashMap; 153 154 #[derive(Clone)] 155 struct MockResolver { 156 document: Document, 157 } 158 159 #[async_trait::async_trait] 160 impl IdentityResolver for MockResolver { 161 async fn resolve(&self, subject: &str) -> Result<Document> { 162 if subject == self.document.id { 163 Ok(self.document.clone()) 164 } else { 165 Err(anyhow::anyhow!( 166 "error-atproto-xrpcs-authorization-1 DID not found: {}", 167 subject 168 )) 169 } 170 } 171 } 172 173 #[derive(Clone)] 174 struct TestState { 175 resolver: Arc<dyn IdentityResolver>, 176 } 177 178 impl FromRef<TestState> for Arc<dyn IdentityResolver> { 179 fn from_ref(state: &TestState) -> Self { 180 state.resolver.clone() 181 } 182 } 183 184 #[tokio::test] 185 async fn test_authorization_optional_from_request_parts() { 186 // Create DID document with the specified DID and verification method 187 let did = "did:plc:cbkjy5n7bk3ax2wplmtjofq2"; 188 let verification_method_id = "did:key:zQ3shXvCK2RyPrSLYQjBEw5CExZkUhJH3n1K2Mb9sC7JbvRMF"; 189 190 let document = Document { 191 context: vec![], 192 id: did.to_string(), 193 also_known_as: vec![], 194 service: vec![], 195 verification_method: vec![VerificationMethod::Multikey { 196 id: format!("{}#atproto", did), 197 controller: did.to_string(), 198 public_key_multibase: verification_method_id.to_string(), 199 extra: HashMap::new(), 200 }], 201 extra: HashMap::new(), 202 }; 203 204 // Create mock resolver 205 let resolver = Arc::new(MockResolver { document }) as Arc<dyn IdentityResolver>; 206 let state = TestState { resolver }; 207 208 // Create request with Authorization header 209 let request = Request::builder() 210 .method(Method::GET) 211 .uri("/") 212 .header("authorization", "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE3NDg5ODg0OTcsImlzcyI6ImRpZDpwbGM6Y2Jrank1bjdiazNheDJ3cGxtdGpvZnEyIiwiYXVkIjoiZGlkOndlYjpuZ2VyYWtpbmVzLnR1bm4uZGV2IiwiZXhwIjoxNzQ4OTg4NTU3LCJseG0iOiJnYXJkZW4ubGV4aWNvbi5uZ2VyYWtpbmVzLmhlbGxvd29ybGQuSGVsbG8iLCJqdGkiOiI0ODQ2YjQ1OWMyMDFiMDNjZjBlZGMzYmE3NjQxNTk0MiJ9.sj74PPS97z81LSay6EyDOu3IQcF-bd4xGqK5u6qruhhWWiQR2IW89YMJ1s0H-P25xaTM1Zacp-pa4RlVsrH2uA") 213 .body(()) 214 .unwrap(); 215 216 let (mut parts, _body) = request.into_parts(); 217 218 // Test the OptionalFromRequestParts implementation 219 let result = Authorization::from_request_parts(&mut parts, &state).await; 220 221 // Verify the result 222 assert!(result.is_ok()); 223 let auth_option = result.unwrap(); 224 assert!(auth_option.is_some()); 225 226 let authorization = auth_option.unwrap(); 227 assert_eq!( 228 authorization.2, 229 "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE3NDg5ODg0OTcsImlzcyI6ImRpZDpwbGM6Y2Jrank1bjdiazNheDJ3cGxtdGpvZnEyIiwiYXVkIjoiZGlkOndlYjpuZ2VyYWtpbmVzLnR1bm4uZGV2IiwiZXhwIjoxNzQ4OTg4NTU3LCJseG0iOiJnYXJkZW4ubGV4aWNvbi5uZ2VyYWtpbmVzLmhlbGxvd29ybGQuSGVsbG8iLCJqdGkiOiI0ODQ2YjQ1OWMyMDFiMDNjZjBlZGMzYmE3NjQxNTk0MiJ9.sj74PPS97z81LSay6EyDOu3IQcF-bd4xGqK5u6qruhhWWiQR2IW89YMJ1s0H-P25xaTM1Zacp-pa4RlVsrH2uA" 230 ); // token 231 232 // The JWT validation may fail (e.g., due to expiration), but we should still get an Authorization object 233 // This tests that the OptionalFromRequestParts implementation works correctly 234 // The validation status (authorization.3) is a boolean - no need to assert 235 236 // If validation succeeded, verify the claims contain the expected issuer 237 if authorization.3 { 238 assert_eq!(authorization.1.jose.issuer.as_ref().unwrap(), did); 239 } 240 } 241 242 #[tokio::test] 243 async fn test_authorization_no_header() { 244 // Create mock resolver 245 let resolver = Arc::new(MockResolver { 246 document: Document { 247 context: vec![], 248 id: "did:plc:test".to_string(), 249 also_known_as: vec![], 250 service: vec![], 251 verification_method: vec![], 252 extra: HashMap::new(), 253 }, 254 }) as Arc<dyn IdentityResolver>; 255 let state = TestState { resolver }; 256 257 // Create request without Authorization header 258 let request = Request::builder() 259 .method(Method::GET) 260 .uri("/") 261 .body(()) 262 .unwrap(); 263 264 let (mut parts, _body) = request.into_parts(); 265 266 // Test the OptionalFromRequestParts implementation 267 let result = Authorization::from_request_parts(&mut parts, &state).await; 268 269 // Verify no authorization is returned when no header is present 270 assert!(result.is_ok()); 271 let auth_option = result.unwrap(); 272 assert!(auth_option.is_none()); 273 } 274}