1use crate::types::{OAuthAuthorizationServerMetadata, OAuthProtectedResourceMetadata};
2use http::{Request, StatusCode};
3use jacquard_common::IntoStatic;
4use jacquard_common::types::did_doc::DidDocument;
5use jacquard_common::types::ident::AtIdentifier;
6use jacquard_common::{http_client::HttpClient, types::did::Did};
7use jacquard_identity::resolver::{IdentityError, IdentityResolver};
8use url::Url;
9
10#[derive(thiserror::Error, Debug, miette::Diagnostic)]
11pub enum ResolverError {
12 #[error("resource not found")]
13 NotFound,
14 #[error("invalid at identifier: {0}")]
15 AtIdentifier(String),
16 #[error("invalid did: {0}")]
17 Did(String),
18 #[error("invalid did document: {0}")]
19 DidDocument(String),
20 #[error("protected resource metadata is invalid: {0}")]
21 ProtectedResourceMetadata(String),
22 #[error("authorization server metadata is invalid: {0}")]
23 AuthorizationServerMetadata(String),
24 #[error("error resolving identity: {0}")]
25 IdentityResolverError(#[from] IdentityError),
26 #[error("unsupported did method: {0:?}")]
27 UnsupportedDidMethod(Did<'static>),
28 #[error(transparent)]
29 Http(#[from] http::Error),
30 #[error("http client error: {0}")]
31 HttpClient(Box<dyn std::error::Error + Send + Sync + 'static>),
32 #[error("http status: {0:?}")]
33 HttpStatus(StatusCode),
34 #[error(transparent)]
35 SerdeJson(#[from] serde_json::Error),
36 #[error(transparent)]
37 SerdeHtmlForm(#[from] serde_html_form::ser::Error),
38 #[error(transparent)]
39 Uri(#[from] url::ParseError),
40}
41
42#[async_trait::async_trait]
43pub trait OAuthResolver: IdentityResolver + HttpClient {
44 async fn verify_issuer(
45 &self,
46 server_metadata: &OAuthAuthorizationServerMetadata<'_>,
47 sub: &Did<'_>,
48 ) -> Result<Url, ResolverError> {
49 let (metadata, identity) = self.resolve_from_identity(sub).await?;
50 if metadata.issuer != server_metadata.issuer {
51 return Err(ResolverError::Did(format!("DIDs did not match")));
52 }
53 Ok(identity
54 .pds_endpoint()
55 .ok_or(ResolverError::DidDocument(format!("{:?}", identity).into()))?)
56 }
57 async fn resolve_oauth(
58 &self,
59 input: &str,
60 ) -> Result<
61 (
62 OAuthAuthorizationServerMetadata<'static>,
63 Option<DidDocument<'static>>,
64 ),
65 ResolverError,
66 > {
67 // Allow using an entryway, or PDS url, directly as login input (e.g.
68 // when the user forgot their handle, or when the handle does not
69 // resolve to a DID)
70 Ok(if input.starts_with("https://") {
71 let url = Url::parse(input).map_err(|_| ResolverError::NotFound)?;
72 (self.resolve_from_service(&url).await?, None)
73 } else {
74 let (metadata, identity) = self.resolve_from_identity(input).await?;
75 (metadata, Some(identity))
76 })
77 }
78 async fn resolve_from_service(
79 &self,
80 input: &Url,
81 ) -> Result<OAuthAuthorizationServerMetadata<'static>, ResolverError> {
82 // Assume first that input is a PDS URL (as required by ATPROTO)
83 if let Ok(metadata) = self.get_resource_server_metadata(input).await {
84 return Ok(metadata);
85 }
86 // Fallback to trying to fetch as an issuer (Entryway)
87 self.get_authorization_server_metadata(input).await
88 }
89 async fn resolve_from_identity(
90 &self,
91 input: &str,
92 ) -> Result<
93 (
94 OAuthAuthorizationServerMetadata<'static>,
95 DidDocument<'static>,
96 ),
97 ResolverError,
98 > {
99 let actor = AtIdentifier::new(input)
100 .map_err(|e| ResolverError::AtIdentifier(format!("{:?}", e)))?;
101 let identity = self.resolve_ident_owned(&actor).await?;
102 if let Some(pds) = &identity.pds_endpoint() {
103 let metadata = self.get_resource_server_metadata(pds).await?;
104 Ok((metadata, identity))
105 } else {
106 Err(ResolverError::DidDocument(format!("Did doc lacking pds")))
107 }
108 }
109 async fn get_authorization_server_metadata(
110 &self,
111 issuer: &Url,
112 ) -> Result<OAuthAuthorizationServerMetadata<'static>, ResolverError> {
113 Ok(resolve_authorization_server(self, issuer).await?)
114 }
115 async fn get_resource_server_metadata(
116 &self,
117 pds: &Url,
118 ) -> Result<OAuthAuthorizationServerMetadata<'static>, ResolverError> {
119 let rs_metadata = resolve_protected_resource_info(self, pds).await?;
120 // ATPROTO requires one, and only one, authorization server entry
121 // > That document MUST contain a single item in the authorization_servers array.
122 // https://github.com/bluesky-social/proposals/tree/main/0004-oauth#server-metadata
123 let issuer = match &rs_metadata.authorization_servers {
124 Some(servers) if !servers.is_empty() => {
125 if servers.len() > 1 {
126 return Err(ResolverError::ProtectedResourceMetadata(format!(
127 "unable to determine authorization server for PDS: {pds}"
128 )));
129 }
130 &servers[0]
131 }
132 _ => {
133 return Err(ResolverError::ProtectedResourceMetadata(format!(
134 "no authorization server found for PDS: {pds}"
135 )));
136 }
137 };
138 let as_metadata = self.get_authorization_server_metadata(issuer).await?;
139 // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-08#name-authorization-server-metada
140 if let Some(protected_resources) = &as_metadata.protected_resources {
141 if !protected_resources.contains(&rs_metadata.resource) {
142 return Err(ResolverError::AuthorizationServerMetadata(format!(
143 "pds {pds} does not protected by issuer: {issuer}",
144 )));
145 }
146 }
147
148 // TODO: atproot specific validation?
149 // https://github.com/bluesky-social/proposals/tree/main/0004-oauth#server-metadata
150 //
151 // eg.
152 // https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html
153 // if as_metadata.client_id_metadata_document_supported != Some(true) {
154 // return Err(Error::AuthorizationServerMetadata(format!(
155 // "authorization server does not support client_id_metadata_document: {issuer}"
156 // )));
157 // }
158
159 Ok(as_metadata)
160 }
161}
162
163pub async fn resolve_authorization_server<T: HttpClient + ?Sized>(
164 client: &T,
165 server: &Url,
166) -> Result<OAuthAuthorizationServerMetadata<'static>, ResolverError> {
167 let url = server
168 .join("/.well-known/oauth-authorization-server")
169 .map_err(|e| ResolverError::HttpClient(e.into()))?;
170
171 let req = Request::builder()
172 .uri(url.to_string())
173 .body(Vec::new())
174 .map_err(|e| ResolverError::HttpClient(e.into()))?;
175 let res = client
176 .send_http(req)
177 .await
178 .map_err(|e| ResolverError::HttpClient(e.into()))?;
179 if res.status() == StatusCode::OK {
180 let metadata = serde_json::from_slice::<OAuthAuthorizationServerMetadata>(res.body())
181 .map_err(ResolverError::SerdeJson)?;
182 // https://datatracker.ietf.org/doc/html/rfc8414#section-3.3
183 if metadata.issuer == server.as_str() {
184 Ok(metadata.into_static())
185 } else {
186 Err(ResolverError::AuthorizationServerMetadata(format!(
187 "invalid issuer: {}",
188 metadata.issuer
189 )))
190 }
191 } else {
192 Err(ResolverError::HttpStatus(res.status()))
193 }
194}
195
196pub async fn resolve_protected_resource_info<T: HttpClient + ?Sized>(
197 client: &T,
198 server: &Url,
199) -> Result<OAuthProtectedResourceMetadata<'static>, ResolverError> {
200 let url = server
201 .join("/.well-known/oauth-protected-resource")
202 .map_err(|e| ResolverError::HttpClient(e.into()))?;
203
204 let req = Request::builder()
205 .uri(url.to_string())
206 .body(Vec::new())
207 .map_err(|e| ResolverError::HttpClient(e.into()))?;
208 let res = client
209 .send_http(req)
210 .await
211 .map_err(|e| ResolverError::HttpClient(e.into()))?;
212 if res.status() == StatusCode::OK {
213 let metadata = serde_json::from_slice::<OAuthProtectedResourceMetadata>(res.body())
214 .map_err(ResolverError::SerdeJson)?;
215 // https://datatracker.ietf.org/doc/html/rfc8414#section-3.3
216 if metadata.resource == server.as_str() {
217 Ok(metadata.into_static())
218 } else {
219 Err(ResolverError::AuthorizationServerMetadata(format!(
220 "invalid resource: {}",
221 metadata.resource
222 )))
223 }
224 } else {
225 Err(ResolverError::HttpStatus(res.status()))
226 }
227}