atproto_oauth/
resources.rs1use serde::Deserialize;
7
8use crate::errors::{AuthServerValidationError, OAuthClientError, ResourceValidationError};
9
10#[derive(Clone, Deserialize)]
14pub struct OAuthProtectedResource {
15 pub resource: String,
17 pub authorization_servers: Vec<String>,
20 #[serde(default)]
22 pub scopes_supported: Vec<String>,
23 #[serde(default)]
25 pub bearer_methods_supported: Vec<String>,
26}
27
28#[cfg_attr(debug_assertions, derive(Debug))]
32#[derive(Clone, Deserialize, Default)]
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
136pub async fn oauth_authorization_server(
145 http_client: &reqwest::Client,
146 pds: &str,
147) -> Result<AuthorizationServer, OAuthClientError> {
148 let destination = format!("{}/.well-known/oauth-authorization-server", pds);
149
150 let resource: AuthorizationServer = http_client
151 .get(destination)
152 .send()
153 .await
154 .map_err(OAuthClientError::AuthorizationServerRequestFailed)?
155 .json()
156 .await
157 .map_err(OAuthClientError::MalformedAuthorizationServerResponse)?;
158
159 if resource.issuer.is_empty() {
163 return Err(OAuthClientError::InvalidAuthorizationServerResponse(
164 AuthServerValidationError::IssuerMustMatchPds.into(),
165 ));
166 }
167 if resource.authorization_endpoint.is_empty() {
168 return Err(OAuthClientError::InvalidAuthorizationServerResponse(
169 AuthServerValidationError::RequiredServerFeaturesMustBeSupported.into(),
170 ));
171 }
172 if resource.token_endpoint.is_empty() {
173 return Err(OAuthClientError::InvalidAuthorizationServerResponse(
174 AuthServerValidationError::RequiredServerFeaturesMustBeSupported.into(),
175 ));
176 }
177
178 if resource.issuer != pds {
179 return Err(OAuthClientError::InvalidAuthorizationServerResponse(
180 AuthServerValidationError::IssuerMustMatchPds.into(),
181 ));
182 }
183
184 resource
185 .response_types_supported
186 .iter()
187 .find(|&x| x == "code")
188 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
189 AuthServerValidationError::ResponseTypesSupportMustIncludeCode.into(),
190 ))?;
191
192 resource
193 .grant_types_supported
194 .iter()
195 .find(|&x| x == "authorization_code")
196 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
197 AuthServerValidationError::GrantTypesSupportMustIncludeAuthorizationCode.into(),
198 ))?;
199 resource
200 .grant_types_supported
201 .iter()
202 .find(|&x| x == "refresh_token")
203 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
204 AuthServerValidationError::GrantTypesSupportMustIncludeRefreshToken.into(),
205 ))?;
206 resource
207 .code_challenge_methods_supported
208 .iter()
209 .find(|&x| x == "S256")
210 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
211 AuthServerValidationError::CodeChallengeMethodsSupportedMustIncludeS256.into(),
212 ))?;
213 resource
214 .token_endpoint_auth_methods_supported
215 .iter()
216 .find(|&x| x == "none")
217 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
218 AuthServerValidationError::TokenEndpointAuthMethodsSupportedMustIncludeNone.into(),
219 ))?;
220 resource
221 .token_endpoint_auth_methods_supported
222 .iter()
223 .find(|&x| x == "private_key_jwt")
224 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
225 AuthServerValidationError::TokenEndpointAuthMethodsSupportedMustIncludePrivateKeyJwt
226 .into(),
227 ))?;
228 resource
229 .token_endpoint_auth_signing_alg_values_supported
230 .iter()
231 .find(|&x| x == "ES256")
232 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
233 AuthServerValidationError::TokenEndpointAuthSigningAlgValuesMustIncludeES256.into(),
234 ))?;
235 resource
236 .scopes_supported
237 .iter()
238 .find(|&x| x == "atproto")
239 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
240 AuthServerValidationError::ScopesSupportedMustIncludeAtProto.into(),
241 ))?;
242 resource
243 .scopes_supported
244 .iter()
245 .find(|&x| x == "transition:generic")
246 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
247 AuthServerValidationError::ScopesSupportedMustIncludeTransitionGeneric.into(),
248 ))?;
249 resource
250 .dpop_signing_alg_values_supported
251 .iter()
252 .find(|&x| x == "ES256")
253 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
254 AuthServerValidationError::DpopSigningAlgValuesSupportedMustIncludeES256.into(),
255 ))?;
256
257 if !(resource.authorization_response_iss_parameter_supported
258 && resource.require_pushed_authorization_requests
259 && resource.client_id_metadata_document_supported)
260 {
261 return Err(OAuthClientError::InvalidAuthorizationServerResponse(
262 AuthServerValidationError::RequiredServerFeaturesMustBeSupported.into(),
263 ));
264 }
265
266 Ok(resource)
267}