atproto_oauth/
resources.rs1use serde::Deserialize;
8
9use crate::errors::{AuthServerValidationError, OAuthClientError, ResourceValidationError};
10
11#[derive(Clone, Deserialize)]
15pub struct OAuthProtectedResource {
16 pub resource: String,
18 pub authorization_servers: Vec<String>,
21 #[serde(default)]
23 pub scopes_supported: Vec<String>,
24 #[serde(default)]
26 pub bearer_methods_supported: Vec<String>,
27}
28
29#[derive(Clone, Deserialize, Default, Debug)]
33pub struct AuthorizationServer {
34 #[serde(default)]
36 pub introspection_endpoint: String,
37 pub authorization_endpoint: String,
39 #[serde(default)]
41 pub authorization_response_iss_parameter_supported: bool,
42 #[serde(default)]
44 pub client_id_metadata_document_supported: bool,
45 #[serde(default)]
47 pub code_challenge_methods_supported: Vec<String>,
48 #[serde(default)]
50 pub dpop_signing_alg_values_supported: Vec<String>,
51 #[serde(default)]
53 pub grant_types_supported: Vec<String>,
54 pub issuer: String,
56 #[serde(default)]
58 pub pushed_authorization_request_endpoint: String,
59 #[serde(default)]
61 pub request_parameter_supported: bool,
62 #[serde(default)]
64 pub require_pushed_authorization_requests: bool,
65 #[serde(default)]
67 pub response_types_supported: Vec<String>,
68 #[serde(default)]
70 pub scopes_supported: Vec<String>,
71 #[serde(default)]
73 pub token_endpoint_auth_methods_supported: Vec<String>,
74 #[serde(default)]
76 pub token_endpoint_auth_signing_alg_values_supported: Vec<String>,
77 pub token_endpoint: String,
79}
80
81pub async fn pds_resources(
86 http_client: &reqwest::Client,
87 pds: &str,
88) -> Result<(OAuthProtectedResource, AuthorizationServer), OAuthClientError> {
89 let protected_resource = oauth_protected_resource(http_client, pds).await?;
90
91 let first_authorization_server = protected_resource
92 .authorization_servers
93 .first()
94 .ok_or(OAuthClientError::InvalidOAuthProtectedResource)?;
95
96 let authorization_server =
97 oauth_authorization_server(http_client, first_authorization_server).await?;
98 Ok((protected_resource, authorization_server))
99}
100
101pub async fn oauth_protected_resource(
107 http_client: &reqwest::Client,
108 pds: &str,
109) -> Result<OAuthProtectedResource, OAuthClientError> {
110 let destination = format!("{}/.well-known/oauth-protected-resource", pds);
111
112 let resource: OAuthProtectedResource = http_client
113 .get(destination)
114 .send()
115 .await
116 .map_err(OAuthClientError::OAuthProtectedResourceRequestFailed)?
117 .json()
118 .await
119 .map_err(OAuthClientError::MalformedOAuthProtectedResourceResponse)?;
120
121 if resource.resource != pds {
122 return Err(OAuthClientError::InvalidOAuthProtectedResourceResponse(
123 ResourceValidationError::ResourceMustMatchPds.into(),
124 ));
125 }
126
127 if resource.authorization_servers.len() != 1 {
128 return Err(OAuthClientError::InvalidOAuthProtectedResourceResponse(
129 ResourceValidationError::AuthorizationServersMustContainExactlyOne.into(),
130 ));
131 }
132
133 Ok(resource)
134}
135
136#[tracing::instrument(skip(http_client), err)]
145pub async fn oauth_authorization_server(
146 http_client: &reqwest::Client,
147 pds: &str,
148) -> Result<AuthorizationServer, OAuthClientError> {
149 let destination = format!("{}/.well-known/oauth-authorization-server", pds);
150
151 let resource: AuthorizationServer = http_client
152 .get(destination)
153 .send()
154 .await
155 .map_err(OAuthClientError::AuthorizationServerRequestFailed)?
156 .json()
157 .await
158 .map_err(OAuthClientError::MalformedAuthorizationServerResponse)?;
159
160 if resource.issuer.is_empty() {
164 return Err(OAuthClientError::InvalidAuthorizationServerResponse(
165 AuthServerValidationError::IssuerMustMatchPds.into(),
166 ));
167 }
168 if resource.authorization_endpoint.is_empty() {
169 return Err(OAuthClientError::InvalidAuthorizationServerResponse(
170 AuthServerValidationError::RequiredServerFeaturesMustBeSupported.into(),
171 ));
172 }
173 if resource.token_endpoint.is_empty() {
174 return Err(OAuthClientError::InvalidAuthorizationServerResponse(
175 AuthServerValidationError::RequiredServerFeaturesMustBeSupported.into(),
176 ));
177 }
178
179 if resource.issuer != pds {
180 return Err(OAuthClientError::InvalidAuthorizationServerResponse(
181 AuthServerValidationError::IssuerMustMatchPds.into(),
182 ));
183 }
184
185 resource
186 .response_types_supported
187 .iter()
188 .find(|&x| x == "code")
189 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
190 AuthServerValidationError::ResponseTypesSupportMustIncludeCode.into(),
191 ))?;
192
193 resource
194 .grant_types_supported
195 .iter()
196 .find(|&x| x == "authorization_code")
197 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
198 AuthServerValidationError::GrantTypesSupportMustIncludeAuthorizationCode.into(),
199 ))?;
200 resource
201 .grant_types_supported
202 .iter()
203 .find(|&x| x == "refresh_token")
204 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
205 AuthServerValidationError::GrantTypesSupportMustIncludeRefreshToken.into(),
206 ))?;
207 resource
208 .code_challenge_methods_supported
209 .iter()
210 .find(|&x| x == "S256")
211 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
212 AuthServerValidationError::CodeChallengeMethodsSupportedMustIncludeS256.into(),
213 ))?;
214 resource
215 .token_endpoint_auth_methods_supported
216 .iter()
217 .find(|&x| x == "none")
218 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
219 AuthServerValidationError::TokenEndpointAuthMethodsSupportedMustIncludeNone.into(),
220 ))?;
221 resource
222 .token_endpoint_auth_methods_supported
223 .iter()
224 .find(|&x| x == "private_key_jwt")
225 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
226 AuthServerValidationError::TokenEndpointAuthMethodsSupportedMustIncludePrivateKeyJwt
227 .into(),
228 ))?;
229 resource
230 .token_endpoint_auth_signing_alg_values_supported
231 .iter()
232 .find(|&x| x == "ES256")
233 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
234 AuthServerValidationError::TokenEndpointAuthSigningAlgValuesMustIncludeES256.into(),
235 ))?;
236 resource
237 .scopes_supported
238 .iter()
239 .find(|&x| x == "atproto")
240 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
241 AuthServerValidationError::ScopesSupportedMustIncludeAtProto.into(),
242 ))?;
243 resource
244 .scopes_supported
245 .iter()
246 .find(|&x| x == "transition:generic")
247 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
248 AuthServerValidationError::ScopesSupportedMustIncludeTransitionGeneric.into(),
249 ))?;
250 resource
251 .dpop_signing_alg_values_supported
252 .iter()
253 .find(|&x| x == "ES256")
254 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
255 AuthServerValidationError::DpopSigningAlgValuesSupportedMustIncludeES256.into(),
256 ))?;
257
258 if !(resource.authorization_response_iss_parameter_supported
259 && resource.require_pushed_authorization_requests
260 && resource.client_id_metadata_document_supported)
261 {
262 return Err(OAuthClientError::InvalidAuthorizationServerResponse(
263 AuthServerValidationError::RequiredServerFeaturesMustBeSupported.into(),
264 ));
265 }
266
267 Ok(resource)
268}