atproto_oauth/
resources.rs

1//! OAuth resource discovery and validation for AT Protocol Personal Data Servers.
2//!
3//! This module provides functionality to discover and validate OAuth 2.0 configuration
4//! from AT Protocol Personal Data Servers (PDS) using RFC 8414 well-known endpoints
5//! with AT Protocol specific requirements and validations.
6
7use serde::Deserialize;
8
9use crate::errors::{AuthServerValidationError, OAuthClientError, ResourceValidationError};
10
11/// OAuth 2.0 protected resource metadata from RFC 8414 oauth-protected-resource endpoint.
12///
13/// AT Protocol requires that the authorization_servers array contains exactly one URL.
14#[derive(Clone, Deserialize)]
15pub struct OAuthProtectedResource {
16    /// The protected resource URI, must match the PDS base URL.
17    pub resource: String,
18    /// Authorization server URLs that can issue tokens for this resource.
19    /// AT Protocol requires exactly one authorization server URL.
20    pub authorization_servers: Vec<String>,
21    /// OAuth 2.0 scopes supported by this protected resource.
22    #[serde(default)]
23    pub scopes_supported: Vec<String>,
24    /// Bearer token methods supported (e.g., "header", "body", "query").
25    #[serde(default)]
26    pub bearer_methods_supported: Vec<String>,
27}
28
29/// OAuth 2.0 authorization server metadata from RFC 8414 oauth-authorization-server endpoint.
30///
31/// AT Protocol requires specific grant types, scopes, authentication methods, and security features.
32#[cfg_attr(debug_assertions, derive(Debug))]
33#[derive(Clone, Deserialize, Default)]
34pub struct AuthorizationServer {
35    /// URL of the authorization server's token introspection endpoint (optional).
36    #[serde(default)]
37    pub introspection_endpoint: String,
38    /// URL of the authorization server's authorization endpoint.
39    pub authorization_endpoint: String,
40    /// Whether the authorization response `iss` parameter is supported (required for AT Protocol).
41    #[serde(default)]
42    pub authorization_response_iss_parameter_supported: bool,
43    /// Whether client ID metadata document is supported (required for AT Protocol).
44    #[serde(default)]
45    pub client_id_metadata_document_supported: bool,
46    /// PKCE code challenge methods supported, must include "S256" for AT Protocol.
47    #[serde(default)]
48    pub code_challenge_methods_supported: Vec<String>,
49    /// DPoP proof JWT signing algorithms supported, must include "ES256" for AT Protocol.
50    #[serde(default)]
51    pub dpop_signing_alg_values_supported: Vec<String>,
52    /// OAuth 2.0 grant types supported, must include "authorization_code" and "refresh_token".
53    #[serde(default)]
54    pub grant_types_supported: Vec<String>,
55    /// The authorization server's issuer identifier, must match PDS URL.
56    pub issuer: String,
57    /// URL of the authorization server's pushed authorization request endpoint (required for AT Protocol).
58    #[serde(default)]
59    pub pushed_authorization_request_endpoint: String,
60    /// Whether the `request` parameter is supported (optional).
61    #[serde(default)]
62    pub request_parameter_supported: bool,
63    /// Whether pushed authorization requests are required (required for AT Protocol).
64    #[serde(default)]
65    pub require_pushed_authorization_requests: bool,
66    /// OAuth 2.0 response types supported, must include "code" for AT Protocol.
67    #[serde(default)]
68    pub response_types_supported: Vec<String>,
69    /// OAuth 2.0 scopes supported, must include "atproto" and "transition:generic" for AT Protocol.
70    #[serde(default)]
71    pub scopes_supported: Vec<String>,
72    /// Client authentication methods supported, must include "none" and "private_key_jwt".
73    #[serde(default)]
74    pub token_endpoint_auth_methods_supported: Vec<String>,
75    /// JWT signing algorithms for client authentication, must include "ES256" for AT Protocol.
76    #[serde(default)]
77    pub token_endpoint_auth_signing_alg_values_supported: Vec<String>,
78    /// URL of the authorization server's token endpoint.
79    pub token_endpoint: String,
80}
81
82/// Retrieves and validates OAuth configuration from a Personal Data Server (PDS).
83///
84/// Fetches both the protected resource metadata and authorization server metadata
85/// from the PDS's well-known OAuth endpoints, returning the first authorization server.
86pub async fn pds_resources(
87    http_client: &reqwest::Client,
88    pds: &str,
89) -> Result<(OAuthProtectedResource, AuthorizationServer), OAuthClientError> {
90    let protected_resource = oauth_protected_resource(http_client, pds).await?;
91
92    let first_authorization_server = protected_resource
93        .authorization_servers
94        .first()
95        .ok_or(OAuthClientError::InvalidOAuthProtectedResource)?;
96
97    let authorization_server =
98        oauth_authorization_server(http_client, first_authorization_server).await?;
99    Ok((protected_resource, authorization_server))
100}
101
102/// Fetches and validates protected resource metadata from a PDS's well-known endpoint.
103///
104/// Retrieves OAuth 2.0 protected resource configuration from `/.well-known/oauth-protected-resource`
105/// and validates that the resource URI matches the PDS URL and has exactly one authorization server
106/// as required by AT Protocol specification.
107pub async fn oauth_protected_resource(
108    http_client: &reqwest::Client,
109    pds: &str,
110) -> Result<OAuthProtectedResource, OAuthClientError> {
111    let destination = format!("{}/.well-known/oauth-protected-resource", pds);
112
113    let resource: OAuthProtectedResource = http_client
114        .get(destination)
115        .send()
116        .await
117        .map_err(OAuthClientError::OAuthProtectedResourceRequestFailed)?
118        .json()
119        .await
120        .map_err(OAuthClientError::MalformedOAuthProtectedResourceResponse)?;
121
122    if resource.resource != pds {
123        return Err(OAuthClientError::InvalidOAuthProtectedResourceResponse(
124            ResourceValidationError::ResourceMustMatchPds.into(),
125        ));
126    }
127
128    if resource.authorization_servers.len() != 1 {
129        return Err(OAuthClientError::InvalidOAuthProtectedResourceResponse(
130            ResourceValidationError::AuthorizationServersMustContainExactlyOne.into(),
131        ));
132    }
133
134    Ok(resource)
135}
136
137/// Fetches and validates authorization server metadata from a PDS's well-known endpoint.
138///
139/// Retrieves OAuth 2.0 authorization server configuration from `/.well-known/oauth-authorization-server`
140/// and validates AT Protocol requirements including:
141/// - Required grant types: authorization_code, refresh_token  
142/// - Required scopes: atproto, transition:generic
143/// - Required security features: PKCE (S256), DPoP (ES256), PAR
144/// - Required authentication methods: none, private_key_jwt
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    // Validate AT Protocol requirements for authorization server metadata
161
162    // Validate required fields are not empty
163    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}