atproto_oauth/
resources.rs

1//! OAuth resource discovery and validation.
2//!
3//! Discover and validate OAuth 2.0 configuration from AT Protocol
4//! PDS servers using RFC 8414 well-known endpoints.
5
6use serde::Deserialize;
7
8use crate::errors::{AuthServerValidationError, OAuthClientError, ResourceValidationError};
9
10/// OAuth 2.0 protected resource metadata from RFC 8414 oauth-protected-resource endpoint.
11///
12/// AT Protocol requires that the authorization_servers array contains exactly one URL.
13#[derive(Clone, Deserialize)]
14pub struct OAuthProtectedResource {
15    /// The protected resource URI, must match the PDS base URL.
16    pub resource: String,
17    /// Authorization server URLs that can issue tokens for this resource.
18    /// AT Protocol requires exactly one authorization server URL.
19    pub authorization_servers: Vec<String>,
20    /// OAuth 2.0 scopes supported by this protected resource.
21    #[serde(default)]
22    pub scopes_supported: Vec<String>,
23    /// Bearer token methods supported (e.g., "header", "body", "query").
24    #[serde(default)]
25    pub bearer_methods_supported: Vec<String>,
26}
27
28/// OAuth 2.0 authorization server metadata from RFC 8414 oauth-authorization-server endpoint.
29///
30/// AT Protocol requires specific grant types, scopes, authentication methods, and security features.
31#[cfg_attr(debug_assertions, derive(Debug))]
32#[derive(Clone, Deserialize, Default)]
33pub struct AuthorizationServer {
34    /// URL of the authorization server's token introspection endpoint (optional).
35    #[serde(default)]
36    pub introspection_endpoint: String,
37    /// URL of the authorization server's authorization endpoint.
38    pub authorization_endpoint: String,
39    /// Whether the authorization response `iss` parameter is supported (required for AT Protocol).
40    #[serde(default)]
41    pub authorization_response_iss_parameter_supported: bool,
42    /// Whether client ID metadata document is supported (required for AT Protocol).
43    #[serde(default)]
44    pub client_id_metadata_document_supported: bool,
45    /// PKCE code challenge methods supported, must include "S256" for AT Protocol.
46    #[serde(default)]
47    pub code_challenge_methods_supported: Vec<String>,
48    /// DPoP proof JWT signing algorithms supported, must include "ES256" for AT Protocol.
49    #[serde(default)]
50    pub dpop_signing_alg_values_supported: Vec<String>,
51    /// OAuth 2.0 grant types supported, must include "authorization_code" and "refresh_token".
52    #[serde(default)]
53    pub grant_types_supported: Vec<String>,
54    /// The authorization server's issuer identifier, must match PDS URL.
55    pub issuer: String,
56    /// URL of the authorization server's pushed authorization request endpoint (required for AT Protocol).
57    #[serde(default)]
58    pub pushed_authorization_request_endpoint: String,
59    /// Whether the `request` parameter is supported (optional).
60    #[serde(default)]
61    pub request_parameter_supported: bool,
62    /// Whether pushed authorization requests are required (required for AT Protocol).
63    #[serde(default)]
64    pub require_pushed_authorization_requests: bool,
65    /// OAuth 2.0 response types supported, must include "code" for AT Protocol.
66    #[serde(default)]
67    pub response_types_supported: Vec<String>,
68    /// OAuth 2.0 scopes supported, must include "atproto" and "transition:generic" for AT Protocol.
69    #[serde(default)]
70    pub scopes_supported: Vec<String>,
71    /// Client authentication methods supported, must include "none" and "private_key_jwt".
72    #[serde(default)]
73    pub token_endpoint_auth_methods_supported: Vec<String>,
74    /// JWT signing algorithms for client authentication, must include "ES256" for AT Protocol.
75    #[serde(default)]
76    pub token_endpoint_auth_signing_alg_values_supported: Vec<String>,
77    /// URL of the authorization server's token endpoint.
78    pub token_endpoint: String,
79}
80
81/// Retrieves and validates OAuth configuration from a Personal Data Server (PDS).
82///
83/// Fetches both the protected resource metadata and authorization server metadata
84/// from the PDS's well-known OAuth endpoints, returning the first authorization server.
85pub 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
101/// Fetches and validates protected resource metadata from a PDS's well-known endpoint.
102///
103/// Retrieves OAuth 2.0 protected resource configuration from `/.well-known/oauth-protected-resource`
104/// and validates that the resource URI matches the PDS URL and has exactly one authorization server
105/// as required by AT Protocol specification.
106pub 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/// Fetches and validates authorization server metadata from a PDS's well-known endpoint.
137///
138/// Retrieves OAuth 2.0 authorization server configuration from `/.well-known/oauth-authorization-server`
139/// and validates AT Protocol requirements including:
140/// - Required grant types: authorization_code, refresh_token  
141/// - Required scopes: atproto, transition:generic
142/// - Required security features: PKCE (S256), DPoP (ES256), PAR
143/// - Required authentication methods: none, private_key_jwt
144pub 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    // Validate AT Protocol requirements for authorization server metadata
160
161    // Validate required fields are not empty
162    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}