A library for ATProtocol identities.
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}