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}