A library for ATProtocol identities.
at main 11 kB view raw
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}