use hickory_resolver::TokioResolver; use serde::Deserialize; const AUTH_SERVER_PATH: &str = "/.well-known/oauth-protected-resource"; const AS_META_PATH: &str = "/.well-known/oauth-authorization-server"; const DID_HTTP_PATH: &str = "/.well-known/atproto-did"; const DID_DNS_TXT_PREFIX: &str = "_atproto."; const PLC_DIRECTORY_URL: &str = "https://plc.directory/"; const ATPROTO_PDS: &str = "AtprotoPersonalDataServer"; #[derive(Deserialize)] struct Service { #[serde(rename = "type")] kind: String, #[serde(rename = "serviceEndpoint")] service_endpoint: String, } #[derive(Deserialize)] struct DIDDoc { service: Vec, } #[derive(Debug, Deserialize)] pub struct AuthServerMeta { pub issuer: String, pub authorization_endpoint: String, pub token_endpoint: String, pub pushed_authorization_request_endpoint: String, pub scopes_supported: Vec, pub dpop_signing_alg_values_supported: Vec, } #[derive(Deserialize)] struct ProtectedResourceMeta { authorization_servers: Vec, } #[derive(Debug)] pub struct DiscoveredSession { pub did: String, pub pds_url: String, pub auth_meta: AuthServerMeta, } // &str is a borrowed string reference, we're not writing to it pub async fn discover_from_handle(handle: &str) -> Result { let did = resolve_handle_to_did(handle) .await .map_err(|e| anyhow::anyhow!("handle resolution failed:\n> {}", e))?; // println!("resolved did: {}", did); let pds_url = resolve_did_to_pds_url(&did) .await .map_err(|e| anyhow::anyhow!("pds_url resolution failed:\n>{}", e))?; // println!("resolved pds_url: {}", pds_url); let auth_meta = extract_auth_server(&pds_url).await.map_err(|e| { anyhow::anyhow!( "auth_server_metadata couldn't get resolved for pds:\n>{}", e ) })?; let sesh = DiscoveredSession { did, pds_url, auth_meta, }; // println!("{:#?}", &sesh); return Ok(sesh); } async fn resolve_handle_to_did(handle: &str) -> Result { // dns resolution let lookup = format!("{}{}.", DID_DNS_TXT_PREFIX, handle); let resolver = TokioResolver::builder_tokio()?.build(); if let Ok(records) = resolver.txt_lookup(lookup).await { for record in records { let record_str = record.to_string(); if let Some(did) = record_str.strip_prefix("did=") { if !did.starts_with("did:") { return Err(anyhow::anyhow!("invalid DID in DNS record: {}", did)); } return Ok(did.to_string()); } } } // http fallback let d = format!("https://{}{}", handle, DID_HTTP_PATH); let client = reqwest::Client::new(); let response = client.get(&d).send().await; if let Ok(resp) = response { if resp.status().is_success() { let did = resp.text().await?.trim().to_string(); if did.starts_with("did:") { return Ok(did); } } } return Err(anyhow::anyhow!( "could not resolve DID for handle {}", handle )); } async fn resolve_did_to_pds_url(did: &str) -> Result { let d = format!("{}{}", PLC_DIRECTORY_URL, did); let client = reqwest::Client::new(); let response = client.get(&d).send().await?; let doc: DIDDoc = response.json().await?; for service in doc.service { if service.kind == ATPROTO_PDS { return Ok(service.service_endpoint); } } return Err(anyhow::anyhow!("could not resolve PDS_URL for did {}", did)); } async fn extract_auth_server(pds: &str) -> Result { let u = format!("{}{}", pds, AUTH_SERVER_PATH); let client = reqwest::Client::new(); let response = client.get(&u).send().await?; let prm: ProtectedResourceMeta = response.json().await?; let as_url = prm.authorization_servers.first().ok_or_else(|| { anyhow::anyhow!("no authorization servers in protected resource metadata") })?; fetch_auth_server_meta(as_url).await } pub async fn fetch_auth_server_meta(as_url: &str) -> Result { let m = format!("{}{}", as_url, AS_META_PATH); let client = reqwest::Client::new(); let response = client.get(&m).send().await?; let as_meta: AuthServerMeta = response.json().await?; Ok(as_meta) }