A library for ATProtocol identities.

refactor: Streamline documentation and add client authentication binaries

* Simplify README files across all crates to be more concise and focused
* Add comprehensive client authentication CLI tools:
* atproto-client-app-password for app password management
* atproto-client-auth for OAuth flow testing
* Extend atproto-client with server operations and improved HTTP client functionality
* Update Dockerfile to include new atproto-jetstream-consumer binary
* Consolidate project documentation to focus on practical usage over detailed implementation
* Update CLAUDE.prompts.md with iterative development approach

Changed files
+1192 -2558
crates
atproto-client
atproto-identity
atproto-jetstream
atproto-oauth
atproto-oauth-axum
atproto-record
atproto-xrpcs
atproto-xrpcs-helloworld
+6 -2
CLAUDE.prompts.md
··· 40 40 41 41 Using `cargo clippy`, satisfy warnings. Think very hard about how to do this. 42 42 43 - Using `cargo build`, `cargo fmt`, `cargo check`, `cargo clippy`, and `cargo test` identity and resolve and warnings or errors. Think very hard. 43 + Using `cargo build`, `cargo fmt`, `cargo check`, `cargo clippy`, and `cargo test` identity and resolve and warnings or errors. Work iteratively and think very hard. 44 44 45 45 ## Cleanup and Check 46 46 ··· 56 56 57 57 5. Update the `README.md` file in the root of the project that describes the project as collection of components used to create ATProtocol applications. Note that parts of this was extracted from the open sourced https://tangled.sh/@smokesignal.events/smokesignal project. This project is open source under the MIT license. 58 58 59 - 6. Ensure all crates can be packaged and published. 59 + 6. Ensure the Dockerfile is accurate and correct. 60 60 61 61 Avoid introducing new dependencies. Think very very hard. 62 + 63 + ## Release 64 + 65 + Ensure all crates can be packaged and published.
+2 -1
Dockerfile
··· 41 41 COPY --from=builder /usr/src/app/target/release/atproto-oauth-tool . 42 42 COPY --from=builder /usr/src/app/target/release/atproto-client-dpop . 43 43 COPY --from=builder /usr/src/app/target/release/atproto-xrpcs-helloworld . 44 + COPY --from=builder /usr/src/app/target/release/atproto-jetstream-consumer . 44 45 45 46 # Default to the main resolution tool 46 47 # Users can override with specific binary: docker run <image> atproto-identity-resolve --help ··· 60 61 LABEL org.opencontainers.image.licenses="MIT" 61 62 62 63 # Document available binaries 63 - LABEL binaries="atproto-identity-resolve,atproto-identity-sign,atproto-identity-validate,atproto-identity-key,atproto-record-sign,atproto-record-verify,atproto-oauth-tool,atproto-client-dpop,atproto-xrpcs-helloworld" 64 + LABEL binaries="atproto-identity-resolve,atproto-identity-sign,atproto-identity-validate,atproto-identity-key,atproto-record-sign,atproto-record-verify,atproto-oauth-tool,atproto-client-dpop,atproto-xrpcs-helloworld,atproto-jetstream-consumer"
+46 -339
README.md
··· 1 - # AT Protocol Identity & Record Library 1 + # atproto-identity-rs 2 2 3 - A comprehensive collection of Rust crates for building AT Protocol applications. This workspace provides complete functionality for identity management, record operations, OAuth 2.0 flows, event streaming, XRPC services, and HTTP client operations across multiple DID methods. 3 + A comprehensive collection of Rust crates for building AT Protocol applications. This workspace provides identity management, record operations, OAuth 2.0 flows, HTTP client operations, XRPC services, and event streaming capabilities. 4 4 5 - This project was extracted from the open-sourced [smokesignal.events](https://tangled.sh/@smokesignal.events/smokesignal) project and is released under the MIT license. 5 + Parts of this project were extracted from the open-source [smokesignal.events](https://tangled.sh/@smokesignal.events/smokesignal) project and are licensed under the MIT license. 6 6 7 - ## Project Overview 7 + ## Components 8 8 9 - This workspace provides a complete toolkit for AT Protocol operations including identity resolution, record signing/verification, OAuth 2.0 flows, HTTP client operations, and web server integration. The library is designed to be modular, secure, and fully compliant with AT Protocol specifications. 9 + ### Core Libraries 10 10 11 - ## Crates 11 + - **[`atproto-identity`](crates/atproto-identity/)** - Identity management with DID resolution and cryptographic operations. Includes 4 CLI tools for identity resolution, key management, signing, and validation. 12 + - **[`atproto-record`](crates/atproto-record/)** - AT Protocol record signature operations. Includes 2 CLI tools for signing and verifying records. 13 + - **[`atproto-oauth`](crates/atproto-oauth/)** - OAuth 2.0 implementation with AT Protocol security extensions including PKCE, DPoP, and JWT operations. 14 + - **[`atproto-client`](crates/atproto-client/)** - HTTP client with DPoP authentication and repository operations. Includes 3 CLI tools for client authentication testing. 12 15 13 - This workspace contains eight specialized crates: 16 + ### Web Framework Integration 14 17 15 - ### [`atproto-identity`](crates/atproto-identity/) 16 - **Core identity management and cryptographic operations** 18 + - **[`atproto-oauth-axum`](crates/atproto-oauth-axum/)** - Axum web handlers for OAuth endpoints. Includes 1 CLI tool for OAuth client testing. 19 + - **[`atproto-xrpcs`](crates/atproto-xrpcs/)** - XRPC service components with JWT authorization extractors. 20 + - **[`atproto-xrpcs-helloworld`](crates/atproto-xrpcs-helloworld/)** - Example XRPC service implementation with DID web support. 17 21 18 - - **DID Resolution**: Support for `did:plc`, `did:web`, and `did:key` methods 19 - - **Handle Resolution**: DNS and HTTP-based handle resolution with validation 20 - - **Identity Documents**: Complete DID document parsing and management 21 - - **Cryptographic Operations**: P-256 and K-256 elliptic curve support with signing/verification 22 - - **Key Management**: DID key identification, conversion, and JWK generation 23 - - **Validation**: Input validation for handles and DIDs 24 - - **Configuration**: Environment-based configuration with DNS and certificate customization 22 + ### Event Streaming 25 23 26 - ### [`atproto-record`](crates/atproto-record/) 27 - **AT Protocol record signature operations** 28 - 29 - - **Record Signing**: Create cryptographic signatures for AT Protocol records with proper `$sig` object handling 30 - - **Signature Verification**: Verify existing signatures against records and public keys 31 - - **IPLD Integration**: Proper IPLD DAG-CBOR serialization for consistent signature generation 32 - - **Multi-curve Support**: Support for P-256 and K-256 elliptic curves via `atproto-identity` 33 - - **Repository Context**: Include repository and collection context in signatures 34 - 35 - ### [`atproto-oauth`](crates/atproto-oauth/) 36 - **Complete OAuth 2.0 operations with AT Protocol extensions** 37 - 38 - - **JWT Operations**: Complete JSON Web Token minting, verification, and validation with ES256/ES256K support 39 - - **JWK Management**: JSON Web Key generation and management for P-256 and K-256 curves 40 - - **PKCE Implementation**: RFC 7636 Proof Key for Code Exchange for secure OAuth flows 41 - - **DPoP Support**: RFC 9449 Demonstration of Proof-of-Possession with automatic retry middleware 42 - - **OAuth Resource Discovery**: RFC 8414 OAuth 2.0 authorization server and protected resource metadata discovery 43 - - **AT Protocol Validation**: Comprehensive validation of OAuth servers against AT Protocol requirements 44 - 45 - ### [`atproto-client`](crates/atproto-client/) 46 - **HTTP client library for AT Protocol services** 47 - 48 - - **HTTP Client Operations**: Authenticated and unauthenticated HTTP GET/POST requests with JSON support 49 - - **DPoP Authentication**: RFC 9449 Demonstration of Proof-of-Possession with automatic retry middleware 50 - - **Repository Operations**: Complete CRUD operations for AT Protocol repository records 51 - - **URL Building**: Flexible URL construction with parameter encoding and query string generation 52 - - **OAuth Integration**: Seamless integration with `atproto-oauth` for DPoP authentication 53 - 54 - ### [`atproto-oauth-axum`](crates/atproto-oauth-axum/) 55 - **Axum web handlers for OAuth 2.0 authorization server endpoints** 56 - 57 - - **Complete OAuth Server Handlers**: Ready-to-use Axum handlers for all required OAuth 2.0 endpoints 58 - - **Client Metadata Endpoint**: RFC 7591 compliant client metadata for dynamic client registration 59 - - **JWKS Endpoint**: JSON Web Key Set serving for JWT signature verification 60 - - **Authorization Callback Handler**: Complete OAuth callback processing with token exchange 61 - - **OAuth Login CLI Tool**: Full-featured command-line tool for testing and development OAuth flows 62 - 63 - ### [`atproto-xrpcs`](crates/atproto-xrpcs/) 64 - **Core building blocks for implementing XRPC services** 65 - 66 - - **JWT Authorization Extractors**: Axum-compatible request extractors for JWT-based authorization 67 - - **DID Document Verification**: Automatic verification of caller identities through DID document resolution 68 - - **XRPC Service Components**: Core building blocks for implementing AT Protocol XRPC endpoints 69 - - **Structured Error Handling**: Comprehensive error types with detailed error codes and messages 70 - - **Axum Integration**: Native integration with Axum web framework for HTTP handlers 71 - 72 - ### [`atproto-xrpcs-helloworld`](crates/atproto-xrpcs-helloworld/) 73 - **Complete example XRPC service implementation** 74 - 75 - - **DID Web Support**: Native DID web identity with automatic service document generation 76 - - **Well-Known Endpoints**: Standard AT Protocol discovery endpoints for identity and service metadata 77 - - **Hello World XRPC**: Example authenticated and unauthenticated XRPC endpoint implementation 78 - - **JWT Authentication**: Demonstrates integration with `atproto-xrpcs` authorization extractors 79 - - **Service Discovery**: Complete service document with verification methods and service endpoints 80 - 81 - ### [`atproto-jetstream`](crates/atproto-jetstream/) 82 - **AT Protocol Jetstream event consumer library** 83 - 84 - - **Event Stream Consumer**: High-performance WebSocket-based event consumption from Jetstream instances 85 - - **Event Handler Registration**: Flexible event handler system supporting multiple concurrent handlers 86 - - **Compression Support**: Optional Zstandard compression with dictionary support for efficient data transfer 87 - - **Graceful Shutdown**: Cancellation token support for clean shutdown and resource cleanup 88 - - **Error Handling**: Comprehensive error types following project conventions with structured logging 89 - 90 - ## Command Line Tools 91 - 92 - The library includes 10 command-line utilities across the crates: 93 - 94 - ### Identity Operations (`atproto-identity`) 95 - - **`atproto-identity-resolve`** - Resolve AT Protocol handles and DIDs to identity documents 96 - - **`atproto-identity-sign`** - Sign JSON data with DID keys using IPLD DAG-CBOR serialization 97 - - **`atproto-identity-validate`** - Verify cryptographic signatures of JSON data 98 - - **`atproto-identity-key`** - Generate and manage cryptographic keys (P-256/K-256) 99 - 100 - ### Record Operations (`atproto-record`) 101 - - **`atproto-record-sign`** - Sign AT Protocol records with embedded signature metadata 102 - - **`atproto-record-verify`** - Verify AT Protocol record signatures with issuer authentication 103 - 104 - ### HTTP Client Operations (`atproto-client`) 105 - - **`atproto-client-dpop`** - Test DPoP authentication flows with AT Protocol services 106 - 107 - ### OAuth Operations (`atproto-oauth-axum`) 108 - - **`atproto-oauth-tool`** - Complete OAuth client flow with local server and token acquisition 109 - 110 - ### XRPC Services (`atproto-xrpcs-helloworld`) 111 - - **`atproto-xrpcs-helloworld`** - Example AT Protocol XRPC service with DID web identity and authentication 112 - 113 - ### Event Streaming (`atproto-jetstream`) 114 - - **`atproto-jetstream-consumer`** - Stream AT Protocol events from Jetstream instances with configurable handlers 24 + - **[`atproto-jetstream`](crates/atproto-jetstream/)** - WebSocket event stream consumer with compression support. Includes 1 CLI tool for event consumption. 115 25 116 26 ## Quick Start 117 27 ··· 120 30 ```toml 121 31 [dependencies] 122 32 atproto-identity = "0.6.0" 123 - atproto-record = "0.6.0" 33 + atproto-record = "0.6.0" 124 34 atproto-oauth = "0.6.0" 125 35 atproto-client = "0.6.0" 126 - atproto-oauth-axum = "0.6.0" 127 - atproto-xrpcs = "0.6.0" 128 - atproto-jetstream = "0.6.0" 36 + # Add others as needed 129 37 ``` 130 38 131 39 ### Basic Identity Resolution ··· 138 46 let http_client = reqwest::Client::new(); 139 47 let dns_resolver = create_resolver(&[]); 140 48 141 - // Resolve a handle to a DID 142 49 let did = resolve_subject(&http_client, &dns_resolver, "alice.bsky.social").await?; 143 50 println!("Resolved DID: {}", did); 144 51 ··· 146 53 } 147 54 ``` 148 55 149 - ### Record Signing and Verification 56 + ### Record Signing 150 57 151 58 ```rust 152 59 use atproto_identity::key::identify_key; ··· 155 62 156 63 #[tokio::main] 157 64 async fn main() -> anyhow::Result<()> { 158 - // Parse DID key for signing operations 159 65 let signing_key = identify_key("did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA")?; 160 66 161 - // Create a record to sign 162 67 let record = json!({ 163 68 "$type": "app.bsky.feed.post", 164 69 "text": "Hello AT Protocol!", 165 70 "createdAt": "2024-01-01T00:00:00.000Z" 166 71 }); 167 72 168 - // Create signature object with issuer and timestamp 169 73 let signature_object = json!({ 170 74 "issuer": "did:plc:issuer123", 171 75 "issuedAt": "2024-01-01T00:00:00.000Z" 172 76 }); 173 77 174 - // Sign the record 175 78 let signed_record = signature::create( 176 79 &signing_key, 177 80 &record, 178 - "did:plc:user123", // repository 179 - "app.bsky.feed.post", // collection 81 + "did:plc:user123", 82 + "app.bsky.feed.post", 180 83 signature_object, 181 84 ).await?; 182 85 183 - // Verify the signature 184 - signature::verify( 185 - "did:plc:issuer123", // issuer 186 - &signing_key, // verification key 187 - signed_record, // signed record 188 - "did:plc:user123", // repository 189 - "app.bsky.feed.post", // collection 190 - ).await?; 191 - 192 - println!("Signature verification successful"); 193 - 194 86 Ok(()) 195 87 } 196 88 ``` 197 89 198 - ### OAuth Operations 199 - 200 - ```rust 201 - use atproto_oauth::jwt::{mint, Header, Claims, JoseClaims}; 202 - use atproto_oauth::pkce; 203 - use atproto_identity::key::identify_key; 204 - 205 - #[tokio::main] 206 - async fn main() -> anyhow::Result<()> { 207 - let key_data = identify_key("did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA")?; 208 - 209 - // Generate PKCE parameters 210 - let (code_verifier, code_challenge) = pkce::generate(); 211 - println!("PKCE Challenge: {}", code_challenge); 212 - 213 - // Create and sign JWT 214 - let header = Header::default(); 215 - let claims = Claims::new(JoseClaims { 216 - issuer: Some("did:plc:issuer123".to_string()), 217 - subject: Some("did:plc:subject456".to_string()), 218 - audience: Some("https://pds.example.com".to_string()), 219 - ..Default::default() 220 - }); 221 - 222 - let token = mint(&key_data, &header, &claims)?; 223 - println!("JWT: {}", token); 224 - 225 - Ok(()) 226 - } 227 - ``` 228 - 229 - ### XRPC Service Implementation 90 + ### XRPC Service 230 91 231 92 ```rust 232 93 use atproto_xrpcs::authorization::ResolvingAuthorization; ··· 239 100 subject: Option<String>, 240 101 } 241 102 242 - // XRPC handler with optional JWT authorization 243 - async fn handle_hello_xrpc( 103 + async fn handle_hello( 244 104 params: Query<HelloParams>, 245 105 authorization: Option<ResolvingAuthorization>, 246 106 ) -> Json<serde_json::Value> { 247 107 let subject = params.subject.as_deref().unwrap_or("World"); 248 108 249 109 let message = if let Some(auth) = authorization { 250 - // Authenticated request - auth automatically validates JWT and resolves DID 251 110 format!("Hello, authenticated {}! (caller: {})", subject, auth.subject()) 252 111 } else { 253 - // Unauthenticated request 254 112 format!("Hello, {}!", subject) 255 113 }; 256 114 ··· 259 117 260 118 #[tokio::main] 261 119 async fn main() -> anyhow::Result<()> { 262 - // Set up Axum router with XRPC endpoint 263 120 let app = Router::new() 264 - .route("/xrpc/com.example.hello", get(handle_hello_xrpc)) 265 - .with_state(your_web_context); // Include identity resolver and storage 121 + .route("/xrpc/com.example.hello", get(handle_hello)) 122 + .with_state(your_web_context); 266 123 267 124 let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?; 268 125 axum::serve(listener, app).await?; ··· 271 128 } 272 129 ``` 273 130 274 - ### Jetstream Event Streaming 131 + ## Command Line Tools 275 132 276 - ```rust 277 - use atproto_jetstream::{Consumer, ConsumerTaskConfig, EventHandler, JetstreamEvent, CancellationToken}; 278 - use async_trait::async_trait; 279 - use std::sync::Arc; 280 - 281 - // Custom event handler 282 - struct PostEventHandler; 283 - 284 - #[async_trait] 285 - impl EventHandler for PostEventHandler { 286 - async fn handle_event(&self, event: JetstreamEvent) -> anyhow::Result<()> { 287 - if event.kind == "commit" { 288 - println!("Received post event: {:?}", event); 289 - } 290 - Ok(()) 291 - } 292 - 293 - fn handler_id(&self) -> String { 294 - "post-handler".to_string() 295 - } 296 - } 297 - 298 - #[tokio::main] 299 - async fn main() -> anyhow::Result<()> { 300 - let config = ConsumerTaskConfig { 301 - user_agent: "my-app/1.0".to_string(), 302 - compression: false, 303 - zstd_dictionary_location: String::new(), 304 - jetstream_hostname: "jetstream1.us-east.bsky.network".to_string(), 305 - collections: vec!["app.bsky.feed.post".to_string()], 306 - }; 307 - 308 - let consumer = Consumer::new(config); 309 - let handler = Arc::new(PostEventHandler); 310 - 311 - consumer.register_handler(handler).await?; 312 - 313 - let cancellation_token = CancellationToken::new(); 314 - consumer.run_background(cancellation_token).await?; 315 - 316 - Ok(()) 317 - } 318 - ``` 319 - 320 - ## CLI Usage Examples 133 + The workspace includes 12 command-line tools across the crates: 321 134 322 135 ```bash 323 - # Resolve a handle or DID to identity document 136 + # Identity operations 324 137 cargo run --bin atproto-identity-resolve alice.bsky.social 325 - 326 - # Get full DID document with verification methods 327 - cargo run --bin atproto-identity-resolve --did-document did:plc:user123 328 - 329 - # Generate a new P-256 private key 330 138 cargo run --bin atproto-identity-key generate p256 331 - 332 - # Sign a record from file with all required context 333 - cargo run --bin atproto-record-sign \ 334 - did:key:zQ3shNzMp4oaaQ1... \ 335 - did:plc:issuer123 \ 336 - record.json \ 337 - repository=did:plc:user123 \ 338 - collection=app.bsky.feed.post 339 - 340 - # Verify a signed record 341 - cargo run --bin atproto-record-verify \ 342 - did:plc:issuer123 \ 343 - did:key:zQ3shNzMp4oaaQ1... \ 344 - signed_record.json \ 345 - repository=did:plc:user123 \ 346 - collection=app.bsky.feed.post 347 - 348 - # Start OAuth login flow (requires EXTERNAL_BASE environment variable) 349 - EXTERNAL_BASE=localhost:8080 cargo run --bin atproto-oauth-tool login \ 350 - did:key:zQ3shNzMp4oaaQ1... \ 351 - alice.bsky.social 352 - 353 - # Run example XRPC service with DID web identity 354 - EXTERNAL_BASE=localhost:8080 SERVICE_KEY=did:key:zQ3shNzMp4oaaQ1... \ 355 - cargo run --bin atproto-xrpcs-helloworld 356 - 357 - # Stream events from Jetstream 358 - cargo run --bin atproto-jetstream-consumer \ 359 - --hostname jetstream1.us-east.bsky.network \ 360 - --collections app.bsky.feed.post \ 361 - --user-agent "my-consumer/1.0" 362 - 363 - # Stream with compression (requires dictionary file) 364 - cargo run --bin atproto-jetstream-consumer \ 365 - --hostname jetstream1.us-east.bsky.network \ 366 - --collections app.bsky.feed.post \ 367 - --compression \ 368 - --zstd-dictionary ./data/zstd_dictionary 369 - ``` 139 + cargo run --bin atproto-identity-sign did:key:... data.json 140 + cargo run --bin atproto-identity-validate did:key:... data.json signature 370 141 371 - ## Features 142 + # Record operations 143 + cargo run --bin atproto-record-sign did:key:... did:plc:issuer record.json repo=did:plc:user collection=app.bsky.feed.post 144 + cargo run --bin atproto-record-verify did:plc:issuer did:key:... signed_record.json repo=did:plc:user collection=app.bsky.feed.post 372 145 373 - - **Async/Await**: Built with modern Rust async patterns using Tokio 374 - - **Error Handling**: Comprehensive structured error types using `thiserror` with standardized error codes 375 - - **Security**: Forbids unsafe code, follows security best practices, and implements all required OAuth security extensions 376 - - **Standards Compliance**: Full AT Protocol, RFC 7636 (PKCE), RFC 9449 (DPoP), and RFC 8414 (OAuth Discovery) compliance 377 - - **Logging**: Structured logging with `tracing` for debugging and monitoring 378 - - **Multi-platform**: Works on all major platforms with configurable DNS and HTTP settings 379 - - **Modular Design**: Clean separation of concerns allowing selective usage of components 146 + # Client operations 147 + cargo run --bin atproto-client-auth --handle alice.bsky.social 148 + cargo run --bin atproto-client-app-password --identifier alice.bsky.social 149 + cargo run --bin atproto-client-dpop --private-key did:key:... --access-token token --url https://pds.example.com/xrpc/endpoint 380 150 381 - ## Architecture 151 + # OAuth operations 152 + EXTERNAL_BASE=localhost:8080 cargo run --bin atproto-oauth-tool login did:key:... alice.bsky.social 382 153 383 - The library follows a layered architecture with clear separation of concerns: 154 + # XRPC service 155 + EXTERNAL_BASE=localhost:8080 SERVICE_KEY=did:key:... cargo run --bin atproto-xrpcs-helloworld 384 156 385 - ``` 386 - ┌─────────────────────────────────────────────────────────────┐ 387 - │ Application Layer │ 388 - │ (CLI Tools & Web Handlers) │ 389 - ├─────────────────────────────────────────────────────────────┤ 390 - │ Protocol Layer │ 391 - │ (atproto-client, atproto-record) │ 392 - ├─────────────────────────────────────────────────────────────┤ 393 - │ OAuth Layer │ 394 - │ (atproto-oauth, atproto-oauth-axum) │ 395 - ├─────────────────────────────────────────────────────────────┤ 396 - │ Foundation Layer │ 397 - │ (atproto-identity - Core Services) │ 398 - └─────────────────────────────────────────────────────────────┘ 157 + # Event streaming 158 + cargo run --bin atproto-jetstream-consumer --hostname jetstream1.us-east.bsky.network --collections app.bsky.feed.post 399 159 ``` 400 160 401 - - **Foundation Layer**: Core identity, cryptographic, and DID operations 402 - - **OAuth Layer**: Complete OAuth 2.0 flows with AT Protocol extensions 403 - - **Protocol Layer**: Higher-level AT Protocol operations (records, client) 404 - - **Application Layer**: Ready-to-use tools and web framework integration 405 - 406 161 ## Development 407 - 408 - ### Building the Project 409 162 410 163 ```bash 411 164 # Build all crates 412 165 cargo build 413 166 414 - # Build specific crate 415 - cargo build -p atproto-identity 416 - 417 - # Build with all features 418 - cargo build --all-features 419 - ``` 420 - 421 - ### Running Tests 422 - 423 - ```bash 424 - # Run all tests 167 + # Run all tests 425 168 cargo test 426 169 427 - # Run tests for specific crate 428 - cargo test -p atproto-oauth 429 - 430 - # Run with output 431 - cargo test -- --nocapture 432 - ``` 433 - 434 - ### Code Quality 435 - 436 - ```bash 437 - # Format code 438 - cargo fmt 439 - 440 - # Lint code 441 - cargo clippy 442 - 443 - # Check without building 444 - cargo check 445 - 446 - # Run all quality checks 447 - cargo fmt && cargo clippy && cargo test 448 - ``` 449 - 450 - ### Documentation 170 + # Format and lint 171 + cargo fmt && cargo clippy 451 172 452 - ```bash 453 173 # Generate documentation 454 - cargo doc --open 455 - 456 - # Generate documentation for all crates 457 174 cargo doc --workspace --open 458 175 ``` 459 176 ··· 461 178 462 179 This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 463 180 464 - ## Contributing 465 - 466 - Contributions are welcome! Please ensure that: 467 - 468 - 1. All tests pass: `cargo test` 469 - 2. Code is properly formatted: `cargo fmt` 470 - 3. No linting issues: `cargo clippy` 471 - 4. New functionality includes appropriate tests and documentation 472 - 5. Error handling follows the project's structured error format 473 - 474 181 ## Acknowledgments 475 182 476 - This library was extracted from the [smokesignal.events](https://tangled.sh/@smokesignal.events/smokesignal) project, an open-source event and RSVP management and discovery application built on AT Protocol. We thank the smokesignal.events contributors for their foundational work on AT Protocol identity management, record operations, and event streaming infrastructure. 183 + This library was extracted from the [smokesignal.events](https://tangled.sh/@smokesignal.events/smokesignal) project, an open-source AT Protocol event and RSVP management application.
+30 -356
crates/atproto-client/README.md
··· 1 1 # atproto-client 2 2 3 - A Rust HTTP client library for AT Protocol services, providing authenticated and unauthenticated HTTP operations with DPoP (Demonstration of Proof-of-Possession) authentication support. 3 + HTTP client library for AT Protocol services with DPoP authentication support. 4 4 5 5 ## Overview 6 6 7 - `atproto-client` provides HTTP client functionality specifically designed for interacting with AT Protocol endpoints. This library handles both basic HTTP operations and advanced DPoP-authenticated requests required for secure AT Protocol communication, including full support for repository record operations. 7 + `atproto-client` provides HTTP client functionality specifically designed for interacting with AT Protocol endpoints. This library handles both basic HTTP operations and DPoP-authenticated requests required for secure AT Protocol communication. 8 8 9 - This project was extracted from the open-sourced [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project and is designed to be a standalone, reusable library for AT Protocol HTTP client operations. 9 + ## Binaries 10 10 11 - ## Features 12 - 13 - - **HTTP Client Operations**: Authenticated and unauthenticated HTTP GET/POST requests with JSON support 14 - - **DPoP Authentication**: RFC 9449 Demonstration of Proof-of-Possession with automatic retry middleware 15 - - **Repository Operations**: Complete CRUD operations for AT Protocol repository records 16 - - **URL Building**: Flexible URL construction with parameter encoding and query string generation 17 - - **Error Handling**: Structured error types with detailed error codes and messages 18 - - **OAuth Integration**: Seamless integration with `atproto-oauth` for DPoP authentication 19 - - **Automatic Retries**: Built-in DPoP nonce retry middleware for robust authentication 20 - - **Structured Logging**: Built-in tracing support for debugging and monitoring 21 - 22 - ## Installation 23 - 24 - Add this to your `Cargo.toml`: 25 - 26 - ```toml 27 - [dependencies] 28 - atproto-client = "0.6.0" 29 - ``` 11 + - **atproto-client-auth**: OAuth authentication flow helper 12 + - **atproto-client-app-password**: App password management tool 13 + - **atproto-client-dpop**: DPoP authentication testing tool 30 14 31 15 ## Usage 32 16 ··· 36 20 use atproto_client::client; 37 21 use reqwest::Client; 38 22 39 - #[tokio::main] 40 - async fn main() -> anyhow::Result<()> { 41 - let http_client = Client::new(); 42 - 43 - // Unauthenticated GET request 44 - let response = client::get_json(&http_client, "https://api.example.com/data").await?; 45 - println!("Response: {}", response); 46 - 47 - Ok(()) 48 - } 23 + let http_client = Client::new(); 24 + let response = client::get_json(&http_client, "https://api.example.com/data").await?; 49 25 ``` 50 26 51 27 ### DPoP Authentication 52 28 53 29 ```rust 54 - use atproto_client::client::{DPoPAuth, get_dpop_json, post_dpop_json}; 30 + use atproto_client::client::{DPoPAuth, get_dpop_json}; 55 31 use atproto_identity::key::identify_key; 56 - use reqwest::Client; 57 - use serde_json::json; 58 32 59 - #[tokio::main] 60 - async fn main() -> anyhow::Result<()> { 61 - let http_client = Client::new(); 62 - 63 - // Set up DPoP authentication 64 - let dpop_auth = DPoPAuth { 65 - dpop_private_key_data: identify_key("did:key:zQ3shNz...")?, 66 - oauth_access_token: "your_access_token".to_string(), 67 - oauth_issuer: "did:plc:issuer123".to_string(), 68 - }; 69 - 70 - // Authenticated GET request 71 - let response = get_dpop_json( 72 - &http_client, 73 - &dpop_auth, 74 - "https://pds.example.com/xrpc/com.atproto.repo.listRecords?repo=did:plc:user123&collection=app.bsky.feed.post" 75 - ).await?; 76 - 77 - // Authenticated POST request 78 - let record_data = json!({ 79 - "$type": "app.bsky.feed.post", 80 - "text": "Hello AT Protocol!", 81 - "createdAt": "2024-01-01T00:00:00Z" 82 - }); 83 - 84 - let post_response = post_dpop_json( 85 - &http_client, 86 - &dpop_auth, 87 - "https://pds.example.com/xrpc/com.atproto.repo.createRecord", 88 - record_data 89 - ).await?; 90 - 91 - println!("Created record: {}", post_response); 92 - 93 - Ok(()) 94 - } 95 - ``` 96 - 97 - ### Repository Operations 98 - 99 - ```rust 100 - use atproto_client::com::atproto::repo::{ 101 - get_record, list_records, create_record, put_record, 102 - CreateRecordRequest, PutRecordRequest, ListRecordsParams 33 + let dpop_auth = DPoPAuth { 34 + dpop_private_key_data: identify_key("did:key:zQ3sh...")?, 35 + oauth_access_token: "your_access_token".to_string(), 36 + oauth_issuer: "did:plc:issuer123".to_string(), 103 37 }; 104 - use atproto_client::client::DPoPAuth; 105 - use atproto_identity::key::identify_key; 106 - use reqwest::Client; 107 - use serde_json::json; 108 38 109 - #[tokio::main] 110 - async fn main() -> anyhow::Result<()> { 111 - let http_client = Client::new(); 112 - let pds_url = "https://pds.example.com"; 113 - 114 - let dpop_auth = DPoPAuth { 115 - dpop_private_key_data: identify_key("did:key:zQ3shNz...")?, 116 - oauth_access_token: "your_access_token".to_string(), 117 - oauth_issuer: "did:plc:issuer123".to_string(), 118 - }; 119 - 120 - // Get a specific record 121 - let record_response = get_record( 122 - &http_client, 123 - &dpop_auth, 124 - pds_url, 125 - "did:plc:user123", 126 - "app.bsky.feed.post", 127 - "3l2uygzaf5c2b", 128 - None // Optional CID for specific version 129 - ).await?; 130 - 131 - // List records in a collection with parameters 132 - let list_response = list_records::<serde_json::Value>( 133 - &http_client, 134 - &dpop_auth, 135 - pds_url, 136 - "did:plc:user123".to_string(), 137 - "app.bsky.feed.post".to_string(), 138 - ListRecordsParams::new() 139 - .limit(50) 140 - .reverse(false) 141 - ).await?; 142 - 143 - // Create a new record 144 - let create_request = CreateRecordRequest { 145 - repo: "did:plc:user123".to_string(), 146 - collection: "app.bsky.feed.post".to_string(), 147 - record_key: None, // Let server generate key 148 - validate: true, 149 - record: json!({ 150 - "$type": "app.bsky.feed.post", 151 - "text": "Hello from atproto-client!", 152 - "createdAt": "2024-01-01T00:00:00Z" 153 - }), 154 - swap_commit: None, 155 - }; 156 - 157 - let create_response = create_record( 158 - &http_client, 159 - &dpop_auth, 160 - pds_url, 161 - create_request 162 - ).await?; 163 - 164 - // Update a record with specific key 165 - let put_request = PutRecordRequest { 166 - repo: "did:plc:user123".to_string(), 167 - collection: "app.bsky.feed.post".to_string(), 168 - record_key: "3l2uygzaf5c2b".to_string(), 169 - validate: true, 170 - record: json!({ 171 - "$type": "app.bsky.feed.post", 172 - "text": "Updated post content", 173 - "createdAt": "2024-01-01T00:00:00Z" 174 - }), 175 - swap_commit: None, 176 - swap_record: None, 177 - }; 178 - 179 - let put_response = put_record( 180 - &http_client, 181 - &dpop_auth, 182 - pds_url, 183 - put_request 184 - ).await?; 185 - 186 - Ok(()) 187 - } 188 - ``` 189 - 190 - ### URL Building 191 - 192 - ```rust 193 - use atproto_client::url::{URLBuilder, build_url}; 194 - 195 - fn main() { 196 - // Using URLBuilder for complex URLs 197 - let mut builder = URLBuilder::new("pds.example.com"); 198 - builder.path("/xrpc/com.atproto.repo.listRecords"); 199 - builder.param("repo", "did:plc:user123"); 200 - builder.param("collection", "app.bsky.feed.post"); 201 - builder.param("limit", "50"); 202 - 203 - let url = builder.build(); 204 - // Result: "https://pds.example.com/xrpc/com.atproto.repo.listRecords?repo=did%3Aplc%3Auser123&collection=app.bsky.feed.post&limit=50" 205 - 206 - // Using convenience function for simple URLs 207 - let simple_url = build_url( 208 - "pds.example.com", 209 - "/xrpc/com.atproto.repo.getRecord", 210 - vec![ 211 - Some(("repo", "did:plc:user123")), 212 - Some(("collection", "app.bsky.feed.post")), 213 - Some(("rkey", "3l2uygzaf5c2b")), 214 - None, // Optional parameters can be None 215 - ] 216 - ); 217 - 218 - println!("Built URL: {}", simple_url); 219 - } 39 + let response = get_dpop_json(&http_client, &dpop_auth, "https://pds.example.com/xrpc/...").await?; 220 40 ``` 221 41 222 - ## AT Protocol Repository Operations 223 - 224 - The `com::atproto::repo` module provides client functions for the core AT Protocol repository XRPC methods: 225 - 226 - ### Supported Operations 227 - 228 - - **`get_record()`**: Retrieve a specific record by repository, collection, and record key 229 - - **`list_records()`**: List records in a collection with pagination and filtering support 230 - - **`create_record()`**: Create a new record in a repository with optional record key 231 - - **`put_record()`**: Update or create a record with a specific record key 232 - 233 - ### Request/Response Types 234 - 235 - - **`ListRecordsParams`**: Builder-style parameters for listing records with pagination 236 - - **`CreateRecordRequest<T>`**: Strongly-typed request for creating new records 237 - - **`PutRecordRequest<T>`**: Strongly-typed request for updating records 238 - - **`GetRecordResponse`**: Response containing record data, URI, and CID 239 - - **`ListRecordsResponse<T>`**: Paginated response with cursor support 240 - - **`CreateRecordResponse`**: Response with created record URI and CID 241 - - **`PutRecordResponse`**: Response with updated record URI and CID 242 - 243 - All operations support: 244 - - Generic record types with serde serialization/deserialization 245 - - Validation options for record schema compliance 246 - - Atomic commit operations with swap parameters 247 - - Comprehensive error handling with structured error types 248 - 249 - ## Modules 250 - 251 - - **[`client`]** - Core HTTP client operations with DPoP authentication support 252 - - **[`com::atproto::repo`]** - AT Protocol repository operations for record management 253 - - **[`url`]** - URL construction utilities with parameter encoding 254 - - **[`errors`]** - Structured error types for client operations 255 - 256 - ## Error Handling 257 - 258 - The crate uses structured error types with detailed error codes: 42 + ### Repository Operations 259 43 260 44 ```rust 261 - use atproto_client::errors::{ClientError, DPoPError}; 262 - 263 - // Example error handling 264 - match result { 265 - Err(ClientError::HttpRequestFailed { url, error }) => { 266 - println!("HTTP request to {} failed: {}", url, error); 267 - }, 268 - Err(ClientError::JsonParseFailed { url, error }) => { 269 - println!("JSON parsing failed for {}: {}", url, error); 270 - }, 271 - Ok(response) => println!("Success: {:?}", response), 272 - } 273 - 274 - // DPoP authentication errors 275 - match dpop_result { 276 - Err(DPoPError::ProofGenerationFailed { error }) => { 277 - println!("DPoP proof generation failed: {}", error); 278 - }, 279 - Err(DPoPError::HttpRequestFailed { url, error }) => { 280 - println!("DPoP authenticated request to {} failed: {}", url, error); 281 - }, 282 - Ok(response) => println!("Authenticated request successful: {:?}", response), 283 - } 284 - ``` 285 - 286 - ### Error Format 45 + use atproto_client::com::atproto::repo::{create_record, CreateRecordRequest}; 287 46 288 - All errors follow the standardized format: 47 + let create_request = CreateRecordRequest { 48 + repo: "did:plc:user123".to_string(), 49 + collection: "app.bsky.feed.post".to_string(), 50 + record: json!({"$type": "app.bsky.feed.post", "text": "Hello!"}), 51 + // ... 52 + }; 289 53 290 - ``` 291 - error-atproto-client-<domain>-<number> <message>: <details> 54 + let response = create_record(&http_client, &dpop_auth, pds_url, create_request).await?; 292 55 ``` 293 56 294 - Example error codes: 295 - - `error-atproto-client-http-1` - HTTP request failures 296 - - `error-atproto-client-http-2` - JSON parsing failures 297 - - `error-atproto-client-dpop-1` - DPoP proof generation failures 298 - - `error-atproto-client-dpop-2` - DPoP authenticated request failures 299 - - `error-atproto-client-dpop-3` - DPoP response parsing failures 300 - 301 - ## Authentication 302 - 303 - ### DPoP Authentication 304 - 305 - The library supports DPoP (Demonstration of Proof-of-Possession) authentication as specified in RFC 9449: 306 - 307 - - Automatic DPoP proof generation for each request 308 - - Built-in retry middleware for nonce-based challenges 309 - - Integration with OAuth access tokens 310 - - Support for both authorization and resource requests 311 - 312 - ### Key Requirements 313 - 314 - - Private key for DPoP proof signing (P-256 or K-256) 315 - - OAuth access token from authorization server 316 - - Issuer identifier for proof validation 317 - 318 - ## Dependencies 319 - 320 - This crate builds on: 321 - 322 - - [`atproto-identity`](../atproto-identity) - Cryptographic key operations and DID resolution 323 - - [`atproto-record`](../atproto-record) - AT Protocol record operations 324 - - [`atproto-oauth`](../atproto-oauth) - OAuth 2.0 and DPoP implementation 325 - - `reqwest` - HTTP client for network operations 326 - - `reqwest-middleware` - HTTP middleware support for DPoP retry logic 327 - - `reqwest-chain` - Middleware chaining for authentication flows 328 - - `serde_json` - JSON serialization for AT Protocol data structures 329 - - `tokio` - Async runtime for HTTP operations 330 - - `tracing` - Structured logging for debugging and monitoring 331 - - `thiserror` - Structured error type derivation 332 - 333 - ## Command Line Tools 334 - 335 - The crate includes one command-line tool for DPoP authentication testing: 336 - 337 - ### `atproto-client-dpop` 338 - 339 - A command-line tool for testing DPoP authentication flows with AT Protocol services. This tool demonstrates the complete DPoP authentication process including proof generation, HTTP request signing, and token usage. 340 - 341 - **Features:** 342 - - **DPoP Proof Generation**: Creates DPoP proofs for HTTP requests using private keys 343 - - **OAuth Integration**: Supports OAuth access tokens with DPoP binding 344 - - **HTTP Client Testing**: Tests DPoP authentication against real AT Protocol endpoints 345 - - **Request Signing**: Demonstrates proper DPoP header generation and validation 346 - - **Token Management**: Shows how to use DPoP-bound access tokens for API requests 57 + ## Command Line Examples 347 58 348 59 ```bash 349 - # Test DPoP authentication with an AT Protocol endpoint 60 + # Test DPoP authentication 350 61 cargo run --bin atproto-client-dpop \ 351 - --private-key did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA \ 352 - --access-token your_access_token \ 353 - --issuer did:plc:issuer123 \ 354 - --url https://pds.example.com/xrpc/com.atproto.repo.listRecords \ 62 + --private-key did:key:zQ3sh... \ 63 + --access-token token \ 64 + --issuer did:plc:issuer \ 65 + --url https://pds.example.com/xrpc/... \ 355 66 --method GET 356 - 357 - # Example POST request with DPoP authentication 358 - cargo run --bin atproto-client-dpop \ 359 - --private-key did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA \ 360 - --access-token your_access_token \ 361 - --issuer did:plc:issuer123 \ 362 - --url https://pds.example.com/xrpc/com.atproto.repo.createRecord \ 363 - --method POST \ 364 - --data '{"repo":"did:plc:user123","collection":"app.bsky.feed.post","record":{"$type":"app.bsky.feed.post","text":"Hello AT Protocol!"}}' 365 67 ``` 366 68 367 - **Arguments:** 368 - - `--private-key` - DID key string for DPoP proof signing 369 - - `--access-token` - OAuth access token for authentication 370 - - `--issuer` - Issuer DID for proof validation 371 - - `--url` - Target URL for the authenticated request 372 - - `--method` - HTTP method (GET, POST, PUT, DELETE) 373 - - `--data` - Optional JSON data for POST/PUT requests 374 - 375 - This tool is useful for: 376 - - Testing DPoP implementation against AT Protocol services 377 - - Validating authentication flows during development 378 - - Debugging DPoP proof generation and validation 379 - - Learning how DPoP authentication works in practice 380 - 381 - ## Contributing 382 - 383 - Contributions are welcome! Please ensure that: 384 - 385 - 1. All tests pass: `cargo test` 386 - 2. Code is properly formatted: `cargo fmt` 387 - 3. No linting issues: `cargo clippy` 388 - 4. New functionality includes appropriate tests and documentation 389 - 5. Error handling follows the project's structured error format 390 - 391 69 ## License 392 70 393 - This project is licensed under the MIT License. See the LICENSE file for details. 394 - 395 - ## Acknowledgments 396 - 397 - This library was extracted from the [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project, an open-source event and RSVP management and discovery application. 71 + MIT License
+243
crates/atproto-client/src/bin/atproto-client-app-password.rs
··· 1 + //! AT Protocol client app password tool for making authenticated XRPC calls. 2 + //! 3 + //! This binary tool makes XRPC calls using app password authentication by resolving 4 + //! subjects to DID documents and constructing authenticated requests. 5 + 6 + use anyhow::Result; 7 + use atproto_client::client::{ 8 + AppPasswordAuth, get_apppassword_json_with_headers, post_apppassword_json_with_headers, 9 + }; 10 + use atproto_identity::{ 11 + config::{CertificateBundles, DnsNameservers, default_env, optional_env, version}, 12 + plc, 13 + resolve::{create_resolver, resolve_subject}, 14 + web, 15 + }; 16 + use reqwest::header::HeaderMap; 17 + use std::{collections::HashMap, env}; 18 + 19 + fn print_usage() { 20 + println!("AT Protocol Client App Password Tool"); 21 + println!(); 22 + println!("Usage:"); 23 + println!(" atproto-client-app-password <subject> <access_token> <xrpc_path> [args...]"); 24 + println!(); 25 + println!("Arguments:"); 26 + println!(" subject Subject identifier to resolve"); 27 + println!(" access_token App password JWT access token"); 28 + println!(" xrpc_path XRPC path with optional prefix:"); 29 + println!(" - query:<path> for GET requests (default)"); 30 + println!(" - procedure:<path> for POST requests"); 31 + println!(" - <path> defaults to GET request"); 32 + println!( 33 + " key=value Additional query parameters (for GET requests) 34 + header=name=value Additional HTTP headers 35 + <file_path> JSON file path (required for procedure: prefix)" 36 + ); 37 + println!(); 38 + println!("Examples:"); 39 + println!(" # GET request (default behavior without prefix)"); 40 + println!( 41 + " atproto-client-app-password alice.bsky.social eyJ0... com.atproto.repo.listRecords repo=alice.bsky.social collection=app.bsky.feed.post" 42 + ); 43 + println!(" # GET request (explicit query: prefix)"); 44 + println!( 45 + " atproto-client-app-password alice.bsky.social eyJ0... query:com.atproto.repo.listRecords repo=alice.bsky.social collection=app.bsky.feed.post" 46 + ); 47 + println!(" # POST request (requires procedure: prefix and JSON file)"); 48 + println!( 49 + " atproto-client-app-password alice.bsky.social eyJ0... procedure:com.atproto.repo.createRecord data.json" 50 + ); 51 + } 52 + 53 + #[tokio::main] 54 + async fn main() -> Result<()> { 55 + // Parse command line arguments 56 + let args: Vec<String> = env::args().skip(1).collect(); 57 + 58 + if args.len() < 3 || args.iter().any(|arg| arg == "--help" || arg == "-h") { 59 + print_usage(); 60 + return Ok(()); 61 + } 62 + 63 + let subject = &args[0]; 64 + let access_token = &args[1]; 65 + let xrpc_path_with_prefix = &args[2]; 66 + 67 + // Parse the xrpc_path prefix (optional, defaults to query:) 68 + let (is_procedure, xrpc_path) = if let Some(path) = xrpc_path_with_prefix.strip_prefix("query:") 69 + { 70 + (false, path) 71 + } else if let Some(path) = xrpc_path_with_prefix.strip_prefix("procedure:") { 72 + (true, path) 73 + } else { 74 + // Default to query if no prefix is provided 75 + (false, xrpc_path_with_prefix.as_str()) 76 + }; 77 + 78 + // Parse additional arguments based on request type 79 + let mut query_params = HashMap::new(); 80 + let mut header_params = HashMap::new(); 81 + let mut json_data: Option<serde_json::Value> = None; 82 + let mut arg_index = 3; 83 + 84 + // For procedure calls, expect the next argument to be a file path 85 + if is_procedure { 86 + if arg_index >= args.len() { 87 + anyhow::bail!("procedure: prefix requires a JSON file path as the next argument"); 88 + } 89 + let file_path = &args[arg_index]; 90 + let file_content = std::fs::read_to_string(file_path) 91 + .map_err(|e| anyhow::anyhow!("Failed to read file '{}': {}", file_path, e))?; 92 + json_data = Some(serde_json::from_str(&file_content).map_err(|e| { 93 + anyhow::anyhow!("Failed to parse JSON from file '{}': {}", file_path, e) 94 + })?); 95 + arg_index += 1; 96 + } 97 + 98 + // Parse remaining key=value arguments and header=name=value arguments 99 + for arg in &args[arg_index..] { 100 + if let Some((key, value)) = arg.split_once('=') { 101 + if key == "header" { 102 + // Parse header=name=value format 103 + if let Some((header_name, header_value)) = value.split_once('=') { 104 + header_params.insert(header_name.to_string(), header_value.to_string()); 105 + } else { 106 + eprintln!("Warning: Ignoring invalid header format: {}", arg); 107 + eprintln!("Expected format: header=name=value"); 108 + } 109 + } else if is_procedure { 110 + eprintln!( 111 + "Warning: Query parameters are not supported for procedure calls. Ignoring: {}", 112 + arg 113 + ); 114 + } else { 115 + query_params.insert(key.to_string(), value.to_string()); 116 + } 117 + } else { 118 + eprintln!("Warning: Ignoring invalid argument format: {}", arg); 119 + eprintln!("Expected format: key=value or header=name=value"); 120 + } 121 + } 122 + 123 + println!("Making app password authenticated XRPC call"); 124 + println!("Subject: {}", subject); 125 + println!( 126 + "Request Type: {}", 127 + if is_procedure { 128 + "POST (procedure)" 129 + } else { 130 + "GET (query)" 131 + } 132 + ); 133 + println!("XRPC Path: {}", xrpc_path); 134 + if !query_params.is_empty() { 135 + println!("Query Parameters: {:?}", query_params); 136 + } 137 + if !header_params.is_empty() { 138 + println!("Additional Headers: {:?}", header_params); 139 + } 140 + if let Some(ref data) = json_data { 141 + println!("JSON Data: {}", serde_json::to_string_pretty(data)?); 142 + } 143 + 144 + // Set up HTTP client configuration 145 + let certificate_bundles: CertificateBundles = optional_env("CERTIFICATE_BUNDLES").try_into()?; 146 + let default_user_agent = format!( 147 + "atproto-identity-rs ({}; +https://tangled.sh/@smokesignal.events/atproto-identity-rs)", 148 + version()? 149 + ); 150 + let user_agent = default_env("USER_AGENT", &default_user_agent); 151 + let dns_nameservers: DnsNameservers = optional_env("DNS_NAMESERVERS").try_into()?; 152 + let plc_hostname = default_env("PLC_HOSTNAME", "plc.directory"); 153 + 154 + let mut client_builder = reqwest::Client::builder(); 155 + for ca_certificate in certificate_bundles.as_ref() { 156 + let cert = std::fs::read(ca_certificate)?; 157 + let cert = reqwest::Certificate::from_pem(&cert)?; 158 + client_builder = client_builder.add_root_certificate(cert); 159 + } 160 + 161 + client_builder = client_builder.user_agent(user_agent); 162 + let http_client = client_builder.build()?; 163 + 164 + let dns_resolver = create_resolver(dns_nameservers.as_ref()); 165 + 166 + println!("Resolving subject: {}", subject); 167 + 168 + // Resolve the subject to a DID 169 + let did = resolve_subject(&http_client, &dns_resolver, subject).await?; 170 + 171 + println!("Resolved DID: {}", did); 172 + 173 + // Get the DID document based on DID type 174 + let document = if did.starts_with("did:plc:") { 175 + plc::query(&http_client, &plc_hostname, &did).await? 176 + } else if did.starts_with("did:web:") { 177 + web::query(&http_client, &did).await? 178 + } else { 179 + anyhow::bail!("Unsupported DID method: {}", did); 180 + }; 181 + 182 + println!("Retrieved DID document for: {}", document.id); 183 + 184 + // Get PDS endpoint from the DID document 185 + let pds_endpoints = document.pds_endpoints(); 186 + let pds_endpoint = pds_endpoints 187 + .first() 188 + .ok_or_else(|| anyhow::anyhow!("No PDS endpoint found in DID document"))?; 189 + 190 + println!("Using PDS endpoint: {}", pds_endpoint); 191 + 192 + // Construct the URL 193 + let mut url = format!("{}/xrpc/{}", pds_endpoint, xrpc_path); 194 + 195 + // Add query parameters if any (only for GET requests) 196 + if !is_procedure && !query_params.is_empty() { 197 + let query_string: Vec<String> = query_params 198 + .iter() 199 + .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v))) 200 + .collect(); 201 + url.push('?'); 202 + url.push_str(&query_string.join("&")); 203 + } 204 + 205 + println!("Request URL: {}", url); 206 + 207 + // Create app password auth 208 + let app_auth = AppPasswordAuth { 209 + access_token: access_token.to_string(), 210 + }; 211 + 212 + // Create HeaderMap from header parameters 213 + let mut additional_headers = HeaderMap::new(); 214 + for (name, value) in header_params { 215 + if let (Ok(header_name), Ok(header_value)) = ( 216 + name.parse::<reqwest::header::HeaderName>(), 217 + value.parse::<reqwest::header::HeaderValue>(), 218 + ) { 219 + additional_headers.insert(header_name, header_value); 220 + } else { 221 + eprintln!("Warning: Invalid header name or value: {}={}", name, value); 222 + } 223 + } 224 + 225 + // Make the authenticated request 226 + println!("Making app password authenticated request..."); 227 + 228 + let response = if is_procedure { 229 + let data = 230 + json_data.ok_or_else(|| anyhow::anyhow!("No JSON data provided for procedure call"))?; 231 + post_apppassword_json_with_headers(&http_client, &app_auth, &url, data, &additional_headers) 232 + .await? 233 + } else { 234 + get_apppassword_json_with_headers(&http_client, &app_auth, &url, &additional_headers) 235 + .await? 236 + }; 237 + 238 + // Print the response 239 + println!("Response:"); 240 + println!("{}", serde_json::to_string_pretty(&response)?); 241 + 242 + Ok(()) 243 + }
+236
crates/atproto-client/src/bin/atproto-client-auth.rs
··· 1 + //! AT Protocol client authentication tool for app password session management. 2 + //! 3 + //! This binary tool provides commands for creating and refreshing app password 4 + //! authentication sessions with AT Protocol servers. 5 + 6 + use anyhow::Result; 7 + use atproto_identity::{ 8 + config::{CertificateBundles, DnsNameservers, default_env, optional_env, version}, 9 + plc, 10 + resolve::{create_resolver, resolve_subject}, 11 + web, 12 + }; 13 + use serde_json::json; 14 + use std::env; 15 + 16 + // Import from public module 17 + use atproto_client::com::atproto::server::{create_session, refresh_session}; 18 + 19 + fn print_usage() { 20 + println!("AT Protocol Client Authentication Tool"); 21 + println!(); 22 + println!("Usage:"); 23 + println!(" atproto-client-auth <command> [args...]"); 24 + println!(); 25 + println!("Commands:"); 26 + println!(" login <identifier> <password> [auth_factor_token]"); 27 + println!(" Create a new app-password session"); 28 + println!(" identifier: Handle or email for authentication"); 29 + println!(" password: App password or account password"); 30 + println!(" auth_factor_token: Optional 2FA token"); 31 + println!(); 32 + println!(" refresh <refresh_token>"); 33 + println!(" Refresh an existing app-password session"); 34 + println!(" refresh_token: JWT refresh token from previous session"); 35 + println!(); 36 + println!("Environment Variables:"); 37 + println!(" CERTIFICATE_BUNDLES: Custom CA certificate bundles"); 38 + println!(" USER_AGENT: Custom user agent string"); 39 + println!(" DNS_NAMESERVERS: Custom DNS nameservers"); 40 + println!(" PDS_ENDPOINT: Override PDS endpoint (skips DID resolution)"); 41 + println!(); 42 + println!("Examples:"); 43 + println!(" # Login with handle and app password"); 44 + println!(" atproto-client-auth login alice.bsky.social app-password-here"); 45 + println!(); 46 + println!(" # Login with email and 2FA"); 47 + println!(" atproto-client-auth login alice@example.com password123 123456"); 48 + println!(); 49 + println!(" # Refresh session"); 50 + println!(" atproto-client-auth refresh eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9..."); 51 + } 52 + 53 + #[tokio::main] 54 + async fn main() -> Result<()> { 55 + // Parse command line arguments 56 + let args: Vec<String> = env::args().skip(1).collect(); 57 + 58 + if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") { 59 + print_usage(); 60 + return Ok(()); 61 + } 62 + 63 + let command = &args[0]; 64 + 65 + // Set up HTTP client configuration 66 + let certificate_bundles: CertificateBundles = optional_env("CERTIFICATE_BUNDLES").try_into()?; 67 + let default_user_agent = format!( 68 + "atproto-identity-rs ({}; +https://tangled.sh/@smokesignal.events/atproto-identity-rs)", 69 + version()? 70 + ); 71 + let user_agent = default_env("USER_AGENT", &default_user_agent); 72 + let dns_nameservers: DnsNameservers = optional_env("DNS_NAMESERVERS").try_into()?; 73 + let plc_hostname = default_env("PLC_HOSTNAME", "plc.directory"); 74 + 75 + let mut client_builder = reqwest::Client::builder(); 76 + for ca_certificate in certificate_bundles.as_ref() { 77 + let cert = std::fs::read(ca_certificate)?; 78 + let cert = reqwest::Certificate::from_pem(&cert)?; 79 + client_builder = client_builder.add_root_certificate(cert); 80 + } 81 + 82 + client_builder = client_builder.user_agent(user_agent); 83 + let http_client = client_builder.build()?; 84 + 85 + let dns_resolver = create_resolver(dns_nameservers.as_ref()); 86 + 87 + match command.as_str() { 88 + "login" => { 89 + if args.len() < 3 { 90 + eprintln!("Error: login command requires identifier and password"); 91 + eprintln!(); 92 + print_usage(); 93 + return Ok(()); 94 + } 95 + 96 + let identifier = &args[1]; 97 + let password = &args[2]; 98 + let auth_factor_token = args.get(3).map(|s| s.as_str()); 99 + 100 + println!("Creating app password session"); 101 + println!("Identifier: {}", identifier); 102 + 103 + // Determine PDS endpoint 104 + let pds_endpoint = if let Ok(endpoint) = env::var("PDS_ENDPOINT") { 105 + println!("Using PDS endpoint from environment: {}", endpoint); 106 + endpoint 107 + } else { 108 + println!("Resolving identifier to find PDS endpoint..."); 109 + 110 + // Resolve the identifier to a DID 111 + let did = resolve_subject(&http_client, &dns_resolver, identifier).await?; 112 + println!("Resolved DID: {}", did); 113 + 114 + // Get the DID document based on DID type 115 + let document = if did.starts_with("did:plc:") { 116 + plc::query(&http_client, &plc_hostname, &did).await? 117 + } else if did.starts_with("did:web:") { 118 + web::query(&http_client, &did).await? 119 + } else { 120 + anyhow::bail!("Unsupported DID method: {}", did); 121 + }; 122 + 123 + println!("Retrieved DID document for: {}", document.id); 124 + 125 + // Get PDS endpoint from the DID document 126 + let pds_endpoints = document.pds_endpoints(); 127 + let pds_endpoint = pds_endpoints 128 + .first() 129 + .ok_or_else(|| anyhow::anyhow!("No PDS endpoint found in DID document"))?; 130 + 131 + println!("Found PDS endpoint: {}", pds_endpoint); 132 + pds_endpoint.to_string() 133 + }; 134 + 135 + // Create session 136 + println!("Creating session..."); 137 + let session = create_session( 138 + &http_client, 139 + &pds_endpoint, 140 + identifier, 141 + password, 142 + auth_factor_token, 143 + ) 144 + .await?; 145 + 146 + println!("Session created successfully!"); 147 + println!(); 148 + println!("Session Details:"); 149 + println!(" DID: {}", session.did); 150 + println!(" Handle: {}", session.handle); 151 + println!(" Email: {}", session.email); 152 + println!(); 153 + println!("Tokens (save these for authenticated requests):"); 154 + println!(" Access Token: {}", session.access_jwt); 155 + println!(" Refresh Token: {}", session.refresh_jwt); 156 + println!(); 157 + println!("Session JSON:"); 158 + println!( 159 + "{}", 160 + serde_json::to_string_pretty(&json!({ 161 + "did": session.did, 162 + "handle": session.handle, 163 + "email": session.email, 164 + "accessJwt": session.access_jwt, 165 + "refreshJwt": session.refresh_jwt 166 + }))? 167 + ); 168 + } 169 + 170 + "refresh" => { 171 + if args.len() < 2 { 172 + eprintln!("Error: refresh command requires refresh_token"); 173 + eprintln!(); 174 + print_usage(); 175 + return Ok(()); 176 + } 177 + 178 + let refresh_token = &args[1]; 179 + 180 + println!("Refreshing app password session"); 181 + 182 + // Determine PDS endpoint 183 + let pds_endpoint = if let Ok(endpoint) = env::var("PDS_ENDPOINT") { 184 + println!("Using PDS endpoint from environment: {}", endpoint); 185 + endpoint 186 + } else { 187 + // For refresh, we don't have the identifier, so require PDS_ENDPOINT 188 + eprintln!("Error: PDS_ENDPOINT environment variable required for refresh command"); 189 + eprintln!("Set PDS_ENDPOINT to your PDS URL (e.g., https://bsky.social)"); 190 + return Ok(()); 191 + }; 192 + 193 + // Refresh session 194 + println!("Refreshing session..."); 195 + let refreshed_session = 196 + refresh_session(&http_client, &pds_endpoint, refresh_token).await?; 197 + 198 + println!("Session refreshed successfully!"); 199 + println!(); 200 + println!("Updated Session Details:"); 201 + println!(" DID: {}", refreshed_session.did); 202 + println!(" Handle: {}", refreshed_session.handle); 203 + if let Some(active) = refreshed_session.active { 204 + println!(" Active: {}", active); 205 + } 206 + if let Some(ref status) = refreshed_session.status { 207 + println!(" Status: {}", status); 208 + } 209 + println!(); 210 + println!("New Tokens (save these for authenticated requests):"); 211 + println!(" Access Token: {}", refreshed_session.access_jwt); 212 + println!(" Refresh Token: {}", refreshed_session.refresh_jwt); 213 + println!(); 214 + println!("Session JSON:"); 215 + println!( 216 + "{}", 217 + serde_json::to_string_pretty(&json!({ 218 + "did": refreshed_session.did, 219 + "handle": refreshed_session.handle, 220 + "accessJwt": refreshed_session.access_jwt, 221 + "refreshJwt": refreshed_session.refresh_jwt, 222 + "active": refreshed_session.active, 223 + "status": refreshed_session.status 224 + }))? 225 + ); 226 + } 227 + 228 + _ => { 229 + eprintln!("Error: Unknown command '{}'", command); 230 + eprintln!(); 231 + print_usage(); 232 + } 233 + } 234 + 235 + Ok(()) 236 + }
+236
crates/atproto-client/src/client.rs
··· 26 26 pub oauth_issuer: String, 27 27 } 28 28 29 + /// App password authentication credentials for authenticated HTTP requests. 30 + /// 31 + /// Contains the JWT access token for Bearer token authentication. 32 + pub struct AppPasswordAuth { 33 + /// JWT access token for the Authorization header 34 + pub access_token: String, 35 + } 36 + 29 37 /// Performs an unauthenticated HTTP GET request and parses the response as JSON. 30 38 /// 31 39 /// # Arguments ··· 390 398 391 399 Ok(value) 392 400 } 401 + 402 + /// Performs an unauthenticated HTTP POST request with JSON body and parses the response as JSON. 403 + /// 404 + /// # Arguments 405 + /// 406 + /// * `http_client` - The HTTP client to use for the request 407 + /// * `url` - The URL to request 408 + /// * `data` - The JSON data to send in the request body 409 + /// 410 + /// # Returns 411 + /// 412 + /// The parsed JSON response as a `serde_json::Value` 413 + /// 414 + /// # Errors 415 + /// 416 + /// Returns `ClientError::HttpRequestFailed` if the HTTP request fails, 417 + /// or `ClientError::JsonParseFailed` if JSON parsing fails. 418 + pub async fn post_json( 419 + http_client: &reqwest::Client, 420 + url: &str, 421 + data: serde_json::Value, 422 + ) -> Result<serde_json::Value> { 423 + let empty = HeaderMap::default(); 424 + post_json_with_headers(http_client, url, data, &empty).await 425 + } 426 + 427 + /// Performs an unauthenticated HTTP POST request with JSON body and additional headers, and parses the response as JSON. 428 + /// 429 + /// # Arguments 430 + /// 431 + /// * `http_client` - The HTTP client to use for the request 432 + /// * `url` - The URL to request 433 + /// * `data` - The JSON data to send in the request body 434 + /// * `additional_headers` - Additional HTTP headers to include in the request 435 + /// 436 + /// # Returns 437 + /// 438 + /// The parsed JSON response as a `serde_json::Value` 439 + /// 440 + /// # Errors 441 + /// 442 + /// Returns `ClientError::HttpRequestFailed` if the HTTP request fails, 443 + /// or `ClientError::JsonParseFailed` if JSON parsing fails. 444 + pub async fn post_json_with_headers( 445 + http_client: &reqwest::Client, 446 + url: &str, 447 + data: serde_json::Value, 448 + additional_headers: &HeaderMap, 449 + ) -> Result<serde_json::Value> { 450 + let http_response = http_client 451 + .post(url) 452 + .headers(additional_headers.clone()) 453 + .json(&data) 454 + .send() 455 + .instrument(tracing::info_span!("post_json_with_headers", url = %url)) 456 + .await 457 + .map_err(|error| ClientError::HttpRequestFailed { 458 + url: url.to_string(), 459 + error, 460 + })?; 461 + 462 + let value = http_response 463 + .json::<serde_json::Value>() 464 + .await 465 + .map_err(|error| ClientError::JsonParseFailed { 466 + url: url.to_string(), 467 + error, 468 + })?; 469 + 470 + Ok(value) 471 + } 472 + 473 + /// Performs an app password-authenticated HTTP GET request and parses the response as JSON. 474 + /// 475 + /// # Arguments 476 + /// 477 + /// * `http_client` - The HTTP client to use for the request 478 + /// * `app_auth` - App password authentication credentials 479 + /// * `url` - The URL to request 480 + /// 481 + /// # Returns 482 + /// 483 + /// The parsed JSON response as a `serde_json::Value` 484 + /// 485 + /// # Errors 486 + /// 487 + /// Returns `ClientError::HttpRequestFailed` if the HTTP request fails, 488 + /// or `ClientError::JsonParseFailed` if JSON parsing fails. 489 + pub async fn get_apppassword_json( 490 + http_client: &reqwest::Client, 491 + app_auth: &AppPasswordAuth, 492 + url: &str, 493 + ) -> Result<serde_json::Value> { 494 + let empty = HeaderMap::default(); 495 + get_apppassword_json_with_headers(http_client, app_auth, url, &empty).await 496 + } 497 + 498 + /// Performs an app password-authenticated HTTP GET request with additional headers and parses the response as JSON. 499 + /// 500 + /// # Arguments 501 + /// 502 + /// * `http_client` - The HTTP client to use for the request 503 + /// * `app_auth` - App password authentication credentials 504 + /// * `url` - The URL to request 505 + /// * `additional_headers` - Additional HTTP headers to include in the request 506 + /// 507 + /// # Returns 508 + /// 509 + /// The parsed JSON response as a `serde_json::Value` 510 + /// 511 + /// # Errors 512 + /// 513 + /// Returns `ClientError::HttpRequestFailed` if the HTTP request fails, 514 + /// or `ClientError::JsonParseFailed` if JSON parsing fails. 515 + pub async fn get_apppassword_json_with_headers( 516 + http_client: &reqwest::Client, 517 + app_auth: &AppPasswordAuth, 518 + url: &str, 519 + additional_headers: &HeaderMap, 520 + ) -> Result<serde_json::Value> { 521 + let mut headers = additional_headers.clone(); 522 + headers.insert( 523 + reqwest::header::AUTHORIZATION, 524 + reqwest::header::HeaderValue::from_str(&format!("Bearer {}", app_auth.access_token))?, 525 + ); 526 + 527 + let http_response = http_client 528 + .get(url) 529 + .headers(headers) 530 + .send() 531 + .instrument(tracing::info_span!("get_apppassword_json_with_headers", url = %url)) 532 + .await 533 + .map_err(|error| ClientError::HttpRequestFailed { 534 + url: url.to_string(), 535 + error, 536 + })?; 537 + 538 + let value = http_response 539 + .json::<serde_json::Value>() 540 + .await 541 + .map_err(|error| ClientError::JsonParseFailed { 542 + url: url.to_string(), 543 + error, 544 + })?; 545 + 546 + Ok(value) 547 + } 548 + 549 + /// Performs an app password-authenticated HTTP POST request with JSON body and parses the response as JSON. 550 + /// 551 + /// # Arguments 552 + /// 553 + /// * `http_client` - The HTTP client to use for the request 554 + /// * `app_auth` - App password authentication credentials 555 + /// * `url` - The URL to request 556 + /// * `data` - The JSON data to send in the request body 557 + /// 558 + /// # Returns 559 + /// 560 + /// The parsed JSON response as a `serde_json::Value` 561 + /// 562 + /// # Errors 563 + /// 564 + /// Returns `ClientError::HttpRequestFailed` if the HTTP request fails, 565 + /// or `ClientError::JsonParseFailed` if JSON parsing fails. 566 + pub async fn post_apppassword_json( 567 + http_client: &reqwest::Client, 568 + app_auth: &AppPasswordAuth, 569 + url: &str, 570 + data: serde_json::Value, 571 + ) -> Result<serde_json::Value> { 572 + let empty = HeaderMap::default(); 573 + post_apppassword_json_with_headers(http_client, app_auth, url, data, &empty).await 574 + } 575 + 576 + /// Performs an app password-authenticated HTTP POST request with JSON body and additional headers, and parses the response as JSON. 577 + /// 578 + /// # Arguments 579 + /// 580 + /// * `http_client` - The HTTP client to use for the request 581 + /// * `app_auth` - App password authentication credentials 582 + /// * `url` - The URL to request 583 + /// * `data` - The JSON data to send in the request body 584 + /// * `additional_headers` - Additional HTTP headers to include in the request 585 + /// 586 + /// # Returns 587 + /// 588 + /// The parsed JSON response as a `serde_json::Value` 589 + /// 590 + /// # Errors 591 + /// 592 + /// Returns `ClientError::HttpRequestFailed` if the HTTP request fails, 593 + /// or `ClientError::JsonParseFailed` if JSON parsing fails. 594 + pub async fn post_apppassword_json_with_headers( 595 + http_client: &reqwest::Client, 596 + app_auth: &AppPasswordAuth, 597 + url: &str, 598 + data: serde_json::Value, 599 + additional_headers: &HeaderMap, 600 + ) -> Result<serde_json::Value> { 601 + let mut headers = additional_headers.clone(); 602 + headers.insert( 603 + reqwest::header::AUTHORIZATION, 604 + reqwest::header::HeaderValue::from_str(&format!("Bearer {}", app_auth.access_token))?, 605 + ); 606 + 607 + let http_response = http_client 608 + .post(url) 609 + .headers(headers) 610 + .json(&data) 611 + .send() 612 + .instrument(tracing::info_span!("post_apppassword_json_with_headers", url = %url)) 613 + .await 614 + .map_err(|error| ClientError::HttpRequestFailed { 615 + url: url.to_string(), 616 + error, 617 + })?; 618 + 619 + let value = http_response 620 + .json::<serde_json::Value>() 621 + .await 622 + .map_err(|error| ClientError::JsonParseFailed { 623 + url: url.to_string(), 624 + error, 625 + })?; 626 + 627 + Ok(value) 628 + }
+161
crates/atproto-client/src/com_atproto_server.rs
··· 1 + //! AT Protocol server operations for session management. 2 + //! 3 + //! This module provides client functions for interacting with AT Protocol server endpoints, 4 + //! including session creation and refresh operations using app passwords. Supports the 5 + //! `com.atproto.server` XRPC methods for authentication and session management. 6 + //! 7 + //! ## Operations 8 + //! 9 + //! - **`create_session()`**: Create a new authentication session with app password 10 + //! - **`refresh_session()`**: Refresh an existing authentication session 11 + //! 12 + //! ## Request/Response Types 13 + //! 14 + //! - **`CreateSessionRequest`**: Parameters for creating a new session 15 + //! - **`AppPasswordSession`**: Response containing session data and tokens 16 + //! - **`RefreshSessionResponse`**: Response from session refresh operation 17 + //! 18 + //! ## Authentication 19 + //! 20 + //! Session creation uses app password authentication, while session refresh requires 21 + //! the refresh JWT token from a previous session. 22 + 23 + use anyhow::Result; 24 + use serde::{Deserialize, Serialize}; 25 + 26 + use crate::{client::post_json, url::URLBuilder}; 27 + 28 + /// Request to create a new authentication session. 29 + #[derive(Debug, Serialize, Clone)] 30 + pub struct CreateSessionRequest { 31 + /// Handle or other identifier supported by the server for the authenticating user 32 + pub identifier: String, 33 + /// User password or app password 34 + pub password: String, 35 + /// Optional two-factor authentication token 36 + #[serde(skip_serializing_if = "Option::is_none", rename = "authFactorToken")] 37 + pub auth_factor_token: Option<String>, 38 + } 39 + 40 + /// App password session data returned from successful authentication. 41 + #[derive(Debug, Deserialize, Clone)] 42 + pub struct AppPasswordSession { 43 + /// Distributed identifier for the authenticated account 44 + pub did: String, 45 + /// Handle for the authenticated account 46 + pub handle: String, 47 + /// Email address for the authenticated account 48 + pub email: String, 49 + /// JWT access token for authenticated requests 50 + #[serde(rename = "accessJwt")] 51 + pub access_jwt: String, 52 + /// JWT refresh token for obtaining new access tokens 53 + #[serde(rename = "refreshJwt")] 54 + pub refresh_jwt: String, 55 + } 56 + 57 + /// Response from refreshing an authentication session. 58 + #[derive(Debug, Deserialize, Clone)] 59 + pub struct RefreshSessionResponse { 60 + /// Distributed identifier for the authenticated account 61 + pub did: String, 62 + /// Handle for the authenticated account 63 + pub handle: String, 64 + /// JWT access token for authenticated requests 65 + #[serde(rename = "accessJwt")] 66 + pub access_jwt: String, 67 + /// JWT refresh token for obtaining new access tokens 68 + #[serde(rename = "refreshJwt")] 69 + pub refresh_jwt: String, 70 + /// Whether the account is active 71 + #[serde(skip_serializing_if = "Option::is_none")] 72 + pub active: Option<bool>, 73 + /// Account status (e.g., "takendown", "suspended", "deactivated") 74 + #[serde(skip_serializing_if = "Option::is_none")] 75 + pub status: Option<String>, 76 + } 77 + 78 + /// Creates a new authentication session using app password credentials. 79 + /// 80 + /// # Arguments 81 + /// 82 + /// * `http_client` - HTTP client for making requests 83 + /// * `base_url` - Base URL of the AT Protocol server 84 + /// * `identifier` - Handle or other identifier for the user 85 + /// * `password` - User password or app password 86 + /// * `auth_factor_token` - Optional two-factor authentication token 87 + /// 88 + /// # Returns 89 + /// 90 + /// The created session data including access and refresh tokens 91 + /// 92 + /// # Errors 93 + /// 94 + /// Returns errors for HTTP request failures, authentication failures, 95 + /// or JSON parsing failures. 96 + pub async fn create_session( 97 + http_client: &reqwest::Client, 98 + base_url: &str, 99 + identifier: &str, 100 + password: &str, 101 + auth_factor_token: Option<&str>, 102 + ) -> Result<AppPasswordSession> { 103 + let mut url_builder = URLBuilder::new(base_url); 104 + url_builder.path("/xrpc/com.atproto.server.createSession"); 105 + let url = url_builder.build(); 106 + 107 + tracing::info!(?url, identifier = %identifier, "create_session"); 108 + 109 + let request = CreateSessionRequest { 110 + identifier: identifier.to_string(), 111 + password: password.to_string(), 112 + auth_factor_token: auth_factor_token.map(|s| s.to_string()), 113 + }; 114 + 115 + let value = serde_json::to_value(request)?; 116 + 117 + post_json(http_client, &url, value) 118 + .await 119 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())) 120 + } 121 + 122 + /// Refreshes an existing authentication session using a refresh token. 123 + /// 124 + /// # Arguments 125 + /// 126 + /// * `http_client` - HTTP client for making requests 127 + /// * `base_url` - Base URL of the AT Protocol server 128 + /// * `refresh_token` - JWT refresh token from a previous session 129 + /// 130 + /// # Returns 131 + /// 132 + /// The refreshed session data with new access and refresh tokens 133 + /// 134 + /// # Errors 135 + /// 136 + /// Returns errors for HTTP request failures, authentication failures, 137 + /// or JSON parsing failures. 138 + pub async fn refresh_session( 139 + http_client: &reqwest::Client, 140 + base_url: &str, 141 + refresh_token: &str, 142 + ) -> Result<RefreshSessionResponse> { 143 + let mut url_builder = URLBuilder::new(base_url); 144 + url_builder.path("/xrpc/com.atproto.server.refreshSession"); 145 + let url = url_builder.build(); 146 + 147 + tracing::info!(?url, "refresh_session"); 148 + 149 + // Create a new client with the refresh token in Authorization header 150 + let mut headers = reqwest::header::HeaderMap::new(); 151 + headers.insert( 152 + reqwest::header::AUTHORIZATION, 153 + reqwest::header::HeaderValue::from_str(&format!("Bearer {}", refresh_token))?, 154 + ); 155 + 156 + let response = http_client.post(&url).headers(headers).send().await?; 157 + 158 + let value = response.json::<serde_json::Value>().await?; 159 + 160 + serde_json::from_value(value).map_err(|err| err.into()) 161 + }
+10 -24
crates/atproto-client/src/lib.rs
··· 1 - //! # AT Protocol HTTP Client 2 - //! 3 - //! Comprehensive HTTP client functionality for AT Protocol services with support for 4 - //! authenticated requests, DPoP authentication, and structured error handling. 5 - //! 6 - //! ## Features 7 - //! 8 - //! - **Authenticated HTTP Requests**: DPoP-based authentication for secure API access 9 - //! - **URL Building**: Utilities for constructing AT Protocol service URLs 10 - //! - **Error Handling**: Structured error types for client operations 11 - //! - **Repository Operations**: Built-in support for AT Protocol repository endpoints 12 - //! 13 - //! ## Modules 14 - //! 15 - //! - [`client`]: Core HTTP client with authentication support 16 - //! - [`errors`]: Structured error types for client operations 17 - //! - [`url`]: URL building utilities for AT Protocol services 18 - //! - [`com::atproto::repo`]: Repository operation implementations 1 + //! HTTP client for AT Protocol services with DPoP authentication and XRPC support. 19 2 //! 20 - //! ## CLI Tools 21 - //! 22 - //! This crate provides the following binary tools: 23 - //! 24 - //! - **`atproto-client-dpop`**: Command-line tool for making authenticated XRPC calls 25 - //! using DPoP authentication 3 + //! Provides binaries: 4 + //! - `atproto-client-dpop`: Make authenticated XRPC calls using DPoP tokens 5 + //! - `atproto-client-auth`: Manage app password authentication and sessions 6 + //! - `atproto-client-app-password`: Make authenticated XRPC calls using Bearer tokens 26 7 27 8 #![warn(missing_docs)] 28 9 ··· 31 12 pub mod url; 32 13 33 14 mod com_atproto_repo; 15 + mod com_atproto_server; 34 16 35 17 /// AT Protocol namespace modules. 36 18 pub mod com { ··· 39 21 /// Repository operations for AT Protocol records. 40 22 pub mod repo { 41 23 pub use crate::com_atproto_repo::*; 24 + } 25 + /// Server operations for AT Protocol authentication and session management. 26 + pub mod server { 27 + pub use crate::com_atproto_server::*; 42 28 } 43 29 } 44 30 }
+22 -248
crates/atproto-identity/README.md
··· 1 1 # atproto-identity 2 2 3 - A Rust library for AT Protocol identity resolution and management. 3 + Identity management library for AT Protocol with DID resolution and cryptographic operations. 4 4 5 5 ## Overview 6 6 7 - `atproto-identity` provides comprehensive support for resolving and managing identities in the AT Protocol ecosystem. This library handles multiple DID (Decentralized Identifier) methods including `did:plc` and `did:web`, as well as AT Protocol handle resolution via both DNS and HTTP methods. 8 - 9 - This project was extracted from the open-sourced [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project and is designed to be a standalone, reusable library for AT Protocol identity operations. 10 - 11 - ## Features 12 - 13 - - **Handle Resolution**: Resolve AT Protocol handles to DIDs using DNS TXT records and HTTP well-known endpoints 14 - - **DID Document Retrieval**: Fetch and parse DID documents for `did:plc` and `did:web` identifiers 15 - - **Multiple Resolution Methods**: Supports both DNS and HTTP-based handle resolution with conflict detection 16 - - **Configurable DNS**: Custom DNS nameserver support with fallback to system defaults 17 - - **Cryptographic Key Operations**: Support for P-256 and K-256 key identification, signature validation, and signing 18 - - **Structured Logging**: Built-in tracing support for debugging and monitoring 19 - - **Type Safety**: Comprehensive error handling with structured error types 20 - 21 - ## Supported DID Methods 7 + `atproto-identity` provides comprehensive support for resolving and managing identities in the AT Protocol ecosystem. This library handles multiple DID methods including `did:plc` and `did:web`, as well as AT Protocol handle resolution via DNS and HTTP methods. 22 8 23 - - **did-method-plc**: Public Ledger of Credentials DIDs via PLC directory 24 - - **did-method-web**: Web-based DIDs following the did:web specification with URL conversion utilities 25 - - **ATProtocol Handle Resolution**: AT Protocol handles (e.g., `ngerakines.me`) can be resolved to DIDs 9 + ## Binaries 26 10 27 - ## Installation 28 - 29 - Add this to your `Cargo.toml`: 30 - 31 - ```toml 32 - [dependencies] 33 - atproto-identity = "0.6.0" 34 - ``` 11 + - **atproto-identity-resolve**: Resolves handles and DIDs to their canonical identifiers 12 + - **atproto-identity-key**: Generates and manages P-256 and K-256 cryptographic keys 13 + - **atproto-identity-sign**: Creates cryptographic signatures for JSON data 14 + - **atproto-identity-validate**: Verifies signatures against public keys 35 15 36 16 ## Usage 37 17 ··· 40 20 ```rust 41 21 use atproto_identity::resolve::{resolve_subject, create_resolver}; 42 22 43 - #[tokio::main] 44 - async fn main() -> anyhow::Result<()> { 45 - let http_client = reqwest::Client::new(); 46 - let dns_resolver = create_resolver(&[]); 47 - 48 - let did = resolve_subject(&http_client, &dns_resolver, "ngerakines.me").await?; 49 - println!("Resolved DID: {}", did); 50 - 51 - Ok(()) 52 - } 53 - ``` 54 - 55 - ### DID Document Retrieval 56 - 57 - ```rust 58 - use atproto_identity::{plc, web}; 23 + let http_client = reqwest::Client::new(); 24 + let dns_resolver = create_resolver(&[]); 59 25 60 - #[tokio::main] 61 - async fn main() -> anyhow::Result<()> { 62 - let http_client = reqwest::Client::new(); 63 - 64 - // Query PLC DID document 65 - let plc_doc = plc::query(&http_client, "plc.directory", "did:plc:example123").await?; 66 - 67 - // Query Web DID document 68 - let web_doc = web::query(&http_client, "did:web:example.com").await?; 69 - 70 - // Convert Web DID to URL (for custom processing) 71 - let did_url = web::did_web_to_url("did:web:example.com")?; 72 - println!("DID document URL: {}", did_url); 73 - 74 - Ok(()) 75 - } 26 + let did = resolve_subject(&http_client, &dns_resolver, "alice.bsky.social").await?; 76 27 ``` 77 28 78 - ### Web DID URL Conversion 79 - 80 - The `web` module provides utilities for converting DID identifiers to their HTTPS document URLs: 29 + ### Key Operations 81 30 82 31 ```rust 83 - use atproto_identity::web; 32 + use atproto_identity::key::{identify_key, validate}; 84 33 85 - fn main() -> anyhow::Result<()> { 86 - // Convert simple hostname DID 87 - let url = web::did_web_to_url("did:web:example.com")?; 88 - // Returns: "https://example.com/.well-known/did.json" 89 - 90 - // Convert DID with path components 91 - let url = web::did_web_to_url("did:web:example.com:path:subpath")?; 92 - // Returns: "https://example.com/path/subpath/did.json" 93 - 94 - Ok(()) 95 - } 34 + let key_data = identify_key("did:key:zQ3sh...")?; 35 + validate(&key_data, &signature, content)?; 96 36 ``` 97 37 98 - ### Cryptographic Key Operations 99 - 100 - The `key` module provides utilities for working with cryptographic keys: 101 - 102 - ```rust 103 - use atproto_identity::key::{identify_key, validate, KeyType}; 104 - 105 - fn main() -> Result<(), Box<dyn std::error::Error>> { 106 - // Identify a key from a DID key string 107 - let key_data = identify_key("did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA")?; 108 - 109 - match key_data.0 { 110 - KeyType::K256Public => println!("K-256 public key"), 111 - KeyType::P256Public => println!("P-256 public key"), 112 - KeyType::K256Private => println!("K-256 private key"), 113 - KeyType::P256Private => println!("P-256 private key"), 114 - } 115 - 116 - // Validate a signature (example with dummy data) 117 - let content = b"hello world"; 118 - let signature = vec![0u8; 64]; // Replace with actual signature 119 - validate(&key_data, &signature, content)?; 120 - 121 - Ok(()) 122 - } 123 - ``` 124 - 125 - ### Configuration 126 - 127 - The library supports various configuration options through environment variables: 128 - 129 - ```bash 130 - # Custom PLC directory hostname 131 - export PLC_HOSTNAME=plc.directory 132 - 133 - # Custom DNS nameservers (semicolon-separated) 134 - export DNS_NAMESERVERS=8.8.8.8;1.1.1.1 135 - 136 - # Custom CA certificate bundles (semicolon-separated paths) 137 - export CERTIFICATE_BUNDLES=/path/to/cert1.pem;/path/to/cert2.pem 138 - 139 - # Custom User-Agent string 140 - export USER_AGENT="my-app/1.0" 141 - ``` 142 - 143 - ## Command Line Tools 144 - 145 - The library includes four command-line tools for AT Protocol identity operations: 146 - 147 - ### `atproto-identity-resolve` 148 - 149 - Resolves AT Protocol handles and DIDs to their canonical DID identifiers and optionally retrieves full DID documents. This tool supports both `did:plc` and `did:web` methods with configurable DNS and HTTP settings for comprehensive identity resolution. 150 - 151 - **Features:** 152 - - **Handle Resolution**: Converts AT Protocol handles (e.g., `alice.bsky.social`) to DIDs 153 - - **DID Document Retrieval**: Fetches complete DID documents with verification methods 154 - - **Multi-Method Support**: Handles both PLC directory and Web DID resolution 155 - - **DNS Configuration**: Supports custom DNS nameservers and certificate bundles 156 - - **Output Options**: Choose between DID-only output or full document retrieval 38 + ## Command Line Examples 157 39 158 40 ```bash 159 - # Resolve a handle to its DID 41 + # Resolve a handle to DID 160 42 cargo run --bin atproto-identity-resolve alice.bsky.social 161 43 162 - # Resolve a DID and get full DID document 163 - cargo run --bin atproto-identity-resolve --did-document did:plc:user123 164 - 165 - # Resolve with custom configuration 166 - PLC_HOSTNAME=plc.directory DNS_NAMESERVERS="8.8.8.8;1.1.1.1" \ 167 - cargo run --bin atproto-identity-resolve --did-document alice.bsky.social 168 - ``` 169 - 170 - ### `atproto-identity-sign` 171 - 172 - Creates cryptographic signatures of JSON data using AT Protocol DID keys. This tool reads JSON files, serializes them using IPLD DAG-CBOR format for consistency, and produces multibase-encoded signatures suitable for AT Protocol operations. 173 - 174 - **Features:** 175 - - **DID Key Support**: Works with both P-256 and K-256 private keys 176 - - **JSON Processing**: Handles arbitrary JSON data structures 177 - - **IPLD Serialization**: Uses DAG-CBOR for deterministic serialization 178 - - **Multibase Output**: Produces signatures in multibase format 179 - - **File Input**: Reads JSON data from specified files 180 - 181 - ```bash 182 - # Sign a JSON file with a DID private key 183 - cargo run --bin atproto-identity-sign did:key:zQ3shNzMp4oaaQ1... data.json 184 - 185 - # Example output: multibase-encoded signature string 186 - # uEiB5vJz8aZhpx3bY2nKfRzPpLmQwA8Z9qXhNvYtF2gH7... 187 - ``` 188 - 189 - ### `atproto-identity-validate` 190 - 191 - Verifies cryptographic signatures of JSON data using AT Protocol DID keys. This tool validates that signatures were created by the holder of a specific private key, ensuring data integrity and authenticity for AT Protocol operations. 192 - 193 - **Features:** 194 - - **Signature Verification**: Validates multibase-encoded signatures 195 - - **DID Key Support**: Works with P-256 and K-256 public keys 196 - - **IPLD Deserialization**: Uses DAG-CBOR for consistent verification 197 - - **File Processing**: Reads JSON data and signatures from files 198 - - **Exit Code Reporting**: Returns appropriate exit codes for success/failure 199 - 200 - ```bash 201 - # Validate a signature against a JSON file 202 - cargo run --bin atproto-identity-validate did:key:zQ3shNzMp4oaaQ1... data.json uEiB5vJz8aZ... 203 - 204 - # Successful validation returns exit code 0 205 - # Failed validation returns exit code 1 206 - ``` 207 - 208 - **Arguments:** 209 - - `<public_key>` - DID key string for verification (did:key:...) 210 - - `<data_file>` - JSON file containing the original data 211 - - `<signature>` - Multibase-encoded signature to verify 212 - 213 - ### `atproto-identity-key` 214 - 215 - Provides comprehensive cryptographic key management capabilities for AT Protocol operations, including key generation and inspection for both P-256 and K-256 elliptic curves. 216 - 217 - **Features:** 218 - - **Key Generation**: Creates new private keys for both curve types 219 - - **Secure Random**: Uses cryptographically secure random number generation 220 - - **DID Key Format**: Outputs keys in standard DID key format 221 - - **Multiple Algorithms**: Supports both P-256 (secp256r1) and K-256 (secp256k1) curves 222 - - **Development Ready**: Generated keys ready for use in AT Protocol operations 223 - 224 - ```bash 225 - # Generate a new P-256 private key (recommended for most AT Protocol use) 44 + # Generate a new P-256 key 226 45 cargo run --bin atproto-identity-key generate p256 227 46 228 - # Generate a new K-256 private key (Bitcoin-style curve) 229 - cargo run --bin atproto-identity-key generate k256 47 + # Sign JSON data 48 + cargo run --bin atproto-identity-sign did:key:zQ3sh... data.json 230 49 231 - # Example output format: 232 - # did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA 50 + # Verify a signature 51 + cargo run --bin atproto-identity-validate did:key:zQ3sh... data.json signature 233 52 ``` 234 53 235 - **Key Types:** 236 - - **P-256**: NIST P-256 curve (secp256r1) - Recommended for new applications 237 - - **K-256**: secp256k1 curve (same as Bitcoin) - For Bitcoin-compatible operations 238 - 239 - **Security Note:** Generated keys are output to stdout and should be stored securely. Never commit private keys to version control or share them publicly. 240 - 241 - ## Architecture 242 - 243 - The library is organized into several modules: 244 - 245 - - **resolve**: Core resolution logic for handles and DIDs 246 - - **plc**: PLC directory client for `did:plc` resolution 247 - - **web**: Web DID client for `did:web` resolution and URL conversion 248 - - **model**: Data structures for DID documents and AT Protocol entities 249 - - **validation**: Input validation for handles and DIDs 250 - - **config**: Configuration management and environment variable handling 251 - - **errors**: Structured error types following project conventions 252 - - **key**: Cryptographic key operations including signature validation and key identification for P-256 and K-256 curves 253 - 254 - ## Error Handling 255 - 256 - All errors follow a structured format: 257 - 258 - ``` 259 - error-atproto-identity-<domain>-<number> <message>: <details> 260 - ``` 261 - 262 - Examples: 263 - - `error-atproto-identity-resolve-1 Multiple DIDs resolved for method` 264 - - `error-atproto-identity-plc-1 HTTP request failed: https://plc.directory/did:plc:example Not Found` 265 - - `error-did-web-1 Invalid DID format: missing 'did:web:' prefix` 266 - 267 - ## Contributing 268 - 269 - Contributions are welcome! Please ensure that: 270 - 271 - 1. All tests pass: `cargo test` 272 - 2. Code is properly formatted: `cargo fmt` 273 - 3. No linting issues: `cargo clippy` 274 - 4. New functionality includes appropriate tests 275 - 276 54 ## License 277 55 278 - This project is licensed under the MIT License. See the LICENSE file for details. 279 - 280 - ## Acknowledgments 281 - 282 - This library was extracted from the [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project, an open-source event and RSVP management and discovery application. 56 + MIT License
+6 -22
crates/atproto-identity/src/lib.rs
··· 1 - //! # atproto-identity 2 - //! 3 - //! A comprehensive Rust library for AT Protocol identity management providing DID resolution, 4 - //! handle resolution, and identity document management across multiple DID methods. 5 - //! 6 - //! ## Core Modules 7 - //! 8 - //! - [`resolve`] - Core resolution logic for handles and DIDs with DNS/HTTP resolution 9 - //! - [`plc`] - PLC directory client for `did:plc` resolution 10 - //! - [`web`] - Web DID client for `did:web` resolution and URL conversion 11 - //! - [`model`] - Data structures for DID documents and AT Protocol entities 12 - //! - [`validation`] - Input validation for handles and DIDs 13 - //! - [`config`] - Configuration management and environment variable handling 14 - //! - [`errors`] - Structured error types following project conventions 15 - //! - [`key`] - Cryptographic key operations for P-256 and K-256 curves 16 - //! - [`storage`] - DID document storage abstraction for CRUD operations 17 - //! - [`storage_lru`] - LRU-based implementation of DID document storage (requires `lru` feature) 18 - //! 19 - //! ## Usage 20 - //! 21 - //! This library supports both programmatic usage and CLI tooling for identity resolution 22 - //! across the AT Protocol ecosystem. 1 + //! AT Protocol identity management library for DID resolution, handle resolution, and cryptographic key operations. 23 2 //! 3 + //! Provides binaries: 4 + //! - `atproto-identity-resolve`: Resolve handles and DIDs to identity documents 5 + //! - `atproto-identity-key`: Generate and manage cryptographic keys 6 + //! - `atproto-identity-sign`: Sign data with private keys 7 + //! - `atproto-identity-validate`: Validate handles and DIDs 24 8 25 9 #![warn(missing_docs)] 26 10
+32 -248
crates/atproto-jetstream/README.md
··· 1 1 # atproto-jetstream 2 2 3 - A Rust library for consuming AT Protocol Jetstream events with high-performance WebSocket streaming, flexible event handling, and optional Zstandard compression support. 3 + WebSocket event stream consumer library for AT Protocol Jetstream. 4 4 5 5 ## Overview 6 6 7 7 `atproto-jetstream` provides a comprehensive async stream consumer for AT Protocol Jetstream events. This library enables real-time consumption of AT Protocol repository events, identity changes, and account updates through WebSocket connections with support for filtering, compression, and graceful shutdown patterns. 8 8 9 - This project was extracted from the open-sourced [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project and is designed to be a standalone, reusable library for AT Protocol event stream consumption. 9 + ## Binaries 10 + 11 + - **atproto-jetstream-consumer**: Real-time event stream consumer with filtering and compression support 10 12 11 13 ## Features 12 14 13 - - **High-Performance WebSocket Streaming**: Async WebSocket-based event consumption with automatic reconnection 14 - - **Flexible Event Handler System**: Register multiple custom event handlers with unique identifiers 15 - - **Zstandard Compression**: Optional compression support with custom dictionaries for bandwidth optimization 16 - - **Event Filtering**: Filter events by collections and DIDs for targeted consumption 17 - - **Graceful Shutdown**: Cancellation token support for clean shutdown and resource cleanup 18 - - **Message Size Management**: Configurable maximum message sizes and rate limiting 19 - - **Cursor Support**: Resume streaming from specific points using cursor positioning 20 - - **Structured Error Handling**: Comprehensive error types with detailed error codes following project conventions 21 - - **Built-in Event Broadcasting**: Event broadcasting to multiple consumers with `tokio::sync::broadcast` 22 - - **Tracing Integration**: Full structured logging support for debugging and monitoring 15 + - High-performance WebSocket streaming with automatic reconnection 16 + - Flexible event handler system with multiple custom handlers 17 + - Zstandard compression support with custom dictionaries 18 + - Event filtering by collections and DIDs 19 + - Graceful shutdown with cancellation token support 20 + - Structured error handling with detailed error codes 23 21 24 22 ## Usage 25 23 ··· 28 26 ```rust 29 27 use atproto_jetstream::{Consumer, ConsumerTaskConfig, EventHandler, JetstreamEvent, CancellationToken}; 30 28 use async_trait::async_trait; 31 - use anyhow::Result; 32 29 33 - // Create a custom event handler 34 30 struct MyEventHandler; 35 31 36 32 #[async_trait] 37 33 impl EventHandler for MyEventHandler { 38 - async fn handle_event(&self, event: JetstreamEvent) -> Result<()> { 34 + async fn handle_event(&self, event: JetstreamEvent) -> anyhow::Result<()> { 39 35 println!("Received event: {:?}", event); 40 36 Ok(()) 41 37 } ··· 45 41 } 46 42 } 47 43 48 - #[tokio::main] 49 - async fn main() -> Result<()> { 50 - let config = ConsumerTaskConfig { 51 - user_agent: "my-app/1.0".to_string(), 52 - compression: false, 53 - zstd_dictionary_location: String::new(), 54 - jetstream_hostname: "jetstream1.us-east.bsky.network".to_string(), 55 - collections: vec!["app.bsky.feed.post".to_string()], 56 - }; 44 + let config = ConsumerTaskConfig { 45 + user_agent: "my-app/1.0".to_string(), 46 + compression: false, 47 + zstd_dictionary_location: String::new(), 48 + jetstream_hostname: "jetstream1.us-east.bsky.network".to_string(), 49 + collections: vec!["app.bsky.feed.post".to_string()], 50 + }; 57 51 58 - let consumer = Consumer::new(config); 59 - let handler = std::sync::Arc::new(MyEventHandler); 60 - 61 - consumer.register_handler(handler).await?; 62 - 63 - let cancellation_token = CancellationToken::new(); 64 - consumer.run_background(cancellation_token).await?; 65 - 66 - Ok(()) 67 - } 68 - ``` 69 - 70 - ### Using Multiple Event Handlers 71 - 72 - ```rust 73 - use atproto_jetstream::{Consumer, LoggingHandler}; 74 - use std::sync::Arc; 75 - 76 - // Register multiple handlers 77 52 let consumer = Consumer::new(config); 53 + consumer.register_handler(std::sync::Arc::new(MyEventHandler)).await?; 78 54 79 - let logging_handler = Arc::new(LoggingHandler::new("logger".to_string())); 80 - let custom_handler = Arc::new(MyEventHandler); 81 - 82 - consumer.register_handler(logging_handler).await?; 83 - consumer.register_handler(custom_handler).await?; 55 + let cancellation_token = CancellationToken::new(); 56 + consumer.run_background(cancellation_token).await?; 84 57 ``` 85 58 86 - ### With Compression Support 59 + ### With Compression 87 60 88 61 ```rust 89 62 let config = ConsumerTaskConfig { 90 - user_agent: "my-app/1.0".to_string(), 91 63 compression: true, 92 64 zstd_dictionary_location: "./data/zstd_dictionary".to_string(), 93 - jetstream_hostname: "jetstream1.us-east.bsky.network".to_string(), 94 - collections: vec!["app.bsky.feed.post".to_string()], 65 + // ... other config 95 66 }; 96 67 97 - // Download the Zstandard dictionary first: 98 - // mkdir -p data/ 68 + // Download dictionary first: 99 69 // curl -o data/zstd_dictionary https://github.com/bluesky-social/jetstream/raw/refs/heads/main/pkg/models/zstd_dictionary 100 70 ``` 101 71 102 - ## Installation 103 - 104 - Add this to your `Cargo.toml`: 105 - 106 - ```toml 107 - [dependencies] 108 - atproto-jetstream = "0.1.0" 109 - ``` 110 - 111 - ## Command Line Tools 112 - 113 - The crate includes a command-line tool for consuming AT Protocol Jetstream events: 114 - 115 - ### `atproto-jetstream-consumer` 116 - 117 - A comprehensive command-line tool for consuming AT Protocol Jetstream events with real-time streaming, filtering capabilities, and optional compression support. This tool provides an easy way to monitor AT Protocol event streams for development, testing, and production monitoring. 118 - 119 - **Features:** 120 - - **Real-Time Event Streaming**: Connects to AT Protocol Jetstream instances for live event consumption 121 - - **Event Filtering**: Filter events by specific collections and DIDs for targeted monitoring 122 - - **Compression Support**: Optional Zstandard compression with dictionary support for bandwidth optimization 123 - - **Flexible Output**: Structured JSON output for each event with customizable logging levels 124 - - **Connection Management**: Automatic reconnection handling and graceful shutdown on interruption 125 - - **Configurable Parameters**: Extensive configuration options for hostname, collections, message sizes, and more 72 + ## Command Line Examples 126 73 127 74 ```bash 128 - # Basic event streaming from Jetstream 75 + # Basic streaming 129 76 cargo run --bin atproto-jetstream-consumer \ 130 77 --hostname jetstream1.us-east.bsky.network \ 131 78 --collections app.bsky.feed.post \ 132 79 --user-agent "my-consumer/1.0" 133 80 134 - # Stream specific collections with filtering 135 - cargo run --bin atproto-jetstream-consumer \ 136 - --hostname jetstream1.us-east.bsky.network \ 137 - --collections "app.bsky.feed.post,app.bsky.actor.profile" \ 138 - --dids "did:plc:user123,did:plc:user456" \ 139 - --user-agent "filtered-consumer/1.0" 140 - 141 - # With Zstandard compression enabled 81 + # With compression 142 82 cargo run --bin atproto-jetstream-consumer \ 143 83 --hostname jetstream1.us-east.bsky.network \ 144 84 --collections app.bsky.feed.post \ 145 85 --compression \ 146 - --zstd-dictionary ./data/zstd_dictionary \ 147 - --user-agent "compressed-consumer/1.0" 148 - 149 - # Advanced configuration with message size limits and cursor 150 - cargo run --bin atproto-jetstream-consumer \ 151 - --hostname jetstream1.us-east.bsky.network \ 152 - --collections app.bsky.feed.post \ 153 - --max-message-size 1048576 \ 154 - --cursor 1234567890 \ 155 - --require-hello \ 156 - --user-agent "advanced-consumer/1.0" 157 - ``` 158 - 159 - **Command Line Arguments:** 160 - - `--hostname` - Jetstream hostname to connect to (e.g., jetstream1.us-east.bsky.network) 161 - - `--collections` - Comma-separated list of AT Protocol collections to subscribe to 162 - - `--dids` - Optional comma-separated list of DIDs to filter events for 163 - - `--user-agent` - User-Agent string for the WebSocket connection 164 - - `--compression` - Enable Zstandard compression (requires zstd-dictionary) 165 - - `--zstd-dictionary` - Path to Zstandard dictionary file for compression 166 - - `--max-message-size` - Maximum message size in bytes (default: 56000) 167 - - `--cursor` - Optional cursor position to start streaming from 168 - - `--require-hello` - Require hello message before receiving events (default: true) 169 - 170 - **Setting up Compression:** 171 - To use compression, you need to download the Zstandard dictionary: 86 + --zstd-dictionary ./data/zstd_dictionary 172 87 173 - ```bash 174 - # Create data directory and download dictionary 175 - mkdir -p data/ 176 - curl -o data/zstd_dictionary \ 177 - https://github.com/bluesky-social/jetstream/raw/refs/heads/main/pkg/models/zstd_dictionary 178 - 179 - # Use with compression enabled 88 + # With filtering 180 89 cargo run --bin atproto-jetstream-consumer \ 181 90 --hostname jetstream1.us-east.bsky.network \ 182 - --collections app.bsky.feed.post \ 183 - --compression \ 184 - --zstd-dictionary ./data/zstd_dictionary 185 - ``` 186 - 187 - **Output Format:** 188 - The tool outputs structured JSON for each received event: 189 - 190 - ```json 191 - { 192 - "kind": "commit", 193 - "time_us": 1704067200000000, 194 - "did": "did:plc:user123", 195 - "commit": { 196 - "rev": "3l2uygzaf5c2b", 197 - "operation": "create", 198 - "collection": "app.bsky.feed.post", 199 - "rkey": "3l2uygzaf5c2c", 200 - "cid": "bafyreif5n4jf6jfczjqzckzqxdxm5qnz4jf6jfczjqzckzqxdxm5qnz4", 201 - "record": { 202 - "$type": "app.bsky.feed.post", 203 - "text": "Hello AT Protocol!", 204 - "createdAt": "2024-01-01T00:00:00Z" 205 - } 206 - } 207 - } 208 - ``` 209 - 210 - This tool is ideal for: 211 - - **Development and Testing**: Monitor AT Protocol events during application development 212 - - **Production Monitoring**: Track repository changes and user activity in real-time 213 - - **Data Analysis**: Collect AT Protocol events for analysis and research 214 - - **Integration Testing**: Verify that your applications are generating expected events 215 - - **System Monitoring**: Monitor the health and activity of AT Protocol networks 216 - 217 - ## Event Types 218 - 219 - The library handles `JetstreamEvent` structures with the following fields: 220 - 221 - - `kind`: Event type (e.g., "commit", "identity", "account") 222 - - `time_us`: Event timestamp in microseconds 223 - - `commit`: Optional commit data for repository events 224 - - `identity`: Optional identity change data 225 - - `account`: Optional account-related data 226 - 227 - ## Modules 228 - 229 - - **[`consumer`]** - Core consumer implementation with WebSocket streaming and event handling 230 - - **[`lib`]** - Public library interface and re-exports 231 - 232 - ## Error Handling 233 - 234 - The crate uses comprehensive structured error types with unique identifiers: 235 - 236 - ``` 237 - error-atproto-jetstream-<domain>-<number> <message>: <details> 238 - ``` 239 - 240 - All errors follow the project convention: 241 - 242 - - `ConsumerError::ConnectionFailed` - WebSocket connection establishment failures 243 - - `ConsumerError::DecompressionFailed` - Zstandard decompression operation failures 244 - - `ConsumerError::DeserializationFailed` - JSON event parsing failures 245 - - `ConsumerError::HandlerRegistrationFailed` - Event handler registration conflicts 246 - - `ConsumerError::EventSenderNotInitialized` - Event broadcasting setup errors 247 - - `ConsumerError::MessageConversionFailed` - WebSocket message format errors 248 - - `ConsumerError::UpdateSerializationFailed` - Subscription update serialization errors 249 - - `ConsumerError::UpdateSendFailed` - Subscription update transmission errors 250 - - `ConsumerError::DecompressorCreationFailed` - Zstandard decompressor initialization errors 251 - 252 - ```rust 253 - use atproto_jetstream::consumer::ConsumerError; 254 - 255 - // Example error handling 256 - match consumer_result { 257 - Err(ConsumerError::ConnectionFailed(details)) => { 258 - println!("Failed to connect to Jetstream: {}", details); 259 - } 260 - Err(ConsumerError::DecompressionFailed(error)) => { 261 - println!("Decompression failed: {}", error); 262 - } 263 - Err(ConsumerError::HandlerRegistrationFailed(error)) => { 264 - println!("Handler registration failed: {}", error); 265 - } 266 - Ok(()) => println!("Consumer operation successful"), 267 - } 91 + --collections "app.bsky.feed.post,app.bsky.actor.profile" \ 92 + --dids "did:plc:user123,did:plc:user456" 268 93 ``` 269 94 270 - ## Dependencies 271 - 272 - This crate builds on: 273 - 274 - - `tokio` - Async runtime for WebSocket connections and event handling 275 - - `tokio-websockets` - WebSocket client implementation for Jetstream connections 276 - - `tokio-util` - Additional utilities including cancellation token support 277 - - `futures` - Stream and sink traits for async WebSocket operations 278 - - `zstd` - Zstandard compression support with dictionary-based decompression 279 - - `serde_json` - JSON serialization and deserialization for AT Protocol events 280 - - `http` - HTTP types for WebSocket headers and URI parsing 281 - - `urlencoding` - URL encoding for query parameters in WebSocket connections 282 - - `async_trait` - Async trait support for event handler implementations 283 - - `anyhow` - Error handling utilities and result types 284 - - `tracing` - Structured logging for debugging and monitoring 285 - - `thiserror` - Structured error type derivation 286 - 287 - ## AT Protocol Jetstream 288 - 289 - This library implements a client for [AT Protocol Jetstream](https://github.com/bluesky-social/jetstream), which provides: 290 - 291 - - **Real-Time Event Streaming**: Live consumption of AT Protocol repository events 292 - - **Efficient Compression**: Zstandard compression with custom dictionaries for bandwidth optimization 293 - - **Event Filtering**: Server-side filtering by collections and DIDs for targeted consumption 294 - - **High Performance**: WebSocket-based streaming designed for high-throughput event processing 295 - - **Reliability**: Built-in connection management and error recovery patterns 296 - 297 - ## Contributing 298 - 299 - Contributions are welcome! Please ensure that: 300 - 301 - 1. All tests pass: `cargo test` 302 - 2. Code is properly formatted: `cargo fmt` 303 - 3. No linting issues: `cargo clippy` 304 - 4. New functionality includes appropriate tests and documentation 305 - 5. Error handling follows the project's structured error format 306 - 307 95 ## License 308 96 309 - This project is licensed under the MIT License. See the LICENSE file for details. 310 - 311 - ## Acknowledgments 312 - 313 - This library was extracted from the [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project, an open-source event and RSVP management and discovery application. 97 + MIT License
+3 -11
crates/atproto-jetstream/src/lib.rs
··· 1 - //! AT Protocol Jetstream event consumer library. 2 - //! 3 - //! Provides async stream consumption and event handling for AT Protocol Jetstream 4 - //! with support for WebSocket connections, event dispatching, and handler registration. 5 - //! 6 - //! ## Key Features 1 + //! WebSocket-based event stream consumer for AT Protocol Jetstream with Zstandard compression. 7 2 //! 8 - //! - **Async Stream Consumer**: High-performance WebSocket-based event consumption 9 - //! - **Event Handler Registration**: Flexible event handler system with multiple handlers 10 - //! - **Compression Support**: Optional Zstandard compression with dictionary support 11 - //! - **Graceful Shutdown**: Cancellation token support for clean shutdown 12 - //! - **Error Handling**: Comprehensive error types following project conventions 3 + //! Provides binary: 4 + //! - `atproto-jetstream-consumer`: Consume and log Jetstream events from AT Protocol relays 13 5 14 6 #![warn(missing_docs)] 15 7
+33 -306
crates/atproto-oauth-axum/README.md
··· 1 1 # atproto-oauth-axum 2 2 3 - A Rust library providing complete Axum web handlers for AT Protocol OAuth 2.0 authorization server endpoints, including client metadata, JWKS, authorization callback handling, and a comprehensive command-line OAuth login tool. 3 + Axum web handlers for AT Protocol OAuth 2.0 authorization server endpoints. 4 4 5 5 ## Overview 6 6 7 - `atproto-oauth-axum` provides ready-to-use Axum web handlers that implement the complete AT Protocol OAuth 2.0 authorization server specification. This library handles OAuth client metadata discovery, JSON Web Key Set (JWKS) endpoints, authorization callback processing, and includes a full-featured command-line tool for OAuth login flows. 7 + `atproto-oauth-axum` provides ready-to-use Axum web handlers that implement the complete AT Protocol OAuth 2.0 authorization server specification. This library handles OAuth client metadata discovery, JWKS endpoints, authorization callback processing, and includes a command-line OAuth login tool. 8 + 9 + ## Binaries 8 10 9 - This project was extracted from the open-sourced [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project and is designed to be a standalone, reusable library for AT Protocol OAuth server implementations. 11 + - **atproto-oauth-tool**: Complete OAuth login CLI tool for AT Protocol services 10 12 11 13 ## Features 12 14 13 - - **Complete OAuth Server Handlers**: Ready-to-use Axum handlers for all required OAuth 2.0 endpoints 14 - - **Client Metadata Endpoint**: RFC 7591 compliant client metadata for dynamic client registration 15 - - **JWKS Endpoint**: JSON Web Key Set serving for JWT signature verification 16 - - **Authorization Callback Handler**: Complete OAuth callback processing with token exchange 17 - - **OAuth Login CLI Tool**: Full-featured command-line tool for testing and development OAuth flows 18 - - **Axum Integration**: Native Axum state management and request extractors 19 - - **Error Handling**: Comprehensive structured error types with proper HTTP responses 20 - - **AT Protocol Compliance**: Implements all AT Protocol-specific OAuth requirements 21 - 22 - ## Installation 23 - 24 - Add this to your `Cargo.toml`: 25 - 26 - ```toml 27 - [dependencies] 28 - atproto-oauth-axum = "0.6.0" 29 - ``` 15 + - Complete OAuth server handlers for Axum web applications 16 + - Client metadata endpoint with RFC 7591 compliance 17 + - JWKS endpoint for JSON Web Key Set serving 18 + - Authorization callback handler with token exchange 19 + - Native Axum state management and request extractors 20 + - AT Protocol compliance validation 30 21 31 22 ## Usage 32 23 33 - ### Basic Axum Server Setup 24 + ### Basic Server Setup 34 25 35 26 ```rust 36 27 use atproto_oauth_axum::{ ··· 40 31 state::OAuthClientConfig, 41 32 }; 42 33 use axum::{routing::get, Router}; 43 - use atproto_identity::key::identify_key; 44 34 45 - #[tokio::main] 46 - async fn main() -> anyhow::Result<()> { 47 - // Set up OAuth client configuration 48 - let oauth_config = OAuthClientConfig { 49 - client_uri: "https://your-app.com".to_string(), 50 - client_id: "https://your-app.com/oauth/client-metadata.json".to_string(), 51 - redirect_uris: "https://your-app.com/oauth/callback".to_string(), 52 - jwks_uri: "https://your-app.com/.well-known/jwks.json".to_string(), 53 - signing_keys: vec![ 54 - identify_key("did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA")? 55 - ], 56 - }; 57 - 58 - // Create Axum router with OAuth handlers 59 - let app = Router::new() 60 - .route("/oauth/client-metadata.json", get(handle_oauth_metadata)) 61 - .route("/.well-known/jwks.json", get(handle_oauth_jwks)) 62 - .route("/oauth/callback", get(handle_oauth_callback)) 63 - .with_state(oauth_config); 64 - 65 - // Start the server 66 - let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?; 67 - axum::serve(listener, app).await?; 35 + let oauth_config = OAuthClientConfig { 36 + client_uri: "https://your-app.com".to_string(), 37 + client_id: "https://your-app.com/oauth/client-metadata.json".to_string(), 38 + redirect_uris: "https://your-app.com/oauth/callback".to_string(), 39 + jwks_uri: "https://your-app.com/.well-known/jwks.json".to_string(), 40 + signing_keys: vec![identify_key("did:key:zQ3sh...")?], 41 + }; 68 42 69 - Ok(()) 70 - } 71 - ``` 72 - 73 - ### OAuth Client Metadata Handler 74 - 75 - ```rust 76 - use atproto_oauth_axum::{handler_metadata::handle_oauth_metadata, state::OAuthClientConfig}; 77 - use axum::{routing::get, Router}; 78 - 79 - // The metadata handler automatically generates RFC 7591 compliant client metadata 80 43 let app = Router::new() 81 44 .route("/oauth/client-metadata.json", get(handle_oauth_metadata)) 82 - .with_state(oauth_config); 83 - 84 - // Returns JSON like: 85 - // { 86 - // "client_id": "https://your-app.com/oauth/client-metadata.json", 87 - // "client_uri": "https://your-app.com", 88 - // "dpop_bound_access_tokens": true, 89 - // "application_type": "web", 90 - // "redirect_uris": ["https://your-app.com/oauth/callback"], 91 - // "grant_types": ["authorization_code", "refresh_token"], 92 - // "response_types": ["code"], 93 - // "scope": "atproto transition:generic", 94 - // "token_endpoint_auth_method": "private_key_jwt", 95 - // "jwks_uri": "https://your-app.com/.well-known/jwks.json" 96 - // } 97 - ``` 98 - 99 - ### JWKS Endpoint Handler 100 - 101 - ```rust 102 - use atproto_oauth_axum::{handle_jwks::handle_oauth_jwks, state::OAuthClientConfig}; 103 - use axum::{routing::get, Router}; 104 - 105 - // The JWKS handler automatically converts your signing keys to JWK format 106 - let app = Router::new() 107 45 .route("/.well-known/jwks.json", get(handle_oauth_jwks)) 46 + .route("/oauth/callback", get(handle_oauth_callback)) 108 47 .with_state(oauth_config); 109 - 110 - // Returns JSON Web Key Set with your public keys for signature verification 111 48 ``` 112 49 113 - ### OAuth Callback Handler 50 + ### OAuth Handlers 114 51 115 - ```rust 116 - use atproto_oauth_axum::{ 117 - handle_complete::handle_oauth_callback, 118 - state::{OAuthClientConfig, HttpClient}, 119 - }; 120 - use atproto_identity::axum::state::{DidDocumentStorageExtractor, KeyProviderExtractor}; 121 - use atproto_oauth::axum::state::OAuthRequestStorageExtractor; 122 - use axum::{routing::get, Router}; 52 + The library provides ready-to-use handlers for: 123 53 124 - // The callback handler processes OAuth authorization callbacks 125 - // It automatically: 126 - // - Validates OAuth state parameters 127 - // - Exchanges authorization codes for tokens 128 - // - Validates DPoP proofs 129 - // - Returns complete OAuth response with tokens 54 + - **Client Metadata**: Generates RFC 7591 compliant metadata 55 + - **JWKS Endpoint**: Serves JSON Web Key Sets for signature verification 56 + - **Callback Processing**: Handles OAuth authorization callbacks with token exchange 130 57 131 - let app = Router::new() 132 - .route("/oauth/callback", get(handle_oauth_callback)) 133 - .with_state(web_context); // Includes all required state 134 - ``` 135 - 136 - ### Integration with Other Libraries 137 - 138 - ```rust 139 - use atproto_oauth_axum::state::{OAuthClientConfig, HttpClient}; 140 - use atproto_identity::{ 141 - axum::state::{DidDocumentStorageExtractor, KeyProviderExtractor}, 142 - storage_lru::LruDidDocumentStorage, 143 - key::KeyProvider, 144 - }; 145 - use atproto_oauth::{ 146 - axum::state::OAuthRequestStorageExtractor, 147 - storage_lru::LruOAuthRequestStorage, 148 - }; 149 - use std::{num::NonZeroUsize, sync::Arc}; 150 - 151 - // Set up storage and state for full OAuth server 152 - let did_storage = Arc::new(LruDidDocumentStorage::new(NonZeroUsize::new(256).unwrap())); 153 - let oauth_storage = Arc::new(LruOAuthRequestStorage::new(NonZeroUsize::new(256).unwrap())); 154 - let key_provider = Arc::new(your_key_provider_impl); 155 - 156 - // The handlers automatically extract these from your application state 157 - ``` 158 - 159 - ## Command Line Tools 160 - 161 - The crate includes a comprehensive command-line tool for OAuth operations: 162 - 163 - ### `atproto-oauth-tool` 164 - 165 - A complete OAuth login CLI tool that implements the full AT Protocol OAuth client flow. This tool sets up a local web server to handle OAuth callbacks and guides users through the complete authorization process from subject resolution to token acquisition. 166 - 167 - **Features:** 168 - - **Subject Resolution**: Automatically resolves AT Protocol handles or DIDs to their identity documents 169 - - **DID Document Retrieval**: Fetches and validates DID documents from PLC directory or Web DID endpoints 170 - - **PDS Discovery**: Discovers Personal Data Server (PDS) endpoints from DID documents 171 - - **Authorization Server Discovery**: Retrieves OAuth authorization server metadata from PDS resources 172 - - **PKCE Implementation**: Generates secure PKCE parameters for authorization code flows 173 - - **DPoP Key Generation**: Creates DPoP keys for bound access tokens 174 - - **Local OAuth Server**: Runs temporary web server to handle authorization callbacks 175 - - **Complete Token Exchange**: Handles full OAuth flow from authorization to token acquisition 58 + ## Command Line Examples 176 59 177 60 ```bash 178 61 # Start OAuth login flow for a handle ··· 180 63 181 64 # Start OAuth login flow for a DID 182 65 cargo run --bin atproto-oauth-tool login did:key:zQ3sh... did:plc:user123 183 - 184 - # The tool will: 185 - # 1. Resolve the subject to a DID 186 - # 2. Fetch the DID document 187 - # 3. Discover the PDS endpoint 188 - # 4. Get OAuth authorization server configuration 189 - # 5. Generate PKCE and DPoP parameters 190 - # 6. Start a local server on http://localhost:8080 191 - # 7. Display the authorization URL to visit 192 - # 8. Handle the OAuth callback 193 - # 9. Exchange authorization code for tokens 194 - # 10. Display the complete OAuth response including access tokens and DPoP key 195 - 196 - # Example output: 197 - # OAuth server started on http://0.0.0.0:8080 198 - # 🔐 OAuth Authorization URL: 199 - # https://auth.bsky.social/oauth/authorize?client_id=https://localhost:8080/oauth/client-metadata.json&request_uri=urn:ietf:params:oauth:request_uri:abc123 200 - # 201 - # Please visit this URL in your browser to complete the OAuth flow. 202 - # The callback will be handled at: https://localhost:8080/oauth/callback 203 66 ``` 204 67 205 - **Server Endpoints:** 206 - The tool automatically sets up these endpoints during the OAuth flow: 207 - - `GET /oauth/client-metadata.json` - OAuth client metadata 208 - - `GET /.well-known/jwks.json` - JSON Web Key Set 209 - - `GET /oauth/callback` - Authorization callback handler 210 - 211 - **Environment Variables:** 212 - ```bash 213 - # Required: Your application's external base URL 214 - export EXTERNAL_BASE=your-app.com 215 - 216 - # Optional: Custom PLC directory 217 - export PLC_HOSTNAME=plc.directory 218 - 219 - # Optional: Custom DNS nameservers 220 - export DNS_NAMESERVERS=8.8.8.8;1.1.1.1 221 - 222 - # Optional: Custom CA certificates 223 - export CERTIFICATE_BUNDLES=/path/to/cert.pem 224 - 225 - # Optional: Custom User-Agent 226 - export USER_AGENT="my-oauth-client/1.0" 227 - ``` 228 - 229 - **Security Features:** 230 - - Cryptographically secure PKCE code verifier generation 231 - - DPoP proof-of-possession for bound access tokens 232 - - State parameter validation for CSRF protection 233 - - Automatic nonce handling for DPoP challenges 234 - - Private key security with no key storage 235 - 236 - ## Modules 237 - 238 - - **[`handle_complete`]** - OAuth authorization callback handler with token exchange 239 - - **[`handler_metadata`]** - OAuth client metadata endpoint (RFC 7591) 240 - - **[`handle_jwks`]** - JSON Web Key Set endpoint for signature verification 241 - - **[`handle_init`]** - OAuth authorization initiation (reserved for future use) 242 - - **[`state`]** - Axum state management and request extractors 243 - - **[`errors`]** - Structured error types for OAuth operations 244 - 245 - ## Error Handling 246 - 247 - The crate uses comprehensive structured error types: 248 - 249 - ```rust 250 - use atproto_oauth_axum::errors::{OAuthCallbackError, OAuthLoginError}; 251 - 252 - // OAuth callback handler errors 253 - match callback_result { 254 - Err(OAuthCallbackError::NoOAuthRequestFound) => { 255 - println!("OAuth state not found - possible CSRF attack"); 256 - }, 257 - Err(OAuthCallbackError::InvalidIssuer { expected, actual }) => { 258 - println!("Issuer mismatch: expected {}, got {}", expected, actual); 259 - }, 260 - Err(OAuthCallbackError::NoDIDDocumentFound) => { 261 - println!("DID document not found for OAuth request"); 262 - }, 263 - Ok(response) => println!("OAuth callback successful"), 264 - } 265 - 266 - // OAuth login CLI errors 267 - match login_result { 268 - Err(OAuthLoginError::SubjectResolutionFailed { error }) => { 269 - println!("Failed to resolve subject: {}", error); 270 - }, 271 - Err(OAuthLoginError::NoPDSEndpointFound) => { 272 - println!("No PDS endpoint found in DID document"); 273 - }, 274 - Err(OAuthLoginError::OAuthInitFailed { error }) => { 275 - println!("OAuth initialization failed: {}", error); 276 - }, 277 - Ok(()) => println!("OAuth login completed successfully"), 278 - } 279 - ``` 280 - 281 - ### Error Format 282 - 283 - All errors follow the standardized format: 284 - 285 - ``` 286 - error-atproto-oauth-axum-<domain>-<number> <message>: <details> 287 - ``` 288 - 289 - Example error codes: 290 - - `error-atproto-oauth-axum-callback-1` through `error-atproto-oauth-axum-callback-7` - OAuth callback errors 291 - - `error-atproto-oauth-axum-login-1` through `error-atproto-oauth-axum-login-11` - OAuth login CLI errors 292 - 293 - ## AT Protocol Compliance 294 - 295 - This library implements all AT Protocol OAuth requirements: 296 - 297 - ### OAuth Server Requirements 298 - - Support for `authorization_code` and `refresh_token` grant types 299 - - PKCE with `S256` code challenge method 300 - - DPoP bound access tokens 301 - - `private_key_jwt` token endpoint authentication 302 - - `ES256` signing algorithm support 303 - - Required OAuth scopes (`atproto`, `transition:generic`) 304 - 305 - ### Client Requirements 306 - - Dynamic client registration metadata 307 - - JWKS endpoint for public key discovery 308 - - Proper redirect URI validation 309 - - State parameter CSRF protection 310 - 311 - ## Dependencies 312 - 313 - This crate builds on: 314 - 315 - - [`atproto-identity`](../atproto-identity) - Identity resolution and cryptographic operations 316 - - [`atproto-oauth`](../atproto-oauth) - Core OAuth 2.0 operations and security extensions 317 - - `axum` - Web framework for HTTP handlers 318 - - `reqwest` - HTTP client for OAuth server communication 319 - - `tokio` - Async runtime for web server operations 320 - - `serde_json` - JSON serialization for OAuth responses 321 - - `chrono` - Date and time handling for OAuth flows 322 - - `anyhow` - Error handling utilities 323 - - `thiserror` - Structured error type derivation 324 - 325 - ## Development and Testing 326 - 327 - This crate is ideal for: 328 - 329 - - **OAuth Server Development**: Complete Axum handlers for AT Protocol OAuth servers 330 - - **OAuth Client Testing**: CLI tool for testing OAuth flows against AT Protocol services 331 - - **Integration Testing**: Ready-to-use handlers for OAuth endpoint testing 332 - - **Development Workflows**: Local OAuth server for development and debugging 333 - 334 - ## Contributing 335 - 336 - Contributions are welcome! Please ensure that: 337 - 338 - 1. All tests pass: `cargo test` 339 - 2. Code is properly formatted: `cargo fmt` 340 - 3. No linting issues: `cargo clippy` 341 - 4. New functionality includes appropriate tests and documentation 342 - 5. Error handling follows the project's structured error format 68 + The tool provides a complete OAuth client implementation with: 69 + - Subject resolution and DID document retrieval 70 + - PDS and authorization server discovery 71 + - PKCE and DPoP parameter generation 72 + - Local web server for callback handling 73 + - Complete token exchange flow 343 74 344 75 ## License 345 76 346 - This project is licensed under the MIT License. See the LICENSE file for details. 347 - 348 - ## Acknowledgments 349 - 350 - This library was extracted from the [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project, an open-source event and RSVP management and discovery application. 77 + MIT License
+3 -61
crates/atproto-oauth-axum/src/lib.rs
··· 1 - //! Axum web framework integration for AT Protocol OAuth workflows. 2 - //! 3 - //! This crate provides complete Axum web handlers and request extractors for implementing 4 - //! AT Protocol OAuth 2.0 client endpoints. It includes RFC-compliant client metadata serving, 5 - //! JWKS public key distribution, and OAuth authorization callback handling. 6 - //! 7 - //! ## Features 8 - //! 9 - //! - **OAuth Client Metadata**: RFC 7591 compliant client metadata endpoint 10 - //! - **JWKS Endpoint**: JSON Web Key Set serving for signature verification 11 - //! - **Authorization Callbacks**: Complete OAuth authorization code flow handling 12 - //! - **DPoP Support**: Demonstration of Proof-of-Possession token binding 13 - //! - **Request Extractors**: Axum state management for OAuth configuration 14 - //! - **Error Handling**: Structured error types for OAuth workflows 15 - //! 16 - //! ## OAuth Endpoints 17 - //! 18 - //! The crate provides handlers for standard OAuth 2.0 endpoints: 19 - //! 20 - //! - `/oauth/client-metadata.json` - OAuth client metadata (RFC 7591) 21 - //! - `/.well-known/jwks.json` - JSON Web Key Set for public keys 22 - //! - `/oauth/callback` - OAuth authorization callback handler 23 - //! 24 - //! ## CLI Tool 25 - //! 26 - //! The `atproto-oauth-tool` binary provides a complete OAuth client implementation: 27 - //! 28 - //! ```bash 29 - //! # Start OAuth login flow 30 - //! atproto-oauth-tool login <private_signing_key> <subject> 31 - //! 32 - //! # Refresh OAuth tokens 33 - //! atproto-oauth-tool refresh <private_signing_key> <subject> <private_dpop_key> <refresh_token> 34 - //! ``` 35 - //! 36 - //! ## Example Integration 37 - //! 38 - //! ```rust,no_run 39 - //! use axum::{routing::get, Router}; 40 - //! use atproto_oauth_axum::{ 41 - //! handle_jwks::handle_oauth_jwks, 42 - //! handler_metadata::handle_oauth_metadata, 43 - //! state::OAuthClientConfig, 44 - //! }; 45 - //! 46 - //! # async fn example() -> Result<(), Box<dyn std::error::Error>> { 47 - //! let router = Router::new() 48 - //! .route("/oauth/client-metadata.json", get(handle_oauth_metadata)) 49 - //! .route("/.well-known/jwks.json", get(handle_oauth_jwks)); 50 - //! // Note: handle_oauth_callback requires additional state extractors 51 - //! // .route("/oauth/callback", get(handle_oauth_callback)) 52 - //! // .with_state(oauth_config); 53 - //! # Ok(()) 54 - //! # } 55 - //! ``` 56 - //! 57 - //! ## Dependencies 1 + //! Axum web handlers for AT Protocol OAuth 2.0 client endpoints with DPoP support. 58 2 //! 59 - //! This crate integrates with: 60 - //! - [`atproto-oauth`]: Core OAuth workflow logic and PKCE implementation 61 - //! - [`atproto-identity`]: AT Protocol identity resolution and key management 62 - //! - [`axum`]: Web framework for HTTP request handling 3 + //! Provides binary: 4 + //! - `atproto-oauth-tool`: OAuth client for login and token refresh operations 63 5 64 6 #![warn(missing_docs)] 65 7
+40 -288
crates/atproto-oauth/README.md
··· 1 1 # atproto-oauth 2 2 3 - A Rust library for AT Protocol OAuth 2.0 operations, providing comprehensive support for OAuth security extensions, JWT operations, and AT Protocol-specific OAuth flows. 3 + OAuth 2.0 implementation library for AT Protocol with security extensions. 4 4 5 5 ## Overview 6 6 7 - `atproto-oauth` provides OAuth 2.0 functionality specifically designed for the AT Protocol ecosystem. This library implements the security extensions and cryptographic operations required for secure OAuth flows in AT Protocol applications, including support for DPoP (Demonstration of Proof-of-Possession), PKCE (Proof Key for Code Exchange), and AT Protocol-specific OAuth resource validation. 8 - 9 - This project was extracted from the open-sourced [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project and is designed to be a standalone, reusable library for AT Protocol OAuth operations. 7 + `atproto-oauth` provides OAuth 2.0 functionality specifically designed for the AT Protocol ecosystem. This library implements DPoP (Demonstration of Proof-of-Possession), PKCE (Proof Key for Code Exchange), JWT operations, and AT Protocol-specific OAuth validation. 10 8 11 9 ## Features 12 10 13 - - **JWT Operations**: Complete JSON Web Token minting, verification, and validation with ES256/ES256K support 14 - - **JWK Management**: JSON Web Key generation, conversion, and management for P-256 and K-256 curves 15 - - **PKCE Implementation**: RFC 7636 Proof Key for Code Exchange for secure OAuth flows 16 - - **DPoP Support**: RFC 9449 Demonstration of Proof-of-Possession with automatic retry middleware 17 - - **OAuth Resource Discovery**: RFC 8414 OAuth 2.0 authorization server and protected resource metadata discovery 18 - - **AT Protocol Validation**: Comprehensive validation of OAuth servers against AT Protocol requirements 19 - - **Structured Error Handling**: Type-safe error handling with detailed error codes and messages 20 - - **Cryptographic Integration**: Seamless integration with `atproto-identity` for key operations 21 - 22 - ## Supported OAuth Extensions 23 - 24 - - **PKCE (RFC 7636)**: Proof Key for Code Exchange with S256 code challenge method 25 - - **DPoP (RFC 9449)**: Demonstration of Proof-of-Possession with ES256 signing algorithm 26 - - **OAuth 2.0 Resource Discovery (RFC 8414)**: Authorization server and protected resource metadata 27 - - **JWT Bearer Tokens (RFC 7523)**: Private key JWT authentication for token endpoints 28 - 29 - ## Installation 30 - 31 - Add this to your `Cargo.toml`: 32 - 33 - ```toml 34 - [dependencies] 35 - atproto-oauth = "0.6.0" 36 - ``` 11 + - JWT minting, verification, and validation with ES256/ES256K support 12 + - JWK generation and management for P-256 and K-256 curves 13 + - PKCE implementation for secure OAuth flows 14 + - DPoP support with automatic retry middleware 15 + - OAuth resource discovery and AT Protocol validation 16 + - Structured error handling with detailed error codes 37 17 38 18 ## Usage 39 19 ··· 43 23 use atproto_oauth::jwt::{mint, verify, Header, Claims, JoseClaims}; 44 24 use atproto_identity::key::identify_key; 45 25 46 - #[tokio::main] 47 - async fn main() -> anyhow::Result<()> { 48 - // Parse a private key 49 - let key_data = identify_key("did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA")?; 50 - 51 - // Create JWT header 52 - let header = Header { 53 - algorithm: Some("ES256".to_string()), 54 - type_: Some("JWT".to_string()), 55 - ..Default::default() 56 - }; 57 - 58 - // Create JWT claims 59 - let claims = Claims::new(JoseClaims { 60 - issuer: Some("did:plc:issuer123".to_string()), 61 - subject: Some("did:plc:subject456".to_string()), 62 - audience: Some("https://pds.example.com".to_string()), 63 - expiration: Some(chrono::Utc::now().timestamp() as u64 + 3600), 64 - ..Default::default() 65 - }); 66 - 67 - // Mint JWT 68 - let token = mint(&key_data, &header, &claims)?; 69 - println!("JWT: {}", token); 70 - 71 - // Verify JWT 72 - verify(&key_data, &token).await?; 73 - println!("JWT verified successfully!"); 74 - 75 - Ok(()) 76 - } 77 - ``` 78 - 79 - ### PKCE Implementation 80 - 81 - ```rust 82 - use atproto_oauth::pkce; 83 - 84 - fn main() { 85 - // Generate PKCE parameters for authorization flow 86 - let (code_verifier, code_challenge) = pkce::generate(); 87 - 88 - println!("Code Challenge: {}", code_challenge); 89 - println!("Code Verifier: {}", code_verifier); 90 - 91 - // Use code_challenge in authorization URL 92 - let auth_url = format!( 93 - "https://auth.example.com/oauth/authorize?code_challenge={}&code_challenge_method=S256", 94 - code_challenge 95 - ); 96 - 97 - // Later, use code_verifier when exchanging authorization code for tokens 98 - // (This would be in your token exchange request) 99 - } 100 - ``` 101 - 102 - ### DPoP Proof Generation 103 - 104 - ```rust 105 - use atproto_oauth::dpop::{auth_dpop, request_dpop}; 106 - use atproto_identity::key::identify_key; 107 - 108 - #[tokio::main] 109 - async fn main() -> anyhow::Result<()> { 110 - let key_data = identify_key("did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA")?; 111 - 112 - // Create DPoP proof for authorization request 113 - let (dpop_token, header, claims) = auth_dpop( 114 - &key_data, 115 - "POST", 116 - "https://auth.example.com/oauth/token" 117 - )?; 118 - 119 - println!("DPoP Authorization Proof: {}", dpop_token); 120 - 121 - // Create DPoP proof for resource request 122 - let (resource_token, _, _) = request_dpop( 123 - &key_data, 124 - "GET", 125 - "https://pds.example.com/api/resource", 126 - "did:plc:issuer123", 127 - "access_token_here" 128 - )?; 129 - 130 - println!("DPoP Resource Proof: {}", resource_token); 131 - 132 - Ok(()) 133 - } 134 - ``` 26 + let key_data = identify_key("did:key:zQ3sh...")?; 135 27 136 - ### OAuth Resource Discovery and Validation 28 + let header = Header { 29 + algorithm: Some("ES256".to_string()), 30 + type_: Some("JWT".to_string()), 31 + ..Default::default() 32 + }; 137 33 138 - ```rust 139 - use atproto_oauth::resources::{discover_protected_resource, discover_authorization_server, validate}; 140 - use reqwest::Client; 34 + let claims = Claims::new(JoseClaims { 35 + issuer: Some("did:plc:issuer123".to_string()), 36 + subject: Some("did:plc:subject456".to_string()), 37 + audience: Some("https://pds.example.com".to_string()), 38 + expiration: Some(chrono::Utc::now().timestamp() as u64 + 3600), 39 + ..Default::default() 40 + }); 141 41 142 - #[tokio::main] 143 - async fn main() -> anyhow::Result<()> { 144 - let client = Client::new(); 145 - let pds_url = "https://pds.example.com"; 146 - 147 - // Discover OAuth protected resource configuration 148 - let protected_resource = discover_protected_resource(&client, pds_url).await?; 149 - println!("Resource: {}", protected_resource.resource); 150 - 151 - // Discover authorization server configuration 152 - let auth_server_url = &protected_resource.authorization_servers[0]; 153 - let auth_server = discover_authorization_server(&client, auth_server_url).await?; 154 - 155 - // Validate AT Protocol requirements 156 - validate::protected_resource(&protected_resource, pds_url)?; 157 - validate::authorization_server(&auth_server, pds_url)?; 158 - 159 - println!("OAuth configuration validated for AT Protocol compliance!"); 160 - 161 - Ok(()) 162 - } 42 + let token = mint(&key_data, &header, &claims)?; 43 + verify(&key_data, &token).await?; 163 44 ``` 164 45 165 - ### JWK Generation 46 + ### PKCE Flow 166 47 167 48 ```rust 168 - use atproto_oauth::jwk::{generate, WrappedJsonWebKeySet}; 169 - use atproto_identity::key::identify_key; 49 + use atproto_oauth::pkce; 170 50 171 - fn main() -> anyhow::Result<()> { 172 - let key_data = identify_key("did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA")?; 173 - 174 - // Generate JWK from key data 175 - let jwk = generate(&key_data)?; 176 - println!("JWK Algorithm: {:?}", jwk.alg); 177 - println!("JWK Key ID: {:?}", jwk.kid); 178 - 179 - // Create JWK Set 180 - let jwk_set = WrappedJsonWebKeySet { 181 - keys: vec![jwk], 182 - }; 183 - 184 - let jwk_json = serde_json::to_string_pretty(&jwk_set)?; 185 - println!("JWK Set: {}", jwk_json); 186 - 187 - Ok(()) 188 - } 51 + let (code_verifier, code_challenge) = pkce::generate(); 52 + // Use code_challenge in authorization URL 53 + // Later use code_verifier for token exchange 189 54 ``` 190 55 191 - ### DPoP Retry Middleware 56 + ### DPoP Proofs 192 57 193 58 ```rust 194 - use atproto_oauth::dpop::{DpopRetry, auth_dpop}; 195 - use reqwest_middleware::ClientBuilder; 196 - use reqwest_chain::ChainableClient; 197 - use atproto_identity::key::identify_key; 198 - 199 - #[tokio::main] 200 - async fn main() -> anyhow::Result<()> { 201 - let key_data = identify_key("did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA")?; 202 - 203 - // Create DPoP proof components 204 - let (_, header, claims) = auth_dpop(&key_data, "POST", "https://auth.example.com/oauth/token")?; 205 - 206 - // Create HTTP client with DPoP retry middleware 207 - let retry_middleware = DpopRetry::new(header, claims, key_data); 208 - let client = ClientBuilder::new(reqwest::Client::new()) 209 - .with_chainer(retry_middleware) 210 - .build(); 211 - 212 - // The client will automatically retry requests with DPoP nonce if needed 213 - let response = client 214 - .post("https://auth.example.com/oauth/token") 215 - .send() 216 - .await?; 217 - 218 - println!("Response status: {}", response.status()); 219 - 220 - Ok(()) 221 - } 222 - ``` 223 - 224 - ## Modules 225 - 226 - - **[`jwt`]** - JSON Web Token minting, verification, and validation 227 - - **[`jwk`]** - JSON Web Key generation and management 228 - - **[`pkce`]** - PKCE (Proof Key for Code Exchange) implementation 229 - - **[`dpop`]** - DPoP (Demonstration of Proof-of-Possession) implementation 230 - - **[`resources`]** - OAuth 2.0 resource discovery and AT Protocol validation 231 - - **[`encoding`]** - Base64 encoding and decoding utilities 232 - - **[`errors`]** - Structured error types for all OAuth operations 233 - 234 - ## AT Protocol Requirements 235 - 236 - This library validates OAuth servers against AT Protocol requirements: 237 - 238 - ### Authorization Server Requirements 239 - 240 - - Must support `authorization_code` and `refresh_token` grant types 241 - - Must support `S256` PKCE code challenge method 242 - - Must support `none` and `private_key_jwt` token endpoint authentication 243 - - Must support `ES256` algorithm for token endpoint authentication 244 - - Must support `atproto` and `transition:generic` scopes 245 - - Must support `ES256` algorithm for DPoP signing 246 - - Must support authorization response parameters, pushed requests, and client ID metadata 59 + use atproto_oauth::dpop::{auth_dpop, request_dpop}; 247 60 248 - ### Protected Resource Requirements 249 - 250 - - Resource URI must match the PDS base URL 251 - - Must specify exactly one authorization server 252 - - Authorization server must be properly configured for AT Protocol 253 - 254 - ## Error Handling 255 - 256 - All errors follow a structured format with unique identifiers: 257 - 258 - ``` 259 - error-atproto-oauth-<domain>-<number> <message>: <details> 61 + let (dpop_token, header, claims) = auth_dpop( 62 + &key_data, 63 + "POST", 64 + "https://auth.example.com/oauth/token" 65 + )?; 260 66 ``` 261 67 262 - Example error categories: 263 - 264 - - `error-atproto-oauth-jwt-1` through `error-atproto-oauth-jwt-9` - JWT validation errors 265 - - `error-atproto-oauth-client-1` through `error-atproto-oauth-client-12` - OAuth client errors 266 - - `error-atproto-oauth-dpop-1` through `error-atproto-oauth-dpop-5` - DPoP operation errors 267 - - `error-atproto-oauth-resource-1` through `error-atproto-oauth-resource-3` - Resource validation errors 268 - - `error-atproto-oauth-auth-server-1` through `error-atproto-oauth-auth-server-12` - Authorization server validation errors 68 + ### OAuth Discovery 269 69 270 70 ```rust 271 - use atproto_oauth::errors::{JWTError, DpopError, OAuthClientError}; 71 + use atproto_oauth::resources::{discover_protected_resource, discover_authorization_server}; 272 72 273 - // Example error handling 274 - match result { 275 - Err(JWTError::TokenExpired) => println!("JWT has expired"), 276 - Err(JWTError::SignatureVerificationFailed) => println!("Invalid JWT signature"), 277 - Err(DpopError::MissingDpopNonceHeader) => println!("Server didn't provide DPoP nonce"), 278 - Err(OAuthClientError::InvalidAuthorizationServerResponse(e)) => { 279 - println!("Authorization server error: {}", e); 280 - } 281 - Ok(result) => println!("Success: {:?}", result), 282 - } 73 + let protected_resource = discover_protected_resource(&client, pds_url).await?; 74 + let auth_server = discover_authorization_server(&client, auth_server_url).await?; 283 75 ``` 284 76 285 - ## Dependencies 286 - 287 - This crate builds on: 288 - 289 - - [`atproto-identity`](../atproto-identity) - Cryptographic key operations and DID resolution 290 - - `reqwest` - HTTP client for OAuth server communication 291 - - `serde_json` - JSON serialization for OAuth messages and JWT claims 292 - - `chrono` - Date and time handling for JWT timestamps 293 - - `base64` - Base64 encoding for JWT and DPoP operations 294 - - `p256` / `k256` - Elliptic curve cryptography for ES256/ES256K signatures 295 - - `ulid` - Unique identifier generation for JWT IDs and key IDs 296 - - `thiserror` - Structured error type derivation 297 - 298 - ## Security Considerations 299 - 300 - - Always validate OAuth server configurations against AT Protocol requirements 301 - - Use PKCE for all authorization code flows to prevent authorization code interception 302 - - Implement DPoP for token requests to bind tokens to specific clients 303 - - Verify JWT signatures and expiration times before accepting tokens 304 - - Use cryptographically secure random number generation for PKCE code verifiers 305 - - Follow RFC specifications for JWT, PKCE, and DPoP implementations 306 - 307 - ## Library Only 308 - 309 - This crate is designed as a library and does not provide command line tools. All functionality is accessed programmatically through the Rust API. For command line operations, see the [`atproto-identity`](../atproto-identity) and [`atproto-record`](../atproto-record) crates which include CLI tools for identity resolution and record signing operations. 310 - 311 - ## Contributing 312 - 313 - Contributions are welcome! Please ensure that: 314 - 315 - 1. All tests pass: `cargo test` 316 - 2. Code is properly formatted: `cargo fmt` 317 - 3. No linting issues: `cargo clippy` 318 - 4. New functionality includes appropriate tests and documentation 319 - 5. Error handling follows the project's structured error format 320 - 321 77 ## License 322 78 323 - This project is licensed under the MIT License. See the LICENSE file for details. 324 - 325 - ## Acknowledgments 326 - 327 - This library was extracted from the [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project, an open-source event and RSVP management and discovery application. 79 + MIT License
+1 -5
crates/atproto-oauth/src/lib.rs
··· 1 - //! AT Protocol OAuth library providing comprehensive OAuth 2.0 functionality. 2 - //! 3 - //! This crate implements OAuth 2.0 security extensions and utilities specifically designed for 4 - //! AT Protocol, including JWT/JWK operations, DPoP proof generation, PKCE implementation, 5 - //! and OAuth resource discovery with AT Protocol-specific validation requirements. 1 + //! OAuth 2.0 implementation for AT Protocol with DPoP support, PKCE, and JWT operations. 6 2 7 3 #![warn(missing_docs)] 8 4
+21 -177
crates/atproto-record/README.md
··· 1 1 # atproto-record 2 2 3 - A Rust library for AT Protocol record signature operations, providing cryptographic signing and verification capabilities for AT Protocol records. 3 + Record signature operations library for AT Protocol. 4 4 5 5 ## Overview 6 6 7 - This crate provides functionality for: 7 + `atproto-record` provides cryptographic signing and verification capabilities for AT Protocol records. This library handles signature creation, verification, and IPLD DAG-CBOR serialization for consistent record signing. 8 8 9 - - **Record Signing**: Create cryptographic signatures for AT Protocol records 10 - - **Signature Verification**: Verify existing signatures against records and public keys 11 - - **Error Handling**: Structured error types for signature operations 12 - - **Multi-curve Support**: Support for P-256 and K-256 elliptic curves via `atproto-identity` 9 + ## Binaries 13 10 14 - ## Features 15 - 16 - - Create signatures for AT Protocol records with proper `$sig` object handling 17 - - Required signature object validation (must include `issuer` and `issued_at` fields) 18 - - Verify record signatures against issuer public keys 19 - - IPLD DAG-CBOR serialization for consistent signature generation 20 - - Multibase encoding for signature representation 21 - - Integration with `atproto-identity` for cryptographic key operations 22 - - Repository and collection context support in signature objects 23 - - Comprehensive error handling with structured error types including creation and verification errors 11 + - **atproto-record-sign**: Creates cryptographic signatures for JSON records 12 + - **atproto-record-verify**: Verifies signatures against public keys 24 13 25 14 ## Usage 26 15 ··· 28 17 29 18 ```rust 30 19 use atproto_record::signature; 31 - use atproto_identity::key::{identify_key, KeyType}; 20 + use atproto_identity::key::identify_key; 32 21 use serde_json::json; 33 - use atproto_record::errors::VerificationError; 34 22 35 - # async fn example() -> Result<(), VerificationError> { 36 - // Prepare key data 37 - let key_data = identify_key("did:key:example...").map_err(|e| { 38 - VerificationError::KeyOperationFailed(e) 39 - })?; 23 + let key_data = identify_key("did:key:zQ3sh...")?; 24 + let record = json!({"$type": "app.bsky.feed.post", "text": "Hello!"}); 25 + let signature_object = json!({"issuer": "did:plc:issuer", "issued_at": "2024-01-01T00:00:00Z"}); 40 26 41 - // Create a record to sign 42 - let record = json!({ 43 - "$type": "app.bsky.feed.post", 44 - "text": "Hello AT Protocol!", 45 - "createdAt": "2024-01-01T00:00:00Z" 46 - }); 47 - 48 - // Create signature object with required fields 49 - let signature_object = json!({ 50 - "issuer": "did:plc:signer123", 51 - "issued_at": "2024-01-01T00:00:00Z" 52 - }); 53 - 54 - // Create signature 55 27 let signed_record = signature::create( 56 28 &key_data, 57 29 &record, 58 - "did:plc:user123", 30 + "did:plc:repo", 59 31 "app.bsky.feed.post", 60 32 signature_object 61 33 ).await?; 62 - # Ok(()) 63 - # } 64 34 ``` 65 35 66 36 ### Verifying Signatures 67 37 68 38 ```rust 69 - use atproto_record::signature; 70 - use atproto_identity::key::identify_key; 71 - use atproto_record::errors::VerificationError; 39 + let issuer_key = identify_key("did:key:zQ3sh...")?; 72 40 73 - # async fn example() -> Result<(), VerificationError> { 74 - // Get the issuer's public key 75 - let issuer_key = identify_key("did:key:issuer...").map_err(|e| { 76 - VerificationError::KeyOperationFailed(e) 77 - })?; 78 - 79 - // Verify the signature 80 41 signature::verify( 81 - "did:plc:issuer123", 42 + "did:plc:issuer", 82 43 &issuer_key, 83 44 signed_record, 84 - "did:plc:user123", 45 + "did:plc:repo", 85 46 "app.bsky.feed.post" 86 47 ).await?; 87 - # Ok(()) 88 - # } 89 48 ``` 90 49 91 - ## Command Line Tools 92 - 93 - The crate includes two command-line tools for AT Protocol record signature operations: 94 - 95 - ### `atproto-record-sign` 96 - 97 - Creates cryptographic signatures for AT Protocol records with proper `$sig` object handling and embedded signature metadata. This tool reads JSON records, applies cryptographic signatures using DID keys, and outputs signed records ready for AT Protocol repository storage. 98 - 99 - **Features:** 100 - - **Flexible Input**: Reads records from files or stdin 101 - - **DID Key Support**: Works with both P-256 and K-256 cryptographic keys 102 - - **Signature Object Creation**: Automatically creates required signature metadata with issuer and timestamp 103 - - **Repository Context**: Includes repository and collection context in signatures 104 - - **IPLD Serialization**: Uses DAG-CBOR serialization for consistent signature generation 105 - - **Multibase Encoding**: Outputs signatures in multibase format for AT Protocol compatibility 106 - 107 - ```bash 108 - # Sign a record from a file with all required parameters 109 - cargo run --bin atproto-record-sign did:key:zQ3sh... did:plc:issuer123 record.json repository=did:plc:user123 collection=app.bsky.feed.post 110 - 111 - # Sign a record from stdin 112 - echo '{"$type":"app.bsky.feed.post","text":"Hello AT Protocol!"}' | \ 113 - cargo run --bin atproto-record-sign did:key:zQ3sh... did:plc:issuer123 -- \ 114 - repository=did:plc:user123 collection=app.bsky.feed.post 115 - 116 - # Example output: JSON record with embedded signatures array 117 - ``` 118 - 119 - **Arguments:** 120 - - `<signing_key>` - DID key string for signing (did:key:...) 121 - - `<issuer_did>` - DID of the signing entity 122 - - `<record_file>` - JSON file containing the record (optional, uses stdin if omitted) 123 - - `repository=<did>` - Repository DID where record will be stored 124 - - `collection=<nsid>` - Collection NSID (e.g., app.bsky.feed.post) 125 - 126 - ### `atproto-record-verify` 127 - 128 - Verifies cryptographic signatures of AT Protocol records using embedded signature metadata. This tool validates that signed records contain authentic signatures from specified issuers, ensuring record integrity and authenticity. 129 - 130 - **Features:** 131 - - **Signature Validation**: Verifies embedded signatures against public keys 132 - - **Issuer Authentication**: Confirms signatures are from specified DID issuers 133 - - **Context Verification**: Validates repository and collection context in signatures 134 - - **Multi-Signature Support**: Handles records with multiple signatures 135 - - **IPLD Deserialization**: Uses DAG-CBOR for signature verification consistency 136 - - **Detailed Error Reporting**: Provides specific feedback on verification failures 50 + ## Command Line Examples 137 51 138 52 ```bash 139 - # Verify a signed record from a file 140 - cargo run --bin atproto-record-verify did:plc:issuer123 did:key:zQ3sh... signed_record.json \ 141 - repository=did:plc:user123 collection=app.bsky.feed.post 142 - 143 - # Verify a signed record from stdin 144 - echo '{"signatures":[{"issuer":"did:plc:issuer123","signature":"u..."}],"$type":"app.bsky.feed.post","text":"Hello"}' | \ 145 - cargo run --bin atproto-record-verify did:plc:issuer123 did:key:zQ3sh... -- \ 146 - repository=did:plc:user123 collection=app.bsky.feed.post 53 + # Sign a record 54 + cargo run --bin atproto-record-sign did:key:zQ3sh... did:plc:issuer record.json \ 55 + repository=did:plc:repo collection=app.bsky.feed.post 147 56 148 - # Successful verification returns exit code 0 149 - # Failed verification returns exit code 1 with error details 57 + # Verify a signature 58 + cargo run --bin atproto-record-verify did:plc:issuer did:key:zQ3sh... signed.json \ 59 + repository=did:plc:repo collection=app.bsky.feed.post 150 60 ``` 151 61 152 - **Arguments:** 153 - - `<issuer_did>` - DID of the expected signature issuer 154 - - `<public_key>` - DID key string for verification (did:key:...) 155 - - `<record_file>` - JSON file containing the signed record (optional, uses stdin if omitted) 156 - - `repository=<did>` - Repository DID context for verification 157 - - `collection=<nsid>` - Collection NSID context for verification 158 - 159 - **Exit Codes:** 160 - - `0` - Signature verification successful 161 - - `1` - Signature verification failed or invalid arguments 162 - - `2` - File I/O or parsing errors 163 - 164 - ## Modules 165 - 166 - - [`signature`] - Core signature creation and verification functions 167 - - [`errors`] - Structured error types for signature operations 168 - 169 - ## Error Handling 170 - 171 - The crate uses structured error types defined in the `errors` module: 172 - 173 - ```rust 174 - use atproto_record::errors::VerificationError; 175 - use serde_json::json; 176 - 177 - // Example error handling for signature creation 178 - let signature_object = json!({ "missing": "required_fields" }); 179 - 180 - match signature::create(&key_data, &record, "repo", "collection", signature_object).await { 181 - Ok(signed_record) => println!("Signature created successfully!"), 182 - Err(VerificationError::SignatureObjectMissingField { field }) => { 183 - println!("Missing required field in signature object: {}", field); 184 - } 185 - Err(VerificationError::InvalidSignatureObjectType) => { 186 - println!("Signature object must be a JSON object"); 187 - } 188 - Err(VerificationError::KeyOperationFailed(e)) => { 189 - println!("Cryptographic operation failed: {}", e); 190 - } 191 - Err(e) => println!("Other error: {}", e), 192 - } 193 - 194 - // Example error handling for signature verification 195 - match signature::verify("did:plc:issuer", &key_data, record, "repo", "collection").await { 196 - Ok(()) => println!("Signature valid!"), 197 - Err(VerificationError::NoValidSignatureForIssuer { issuer }) => { 198 - println!("No valid signature found for issuer: {}", issuer); 199 - } 200 - Err(VerificationError::NoSignaturesField) => { 201 - println!("Record contains no signatures field"); 202 - } 203 - Err(e) => println!("Verification failed: {}", e), 204 - } 205 - ``` 206 - 207 - ## Dependencies 208 - 209 - This crate builds on: 210 - 211 - - [`atproto-identity`](../atproto-identity) - Cryptographic key operations and DID resolution 212 - - `serde_ipld_dagcbor` - IPLD DAG-CBOR serialization for signature content 213 - - `multibase` - Base encoding for signature representation 214 - - `serde_json` - JSON handling for AT Protocol records 215 - - `anyhow` - Error handling utilities 216 - - `thiserror` - Structured error type derivation 217 - 218 62 ## License 219 63 220 - Licensed under the MIT License. 64 + MIT License
+4 -113
crates/atproto-record/src/lib.rs
··· 1 - //! # atproto-record 2 - //! 3 - //! A Rust library for AT Protocol record signature operations, providing cryptographic 4 - //! signing and verification capabilities for AT Protocol records. 5 - //! 6 - //! This crate handles the cryptographic aspects of AT Protocol record attestation, 7 - //! including signature creation and verification with proper `$sig` object handling 8 - //! and IPLD DAG-CBOR serialization. 9 - //! 10 - //! ## Core Modules 11 - //! 12 - //! - [`signature`] - Core signature creation and verification functions 13 - //! - [`errors`] - Structured error types for signature verification operations 14 - //! 15 - //! ## Key Features 1 + //! Cryptographic signature operations for AT Protocol records with IPLD DAG-CBOR serialization. 16 2 //! 17 - //! - **Record Signing**: Create cryptographic signatures for AT Protocol records 18 - //! - **Signature Verification**: Verify existing signatures against records and public keys 19 - //! - **Multi-curve Support**: Support for P-256 and K-256 elliptic curves via `atproto-identity` 20 - //! - **IPLD Integration**: Proper IPLD DAG-CBOR serialization for signature content 21 - //! - **Error Handling**: Comprehensive structured error types following project conventions 22 - //! 23 - //! ## Usage Examples 24 - //! 25 - //! ### Creating a Signature 26 - //! 27 - //! ```rust 28 - //! use atproto_record::signature; 29 - //! use atproto_record::errors::VerificationError; 30 - //! use serde_json::json; 31 - //! use atproto_identity::key::{KeyData, KeyType}; 32 - //! async fn example() -> Result<(), VerificationError> { 33 - //! let key_data = KeyData::new(KeyType::P256Public, vec![]); 34 - //! 35 - //! let record = json!({ 36 - //! "$type": "app.bsky.feed.post", 37 - //! "text": "Hello AT Protocol!" 38 - //! }); 39 - //! 40 - //! // Create signature object with required fields 41 - //! let signature_object = json!({ 42 - //! "issuer": "did:plc:signer123", 43 - //! "issued_at": "2024-01-01T00:00:00Z" 44 - //! }); 45 - //! 46 - //! let signed_record = signature::create( 47 - //! &key_data, 48 - //! &record, 49 - //! "did:plc:user123", 50 - //! "app.bsky.feed.post", 51 - //! signature_object 52 - //! ).await?; 53 - //! Ok(()) 54 - //! } 55 - //! ``` 56 - //! 57 - //! ### Verifying a Signature 58 - //! 59 - //! ```rust 60 - //! use atproto_record::signature; 61 - //! use atproto_record::errors::VerificationError; 62 - //! use atproto_identity::key::{KeyData, KeyType}; 63 - //! use serde_json::json; 64 - //! async fn example() -> Result<(), VerificationError> { 65 - //! let issuer_key = KeyData::new(KeyType::P256Public, vec![]); 66 - //! let signed_record = json!({ 67 - //! "signatures": [{ 68 - //! "issuer": "did:plc:issuer123", 69 - //! "signature": "uExample_signature_data" 70 - //! }] 71 - //! }); 72 - //! 73 - //! signature::verify( 74 - //! "did:plc:issuer123", 75 - //! &issuer_key, 76 - //! signed_record, 77 - //! "did:plc:user123", 78 - //! "app.bsky.feed.post" 79 - //! ).await?; 80 - //! Ok(()) 81 - //! } 82 - //! ``` 83 - //! 84 - //! ## Error Handling 85 - //! 86 - //! All signature operations return structured errors defined in the [`errors`] module: 87 - //! 88 - //! ```rust 89 - //! use atproto_record::errors::VerificationError; 90 - //! use atproto_record::signature::create; 91 - //! use serde_json::json; 92 - //! 93 - //! // Example: handling signature creation errors 94 - //! async fn example() -> Result<(), Box<dyn std::error::Error>> { 95 - //! let key_data = atproto_identity::key::KeyData::new(atproto_identity::key::KeyType::P256Public, vec![]); 96 - //! let record = json!({}); 97 - //! let invalid_signature_object = json!({ "missing": "required_fields" }); 98 - //! 99 - //! match create(&key_data, &record, "repo", "collection", invalid_signature_object).await { 100 - //! Ok(_) => println!("Signature created successfully!"), 101 - //! Err(VerificationError::SignatureObjectMissingField { field }) => { 102 - //! println!("Missing required field: {}", field); 103 - //! } 104 - //! Err(VerificationError::InvalidSignatureObjectType) => { 105 - //! println!("Signature object must be a JSON object"); 106 - //! } 107 - //! Err(VerificationError::KeyOperationFailed { .. }) => { 108 - //! println!("Cryptographic operation failed"); 109 - //! } 110 - //! Err(e) => println!("Other error: {}", e), 111 - //! } 112 - //! Ok(()) 113 - //! } 114 - //! ``` 3 + //! Provides binaries: 4 + //! - `atproto-record-sign`: Sign AT Protocol records with private keys 5 + //! - `atproto-record-verify`: Verify signatures on AT Protocol records 115 6 116 7 #![warn(missing_docs)] 117 8
+33 -176
crates/atproto-xrpcs-helloworld/README.md
··· 1 1 # atproto-xrpcs-helloworld 2 2 3 - A complete example implementation of an AT Protocol XRPC service demonstrating DID web functionality, service document generation, and JWT-based authentication using the `atproto-xrpcs` library. 3 + Example XRPC service implementation demonstrating AT Protocol service patterns. 4 4 5 5 ## Overview 6 6 7 - `atproto-xrpcs-helloworld` provides a fully functional AT Protocol XRPC service that serves as both a reference implementation and a development tool. This service demonstrates all the core concepts of AT Protocol XRPC services including DID web identity, service document generation, well-known endpoint handling, and authenticated XRPC endpoints. 7 + `atproto-xrpcs-helloworld` provides a complete AT Protocol XRPC service that demonstrates DID web identity, service document generation, well-known endpoint handling, and JWT-based authentication using the `atproto-xrpcs` library. 8 8 9 - This project was extracted from the open-sourced [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project and serves as an example of how to build complete AT Protocol services using the project's XRPC components. 9 + ## Binaries 10 + 11 + - **atproto-xrpcs-helloworld**: Complete XRPC service with DID web identity and authentication 10 12 11 13 ## Features 12 14 13 - - **Complete XRPC Service**: Full implementation of an AT Protocol XRPC service with authentication 14 - - **DID Web Support**: Native DID web identity with automatic service document generation 15 - - **Well-Known Endpoints**: Standard AT Protocol discovery endpoints for identity and service metadata 16 - - **Hello World XRPC**: Example authenticated and unauthenticated XRPC endpoint implementation 17 - - **JWT Authentication**: Demonstrates integration with `atproto-xrpcs` authorization extractors 18 - - **Service Discovery**: Complete service document with verification methods and service endpoints 19 - - **Development Ready**: Configured for immediate deployment and testing 15 + - Complete XRPC service implementation with authentication 16 + - DID web identity with automatic service document generation 17 + - Well-known endpoints for AT Protocol discovery 18 + - Example authenticated and unauthenticated XRPC endpoint 19 + - JWT authorization with DID document verification 20 20 21 - ## Installation 21 + ## Usage 22 22 23 - This crate produces a binary executable. Build it with: 23 + The service provides these endpoints: 24 24 25 - ```bash 26 - cargo build --release --bin atproto-xrpcs-helloworld 27 - ``` 25 + - `GET /` - HTML hello world page 26 + - `GET /.well-known/did.json` - DID web document 27 + - `GET /.well-known/atproto-did` - AT Protocol DID discovery 28 + - `GET /xrpc/garden.lexicon.ngerakines.helloworld.Hello` - Example XRPC endpoint 28 29 29 - ## Command Line Tool 30 - 31 - ### `atproto-xrpcs-helloworld` 32 - 33 - A complete AT Protocol XRPC service implementation that demonstrates all core functionality for building production XRPC services. This tool sets up a full web server with DID web identity, service discovery endpoints, and authenticated XRPC endpoints. 34 - 35 - **Features:** 36 - - **DID Web Identity**: Automatically generates and serves DID web documents 37 - - **Service Document**: Creates complete AT Protocol service documents with verification methods 38 - - **Well-Known Endpoints**: Implements required discovery endpoints for AT Protocol services 39 - - **XRPC Hello World**: Example XRPC endpoint with both authenticated and unauthenticated access 40 - - **JWT Authorization**: Demonstrates proper JWT validation and DID document verification 41 - - **Configurable Identity**: Uses environment variables for service identity configuration 30 + ### Environment Variables 42 31 43 32 ```bash 44 - # Start the XRPC hello world service 45 - cargo run --bin atproto-xrpcs-helloworld 46 - 47 - # The service will start on http://0.0.0.0:8080 with the following endpoints: 48 - # GET / - HTML hello world page 49 - # GET /.well-known/did.json - DID web document 50 - # GET /.well-known/atproto-did - AT Protocol DID discovery 51 - # GET /xrpc/garden.lexicon.ngerakines.helloworld.Hello - XRPC hello world endpoint 52 - 53 - # Example XRPC requests: 54 - # curl "http://localhost:8080/xrpc/garden.lexicon.ngerakines.helloworld.Hello?subject=Alice" 55 - # curl -H "Authorization: Bearer <jwt-token>" "http://localhost:8080/xrpc/garden.lexicon.ngerakines.helloworld.Hello?subject=Bob" 56 - ``` 57 - 58 - **Environment Variables:** 59 - 60 - ```bash 61 - # Required: External base URL for service identity 33 + # Required 62 34 export EXTERNAL_BASE=your-service.com 63 - 64 - # Required: Service private key for DID web identity 65 - export SERVICE_KEY=did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA 35 + export SERVICE_KEY=did:key:zQ3sh... 66 36 67 - # Optional: Custom PLC directory hostname 37 + # Optional 68 38 export PLC_HOSTNAME=plc.directory 69 - 70 - # Optional: Custom DNS nameservers (semicolon-separated) 71 39 export DNS_NAMESERVERS=8.8.8.8;1.1.1.1 72 - 73 - # Optional: Custom CA certificate bundles (semicolon-separated paths) 74 - export CERTIFICATE_BUNDLES=/path/to/cert1.pem;/path/to/cert2.pem 75 - 76 - # Optional: Custom User-Agent string 77 40 export USER_AGENT="my-xrpc-service/1.0" 78 41 ``` 79 42 80 - **Service Endpoints:** 81 - 82 - The service automatically provides these AT Protocol standard endpoints: 83 - 84 - 1. **`GET /`** - Simple HTML hello world page for service status 85 - 2. **`GET /.well-known/did.json`** - DID web document with verification methods and service endpoints 86 - 3. **`GET /.well-known/atproto-did`** - AT Protocol DID discovery endpoint returning the service DID 87 - 4. **`GET /xrpc/garden.lexicon.ngerakines.helloworld.Hello`** - Example XRPC endpoint with optional authentication 88 - 89 - **XRPC Endpoint Details:** 90 - 91 - The hello world XRPC endpoint demonstrates: 92 - - **Optional Authentication**: Works with or without JWT authorization headers 93 - - **Parameter Processing**: Accepts optional `subject` query parameter 94 - - **Different Responses**: Returns different messages for authenticated vs unauthenticated requests 95 - - **Proper XRPC Format**: Returns JSON responses following AT Protocol XRPC conventions 43 + ### Example Requests 96 44 97 45 ```bash 98 46 # Unauthenticated request 99 47 curl "http://localhost:8080/xrpc/garden.lexicon.ngerakines.helloworld.Hello?subject=World" 100 48 # Response: {"message": "Hello, World!"} 101 49 102 - # Authenticated request (with valid JWT) 103 - curl -H "Authorization: Bearer <valid-jwt>" \ 50 + # Authenticated request 51 + curl -H "Authorization: Bearer <jwt-token>" \ 104 52 "http://localhost:8080/xrpc/garden.lexicon.ngerakines.helloworld.Hello?subject=Alice" 105 53 # Response: {"message": "Hello, authenticated Alice!"} 106 54 ``` 107 55 108 - **Service Document Structure:** 109 - 110 - The service automatically generates a complete DID web document: 111 - 112 - ```json 113 - { 114 - "@context": [ 115 - "https://www.w3.org/ns/did/v1", 116 - "https://w3id.org/security/multikey/v1" 117 - ], 118 - "id": "did:web:your-service.com", 119 - "verificationMethod": [{ 120 - "id": "did:web:your-service.com#atproto", 121 - "type": "Multikey", 122 - "controller": "did:web:your-service.com", 123 - "publicKeyMultibase": "zQ3sh..." 124 - }], 125 - "service": [{ 126 - "id": "#bsky_appview", 127 - "type": "BskyAppView", 128 - "serviceEndpoint": "https://your-service.com" 129 - }] 130 - } 131 - ``` 132 - 133 - ## Use Cases 134 - 135 - This example service is ideal for: 136 - 137 - - **Learning AT Protocol**: Understanding how XRPC services work in practice 138 - - **Development Testing**: Testing AT Protocol clients against a known service 139 - - **Service Template**: Starting point for building production AT Protocol services 140 - - **Integration Testing**: Testing authentication and authorization flows 141 - - **DID Web Examples**: Understanding DID web document structure and endpoints 142 - 143 - ## Implementation Details 144 - 145 - The service demonstrates several key AT Protocol concepts: 146 - 147 - ### DID Web Identity 148 - - Automatic generation of DID web documents with verification methods 149 - - Service endpoint definitions for AT Protocol service discovery 150 - - Public key management for service authentication 151 - 152 - ### XRPC Service Implementation 153 - - Proper XRPC endpoint naming following AT Protocol conventions 154 - - JSON request/response handling with serde integration 155 - - Parameter extraction and validation using Axum extractors 156 - 157 - ### Authentication Integration 158 - - Optional JWT authorization using `ResolvingAuthorization` extractor 159 - - Automatic DID document resolution and signature verification 160 - - Different behavior for authenticated vs unauthenticated requests 161 - 162 - ### Service Architecture 163 - - Modular state management using Axum's state system 164 - - Integration with `atproto-identity` for cryptographic operations 165 - - LRU caching for DID document storage and performance 166 - 167 - ## Dependencies 168 - 169 - This binary builds on: 170 - 171 - - [`atproto-xrpcs`](../atproto-xrpcs) - XRPC service building blocks and authorization 172 - - [`atproto-identity`](../atproto-identity) - Identity resolution and cryptographic operations 173 - - [`atproto-oauth`](../atproto-oauth) - OAuth 2.0 operations and JWT handling 174 - - [`atproto-record`](../atproto-record) - AT Protocol record operations 175 - - `axum` - Web framework for HTTP server implementation 176 - - `reqwest` - HTTP client for identity resolution 177 - - `tokio` - Async runtime for web service operations 178 - - `serde_json` - JSON handling for XRPC requests and responses 179 - 180 - ## Development 181 - 182 - To run the service locally for development: 56 + ## Command Line Examples 183 57 184 58 ```bash 185 - # Set required environment variables 186 - export EXTERNAL_BASE=localhost:8080 187 - export SERVICE_KEY=did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA 188 - 189 - # Run the service 59 + # Start the service 190 60 cargo run --bin atproto-xrpcs-helloworld 191 61 192 - # Test the endpoints 62 + # Test endpoints 193 63 curl http://localhost:8080/ 194 64 curl http://localhost:8080/.well-known/did.json 195 - curl http://localhost:8080/.well-known/atproto-did 196 65 curl "http://localhost:8080/xrpc/garden.lexicon.ngerakines.helloworld.Hello?subject=Test" 197 66 ``` 198 67 199 - ## Security Considerations 68 + ## Use Cases 200 69 201 - - **Service Key Management**: The `SERVICE_KEY` environment variable contains private key material and should be handled securely 202 - - **JWT Validation**: The service performs full JWT signature validation against DID documents 203 - - **DID Verification**: All authentication goes through proper DID document resolution and verification 204 - - **HTTPS Recommended**: Production deployments should use HTTPS for all endpoints 70 + This example service is ideal for: 205 71 206 - ## Contributing 207 - 208 - Contributions are welcome! Please ensure that: 209 - 210 - 1. All tests pass: `cargo test` 211 - 2. Code is properly formatted: `cargo fmt` 212 - 3. No linting issues: `cargo clippy` 213 - 4. New functionality includes appropriate tests and documentation 214 - 5. Changes maintain compatibility with AT Protocol specifications 72 + - Learning AT Protocol XRPC service patterns 73 + - Testing AT Protocol clients against a known service 74 + - Starting point for building production services 75 + - Understanding DID web document structure 215 76 216 77 ## License 217 78 218 - This project is licensed under the MIT License. See the LICENSE file for details. 219 - 220 - ## Acknowledgments 221 - 222 - This service was extracted from the [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project, an open-source event and RSVP management and discovery application. 79 + MIT License
+23 -176
crates/atproto-xrpcs/README.md
··· 1 1 # atproto-xrpcs 2 2 3 - A Rust library providing core building blocks for implementing XRPC services in the AT Protocol ecosystem, with JWT-based authorization extractors for Axum web handlers and DID document verification support. 3 + XRPC service components library for AT Protocol with JWT authorization. 4 4 5 5 ## Overview 6 6 7 - `atproto-xrpcs` provides foundational components for building AT Protocol XRPC (Cross-Protocol Remote Procedure Call) services. This library offers JWT-based authorization extractors that integrate with Axum web handlers, enabling secure XRPC endpoints with automatic DID document verification and structured error handling. 8 - 9 - This project was extracted from the open-sourced [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project and is designed to be a standalone, reusable library for AT Protocol XRPC service implementations. 7 + `atproto-xrpcs` provides foundational components for building AT Protocol XRPC services. This library offers JWT-based authorization extractors that integrate with Axum web handlers, enabling secure XRPC endpoints with automatic DID document verification. 10 8 11 9 ## Features 12 10 13 - - **JWT Authorization Extractors**: Axum-compatible request extractors for JWT-based authorization 14 - - **DID Document Verification**: Automatic verification of caller identities through DID document resolution 15 - - **XRPC Service Components**: Core building blocks for implementing AT Protocol XRPC endpoints 16 - - **Structured Error Handling**: Comprehensive error types with detailed error codes and messages 17 - - **Axum Integration**: Native integration with Axum web framework for HTTP handlers 18 - - **Identity Resolution**: Built-in support for resolving AT Protocol identities and handles 19 - - **Security Features**: Robust authorization patterns with cryptographic verification 20 - 21 - ## Installation 22 - 23 - Add this to your `Cargo.toml`: 24 - 25 - ```toml 26 - [dependencies] 27 - atproto-xrpcs = "0.6.0" 28 - ``` 11 + - JWT authorization extractors for Axum web handlers 12 + - Automatic DID document verification for caller identities 13 + - Structured error handling with detailed error codes 14 + - Native Axum integration for HTTP handlers 15 + - Identity resolution support for AT Protocol 29 16 30 17 ## Usage 31 18 32 - ### Basic XRPC Service Setup 19 + ### Basic XRPC Service 33 20 34 21 ```rust 35 22 use atproto_xrpcs::authorization::ResolvingAuthorization; ··· 42 29 name: Option<String>, 43 30 } 44 31 45 - // XRPC handler with optional JWT authorization 46 32 async fn handle_hello( 47 33 params: Query<HelloParams>, 48 34 authorization: Option<ResolvingAuthorization>, ··· 58 44 Json(json!({ "message": message })) 59 45 } 60 46 61 - #[tokio::main] 62 - async fn main() -> anyhow::Result<()> { 63 - // Set up your Axum router with XRPC endpoints 64 - let app = Router::new() 65 - .route("/xrpc/com.example.hello", get(handle_hello)) 66 - .with_state(your_web_context); 67 - 68 - let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?; 69 - axum::serve(listener, app).await?; 70 - 71 - Ok(()) 72 - } 47 + let app = Router::new() 48 + .route("/xrpc/com.example.hello", get(handle_hello)) 49 + .with_state(your_web_context); 73 50 ``` 74 51 75 - ### JWT Authorization with DID Verification 52 + ### JWT Authorization 76 53 77 54 ```rust 78 55 use atproto_xrpcs::authorization::ResolvingAuthorization; 79 - use axum::{Json, extract::Query}; 80 - use serde::Deserialize; 81 - use serde_json::json; 82 56 83 - #[derive(Deserialize)] 84 - struct SecureParams { 85 - data: String, 86 - } 87 - 88 - // Handler that requires authentication 89 57 async fn handle_secure_endpoint( 90 - params: Query<SecureParams>, 91 58 authorization: ResolvingAuthorization, // Required authorization 92 59 ) -> Json<serde_json::Value> { 93 60 // The ResolvingAuthorization extractor automatically: 94 61 // 1. Validates the JWT token 95 - // 2. Resolves the caller's DID document 62 + // 2. Resolves the caller's DID document 96 63 // 3. Verifies the signature against the DID document 97 64 // 4. Provides access to caller identity information 98 65 99 66 let caller_did = authorization.subject(); 100 - 101 - Json(json!({ 102 - "message": "Secure operation completed", 103 - "caller": caller_did, 104 - "data": params.data 105 - })) 67 + Json(json!({"caller": caller_did, "status": "authenticated"})) 106 68 } 107 69 ``` 108 70 109 - ### Error Handling in XRPC Services 71 + ### Error Handling 110 72 111 73 ```rust 112 74 use atproto_xrpcs::errors::AuthorizationError; 113 75 use axum::{response::IntoResponse, http::StatusCode}; 114 76 115 - // XRPC services can handle authorization errors appropriately 116 77 async fn protected_handler( 117 78 authorization: Result<ResolvingAuthorization, AuthorizationError>, 118 79 ) -> impl IntoResponse { 119 80 match authorization { 120 - Ok(auth) => { 121 - // Handle authenticated request 122 - (StatusCode::OK, "Access granted").into_response() 123 - } 81 + Ok(auth) => (StatusCode::OK, "Access granted").into_response(), 124 82 Err(AuthorizationError::InvalidJWTToken { .. }) => { 125 83 (StatusCode::UNAUTHORIZED, "Invalid token").into_response() 126 84 } ··· 134 92 } 135 93 ``` 136 94 137 - ### Integration with AT Protocol Identity System 138 - 139 - ```rust 140 - use atproto_xrpcs::authorization::ResolvingAuthorization; 141 - use atproto_identity::{ 142 - axum::state::DidDocumentStorageExtractor, 143 - resolve::IdentityResolver, 144 - }; 145 - use axum::extract::State; 146 - 147 - // XRPC handlers can access the full AT Protocol identity system 148 - async fn handle_identity_operation( 149 - authorization: ResolvingAuthorization, 150 - State(identity_resolver): State<IdentityResolver>, 151 - State(storage): State<DidDocumentStorageExtractor>, 152 - ) -> Json<serde_json::Value> { 153 - let caller_did = authorization.subject(); 154 - 155 - // Use the identity resolver for additional operations 156 - // Access DID document storage for caching and retrieval 157 - 158 - Json(json!({ 159 - "caller": caller_did, 160 - "status": "Identity verified and processed" 161 - })) 162 - } 163 - ``` 164 - 165 - ## Modules 166 - 167 - - **[`authorization`]** - JWT-based authorization extractors with DID document verification 168 - - **[`errors`]** - Structured error types for XRPC authorization operations 169 - 170 95 ## Authorization Flow 171 96 172 - The `ResolvingAuthorization` extractor implements a comprehensive authorization flow: 173 - 174 - 1. **JWT Extraction**: Extracts JWT tokens from HTTP Authorization headers 175 - 2. **Token Validation**: Validates JWT signature and claims structure 176 - 3. **DID Resolution**: Resolves the token issuer's DID document 177 - 4. **Signature Verification**: Verifies the JWT signature against the DID document's public keys 178 - 5. **Identity Confirmation**: Confirms the caller's identity and authorization scope 179 - 180 - ## Error Handling 181 - 182 - All errors follow a structured format with unique identifiers: 183 - 184 - ``` 185 - error-atproto-xrpcs-<domain>-<number> <message>: <details> 186 - ``` 187 - 188 - Example error categories: 189 - 190 - - `error-atproto-xrpcs-authorization-1` through `error-atproto-xrpcs-authorization-15` - JWT authorization errors 97 + The `ResolvingAuthorization` extractor implements: 191 98 192 - ```rust 193 - use atproto_xrpcs::errors::AuthorizationError; 194 - 195 - // Example error handling 196 - match authorization_result { 197 - Err(AuthorizationError::MissingAuthorizationHeader) => { 198 - println!("No authorization header provided"); 199 - } 200 - Err(AuthorizationError::InvalidJWTToken { error }) => { 201 - println!("Invalid JWT token: {}", error); 202 - } 203 - Err(AuthorizationError::DIDDocumentResolutionFailed { did, error }) => { 204 - println!("Failed to resolve DID {}: {}", did, error); 205 - } 206 - Ok(auth) => println!("Authorization successful for: {}", auth.subject()), 207 - } 208 - ``` 209 - 210 - ## Security Features 211 - 212 - - **JWT Validation**: Comprehensive JWT token structure and signature validation 213 - - **DID Verification**: Automatic resolution and verification of caller DID documents 214 - - **Cryptographic Security**: Integration with `atproto-identity` for secure key operations 215 - - **Authorization Scope**: Support for AT Protocol authorization scopes and permissions 216 - - **Identity Binding**: Strong binding between JWT tokens and DID document public keys 217 - 218 - ## Dependencies 219 - 220 - This crate builds on: 221 - 222 - - [`atproto-identity`](../atproto-identity) - Identity resolution and cryptographic operations 223 - - [`atproto-oauth`](../atproto-oauth) - OAuth 2.0 operations and JWT handling 224 - - [`atproto-record`](../atproto-record) - AT Protocol record operations 225 - - `axum` - Web framework for HTTP handlers and extractors 226 - - `reqwest` - HTTP client for DID document resolution 227 - - `serde_json` - JSON handling for XRPC request/response data 228 - - `tokio` - Async runtime for web service operations 229 - - `thiserror` - Structured error type derivation 230 - 231 - ## AT Protocol XRPC 232 - 233 - This library implements components for [AT Protocol XRPC](https://atproto.com/specs/xrpc), which provides: 234 - 235 - - **Cross-Protocol Communication**: Standardized RPC over HTTP for AT Protocol services 236 - - **Schema-Driven APIs**: Type-safe API definitions using Lexicon schemas 237 - - **Authentication Integration**: Seamless integration with AT Protocol identity and authorization 238 - - **Service Discovery**: Support for AT Protocol service discovery and routing 239 - 240 - ## Library Only 241 - 242 - This crate is designed as a library and does not provide command line tools. All functionality is accessed programmatically through the Rust API. For a complete example implementation, see the [`atproto-xrpcs-helloworld`](../atproto-xrpcs-helloworld) crate which demonstrates a full XRPC service using these components. 243 - 244 - ## Contributing 245 - 246 - Contributions are welcome! Please ensure that: 247 - 248 - 1. All tests pass: `cargo test` 249 - 2. Code is properly formatted: `cargo fmt` 250 - 3. No linting issues: `cargo clippy` 251 - 4. New functionality includes appropriate tests and documentation 252 - 5. Error handling follows the project's structured error format 99 + 1. JWT extraction from HTTP Authorization headers 100 + 2. Token validation (signature and claims structure) 101 + 3. DID resolution for the token issuer 102 + 4. Signature verification against DID document public keys 103 + 5. Identity confirmation and authorization scope validation 253 104 254 105 ## License 255 106 256 - This project is licensed under the MIT License. See the LICENSE file for details. 257 - 258 - ## Acknowledgments 259 - 260 - This library was extracted from the [Smokesignal](https://tangled.sh/@smokesignal.events/smokesignal) project, an open-source event and RSVP management and discovery application. 107 + MIT License
+1 -5
crates/atproto-xrpcs/src/lib.rs
··· 1 - //! # AT Protocol XRPC Service Components 2 - //! 3 - //! Core building blocks for implementing XRPC services in the AT Protocol ecosystem. 4 - //! Provides JWT-based authorization extractors for Axum web handlers with DID document 5 - //! verification and structured error handling. 1 + //! XRPC service components for AT Protocol with JWT authorization and DID verification. 6 2 7 3 #![warn(missing_docs)] 8 4