A library for ATProtocol identities.
1//! Example XRPC service with DID:web identity and JWT authentication. 2 3use anyhow::Result; 4use async_trait::async_trait; 5use atproto_identity::resolve::SharedIdentityResolver; 6use atproto_identity::{ 7 config::{CertificateBundles, DnsNameservers, default_env, optional_env, require_env, version}, 8 key::{KeyData, KeyResolver, identify_key, to_public}, 9 resolve::{HickoryDnsResolver, IdentityResolver, InnerIdentityResolver}, 10}; 11use atproto_xrpcs::authorization::Authorization; 12use axum::{ 13 Json, Router, 14 extract::{FromRef, Query, State}, 15 response::{Html, IntoResponse, Response}, 16 routing::get, 17}; 18use clap::Parser; 19use http::{HeaderMap, StatusCode}; 20use serde::Deserialize; 21use serde_json::json; 22use std::{collections::HashMap, ops::Deref, sync::Arc}; 23 24#[derive(Clone)] 25pub struct SimpleKeyResolver { 26 keys: HashMap<String, KeyData>, 27} 28 29impl Default for SimpleKeyResolver { 30 fn default() -> Self { 31 Self::new() 32 } 33} 34 35impl SimpleKeyResolver { 36 pub fn new() -> Self { 37 Self { 38 keys: HashMap::new(), 39 } 40 } 41} 42 43#[async_trait] 44impl KeyResolver for SimpleKeyResolver { 45 async fn resolve(&self, key: &str) -> anyhow::Result<KeyData> { 46 if let Some(key_data) = self.keys.get(key) { 47 Ok(key_data.clone()) 48 } else { 49 identify_key(key).map_err(Into::into) 50 } 51 } 52} 53 54#[derive(Clone)] 55pub struct ServiceDocument(pub serde_json::Value); 56 57#[derive(Clone)] 58pub struct ServiceDID(pub String); 59 60pub struct InnerWebContext { 61 pub http_client: reqwest::Client, 62 pub key_resolver: Arc<dyn KeyResolver>, 63 pub service_document: ServiceDocument, 64 pub service_did: ServiceDID, 65 pub identity_resolver: Arc<dyn IdentityResolver>, 66} 67 68#[derive(Clone, FromRef)] 69pub struct WebContext(pub Arc<InnerWebContext>); 70 71impl Deref for WebContext { 72 type Target = InnerWebContext; 73 74 fn deref(&self) -> &Self::Target { 75 &self.0 76 } 77} 78 79impl FromRef<WebContext> for reqwest::Client { 80 fn from_ref(context: &WebContext) -> Self { 81 context.0.http_client.clone() 82 } 83} 84 85impl FromRef<WebContext> for ServiceDocument { 86 fn from_ref(context: &WebContext) -> Self { 87 context.0.service_document.clone() 88 } 89} 90 91impl FromRef<WebContext> for ServiceDID { 92 fn from_ref(context: &WebContext) -> Self { 93 context.0.service_did.clone() 94 } 95} 96 97impl FromRef<WebContext> for Arc<dyn KeyResolver> { 98 fn from_ref(context: &WebContext) -> Self { 99 context.0.key_resolver.clone() 100 } 101} 102 103impl FromRef<WebContext> for Arc<dyn IdentityResolver> { 104 fn from_ref(context: &WebContext) -> Self { 105 context.0.identity_resolver.clone() 106 } 107} 108 109/// AT Protocol XRPC Hello World Service 110#[derive(Parser)] 111#[command( 112 name = "atproto-xrpcs-helloworld", 113 version, 114 about = "AT Protocol XRPC Hello World demonstration service", 115 long_about = " 116A demonstration XRPC service implementation showcasing the AT Protocol ecosystem. 117This service provides a simple \"Hello, World!\" endpoint that supports both 118authenticated and unauthenticated requests. 119 120FEATURES: 121 - AT Protocol identity resolution and DID document management 122 - XRPC service endpoint with optional authentication 123 - DID:web identity publishing via .well-known endpoints 124 - JWT-based request authentication using AT Protocol standards 125 126ENVIRONMENT VARIABLES: 127 SERVICE_KEY Private key for service identity (required) 128 EXTERNAL_BASE External hostname for service endpoints (required) 129 PORT HTTP server port (default: 8080) 130 PLC_HOSTNAME PLC directory hostname (default: plc.directory) 131 USER_AGENT HTTP User-Agent header (auto-generated) 132 DNS_NAMESERVERS Custom DNS nameservers (optional) 133 CERTIFICATE_BUNDLES Additional CA certificates (optional) 134 135ENDPOINTS: 136 GET / HTML index page 137 GET /.well-known/did.json DID document (DID:web) 138 GET /.well-known/atproto-did AT Protocol DID identifier 139 GET /xrpc/.../Hello Hello World XRPC endpoint 140" 141)] 142struct Args {} 143 144#[tokio::main] 145async fn main() -> Result<()> { 146 let _args = Args::parse(); 147 148 let plc_hostname = default_env("PLC_HOSTNAME", "plc.directory"); 149 let certificate_bundles: CertificateBundles = optional_env("CERTIFICATE_BUNDLES").try_into()?; 150 let default_user_agent = format!( 151 "atproto-identity-rs ({}; +https://tangled.sh/@smokesignal.events/atproto-identity-rs)", 152 version()? 153 ); 154 let user_agent = default_env("USER_AGENT", &default_user_agent); 155 let dns_nameservers: DnsNameservers = optional_env("DNS_NAMESERVERS").try_into()?; 156 157 let mut client_builder = reqwest::Client::builder(); 158 for ca_certificate in certificate_bundles.as_ref() { 159 let cert = std::fs::read(ca_certificate)?; 160 let cert = reqwest::Certificate::from_pem(&cert)?; 161 client_builder = client_builder.add_root_certificate(cert); 162 } 163 164 client_builder = client_builder.user_agent(user_agent); 165 let http_client = client_builder.build()?; 166 167 let dns_resolver = HickoryDnsResolver::create_resolver(dns_nameservers.as_ref()); 168 169 let external_base = require_env("EXTERNAL_BASE")?; 170 let port = default_env("PORT", "8080"); 171 let service_did = format!("did:web:{}", external_base); 172 173 let private_service_key = require_env("SERVICE_KEY")?; 174 let private_service_key_data = identify_key(&private_service_key)?; 175 let public_service_key_data = to_public(&private_service_key_data)?; 176 let public_service_key = public_service_key_data.to_string(); 177 178 let signing_key_storage = HashMap::from_iter(vec![( 179 public_service_key.clone(), 180 private_service_key_data.clone(), 181 )]); 182 183 let service_document = ServiceDocument(json!({ 184 "@context": vec!["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"], 185 "id": service_did, 186 "verificationMethod":[{ 187 "id": format!("{service_did}#atproto"), 188 "type":"Multikey", 189 "controller": service_did, 190 "publicKeyMultibase": public_service_key 191 }], 192 "service":[{ 193 "id":"#helloworld", 194 "type":"HelloWorldService", 195 "serviceEndpoint":format!("https://{external_base}") 196 }] 197 } 198 )); 199 200 let service_did = ServiceDID(service_did); 201 202 let identity_resolver = Arc::new(SharedIdentityResolver(Arc::new(InnerIdentityResolver { 203 dns_resolver: Arc::new(dns_resolver), 204 http_client: http_client.clone(), 205 plc_hostname, 206 }))); 207 208 let web_context = WebContext(Arc::new(InnerWebContext { 209 http_client: http_client.clone(), 210 key_resolver: Arc::new(SimpleKeyResolver { 211 keys: signing_key_storage, 212 }), 213 service_document, 214 service_did, 215 identity_resolver, 216 })); 217 218 let router = Router::new() 219 .route("/", get(handle_index)) 220 .route("/.well-known/did.json", get(handle_wellknown_did_web)) 221 .route( 222 "/.well-known/atproto-did", 223 get(handle_wellknown_atproto_did), 224 ) 225 .route( 226 "/xrpc/garden.lexicon.ngerakines.helloworld.Hello", 227 get(handle_xrpc_hello_world), 228 ) 229 .with_state(web_context); 230 231 let bind_address = format!("0.0.0.0:{}", port); 232 let listener = tokio::net::TcpListener::bind(&bind_address).await?; 233 234 // Start the web server in the background 235 let server_handle = tokio::spawn(async move { 236 if let Err(e) = axum::serve(listener, router).await { 237 eprintln!("Server error: {}", e); 238 } 239 }); 240 241 println!( 242 "XRPC Hello World service started on http://0.0.0.0:{}", 243 port 244 ); 245 246 // Keep the server running 247 server_handle.await.unwrap(); 248 249 Ok(()) 250} 251 252async fn handle_index() -> Html<&'static str> { 253 Html("<html><body><h1>Hello, World!</h1></body></html>") 254} 255 256// /.well-known/did.json 257async fn handle_wellknown_did_web( 258 service_document: State<ServiceDocument>, 259) -> Json<serde_json::Value> { 260 Json(service_document.0.0) 261} 262 263// /.well-known/atproto-did 264async fn handle_wellknown_atproto_did(service_did: State<ServiceDID>) -> Response { 265 (StatusCode::OK, service_did.0.0).into_response() 266} 267 268#[derive(Deserialize)] 269struct HelloParameters { 270 subject: Option<String>, 271} 272 273// /xrpc/garden.lexicon.ngerakines.helloworld.Hello 274async fn handle_xrpc_hello_world( 275 parameters: Query<HelloParameters>, 276 headers: HeaderMap, 277 authorization: Option<Authorization>, 278) -> Json<serde_json::Value> { 279 println!("headers {headers:?}"); 280 let subject = parameters.subject.as_deref().unwrap_or("World"); 281 let message = if authorization.is_none() { 282 format!("Hello, {subject}!") 283 } else { 284 format!("Hello, authenticated {subject}!") 285 }; 286 Json(json!({ "message": message})) 287}