use serde::Deserialize;
use crate::errors::{AuthServerValidationError, OAuthClientError, ResourceValidationError};
#[derive(Clone, Deserialize)]
pub struct OAuthProtectedResource {
pub resource: String,
pub authorization_servers: Vec<String>,
#[serde(default)]
pub scopes_supported: Vec<String>,
#[serde(default)]
pub bearer_methods_supported: Vec<String>,
}
#[cfg_attr(debug_assertions, derive(Debug))]
#[derive(Clone, Deserialize, Default)]
pub struct AuthorizationServer {
#[serde(default)]
pub introspection_endpoint: String,
pub authorization_endpoint: String,
#[serde(default)]
pub authorization_response_iss_parameter_supported: bool,
#[serde(default)]
pub client_id_metadata_document_supported: bool,
#[serde(default)]
pub code_challenge_methods_supported: Vec<String>,
#[serde(default)]
pub dpop_signing_alg_values_supported: Vec<String>,
#[serde(default)]
pub grant_types_supported: Vec<String>,
pub issuer: String,
#[serde(default)]
pub pushed_authorization_request_endpoint: String,
#[serde(default)]
pub request_parameter_supported: bool,
#[serde(default)]
pub require_pushed_authorization_requests: bool,
#[serde(default)]
pub response_types_supported: Vec<String>,
#[serde(default)]
pub scopes_supported: Vec<String>,
#[serde(default)]
pub token_endpoint_auth_methods_supported: Vec<String>,
#[serde(default)]
pub token_endpoint_auth_signing_alg_values_supported: Vec<String>,
pub token_endpoint: String,
}
pub async fn pds_resources(
http_client: &reqwest::Client,
pds: &str,
) -> Result<(OAuthProtectedResource, AuthorizationServer), OAuthClientError> {
let protected_resource = oauth_protected_resource(http_client, pds).await?;
let first_authorization_server = protected_resource
.authorization_servers
.first()
.ok_or(OAuthClientError::InvalidOAuthProtectedResource)?;
let authorization_server =
oauth_authorization_server(http_client, first_authorization_server).await?;
Ok((protected_resource, authorization_server))
}
pub async fn oauth_protected_resource(
http_client: &reqwest::Client,
pds: &str,
) -> Result<OAuthProtectedResource, OAuthClientError> {
let destination = format!("{}/.well-known/oauth-protected-resource", pds);
let resource: OAuthProtectedResource = http_client
.get(destination)
.send()
.await
.map_err(OAuthClientError::OAuthProtectedResourceRequestFailed)?
.json()
.await
.map_err(OAuthClientError::MalformedOAuthProtectedResourceResponse)?;
if resource.resource != pds {
return Err(OAuthClientError::InvalidOAuthProtectedResourceResponse(
ResourceValidationError::ResourceMustMatchPds.into(),
));
}
if resource.authorization_servers.len() != 1 {
return Err(OAuthClientError::InvalidOAuthProtectedResourceResponse(
ResourceValidationError::AuthorizationServersMustContainExactlyOne.into(),
));
}
Ok(resource)
}
pub async fn oauth_authorization_server(
http_client: &reqwest::Client,
pds: &str,
) -> Result<AuthorizationServer, OAuthClientError> {
let destination = format!("{}/.well-known/oauth-authorization-server", pds);
let resource: AuthorizationServer = http_client
.get(destination)
.send()
.await
.map_err(OAuthClientError::AuthorizationServerRequestFailed)?
.json()
.await
.map_err(OAuthClientError::MalformedAuthorizationServerResponse)?;
if resource.issuer.is_empty() {
return Err(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::IssuerMustMatchPds.into(),
));
}
if resource.authorization_endpoint.is_empty() {
return Err(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::RequiredServerFeaturesMustBeSupported.into(),
));
}
if resource.token_endpoint.is_empty() {
return Err(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::RequiredServerFeaturesMustBeSupported.into(),
));
}
if resource.issuer != pds {
return Err(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::IssuerMustMatchPds.into(),
));
}
resource
.response_types_supported
.iter()
.find(|&x| x == "code")
.ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::ResponseTypesSupportMustIncludeCode.into(),
))?;
resource
.grant_types_supported
.iter()
.find(|&x| x == "authorization_code")
.ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::GrantTypesSupportMustIncludeAuthorizationCode.into(),
))?;
resource
.grant_types_supported
.iter()
.find(|&x| x == "refresh_token")
.ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::GrantTypesSupportMustIncludeRefreshToken.into(),
))?;
resource
.code_challenge_methods_supported
.iter()
.find(|&x| x == "S256")
.ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::CodeChallengeMethodsSupportedMustIncludeS256.into(),
))?;
resource
.token_endpoint_auth_methods_supported
.iter()
.find(|&x| x == "none")
.ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::TokenEndpointAuthMethodsSupportedMustIncludeNone.into(),
))?;
resource
.token_endpoint_auth_methods_supported
.iter()
.find(|&x| x == "private_key_jwt")
.ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::TokenEndpointAuthMethodsSupportedMustIncludePrivateKeyJwt
.into(),
))?;
resource
.token_endpoint_auth_signing_alg_values_supported
.iter()
.find(|&x| x == "ES256")
.ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::TokenEndpointAuthSigningAlgValuesMustIncludeES256.into(),
))?;
resource
.scopes_supported
.iter()
.find(|&x| x == "atproto")
.ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::ScopesSupportedMustIncludeAtProto.into(),
))?;
resource
.scopes_supported
.iter()
.find(|&x| x == "transition:generic")
.ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::ScopesSupportedMustIncludeTransitionGeneric.into(),
))?;
resource
.dpop_signing_alg_values_supported
.iter()
.find(|&x| x == "ES256")
.ok_or(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::DpopSigningAlgValuesSupportedMustIncludeES256.into(),
))?;
if !(resource.authorization_response_iss_parameter_supported
&& resource.require_pushed_authorization_requests
&& resource.client_id_metadata_document_supported)
{
return Err(OAuthClientError::InvalidAuthorizationServerResponse(
AuthServerValidationError::RequiredServerFeaturesMustBeSupported.into(),
));
}
Ok(resource)
}