use crate::errors::{AuthError, Result};
use crate::oauth2_server::OAuth2Server; use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthorizationServerMetadata {
pub issuer: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jwks_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registration_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scopes_supported: Option<Vec<String>>,
pub response_types_supported: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_modes_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub grant_types_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revocation_endpoint_auth_methods_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub introspection_endpoint_auth_methods_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code_challenge_methods_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revocation_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub introspection_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization_response_iss_parameter_supported: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pushed_authorization_request_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub require_pushed_authorization_requests: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub device_authorization_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dpop_signing_alg_values_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mtls_endpoint_aliases: Option<MtlsEndpointAliases>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tls_client_certificate_bound_access_tokens: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub userinfo_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject_types_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_token_signing_alg_values_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_token_encryption_alg_values_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_token_encryption_enc_values_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub claims_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub claims_parameter_supported: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_parameter_supported: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_uri_parameter_supported: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MtlsEndpointAliases {
#[serde(skip_serializing_if = "Option::is_none")]
pub token_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revocation_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub introspection_endpoint: Option<String>,
}
pub struct MetadataBuilder {
metadata: AuthorizationServerMetadata,
}
impl MetadataBuilder {
pub fn new(issuer: String) -> Self {
Self {
metadata: AuthorizationServerMetadata {
issuer,
authorization_endpoint: None,
token_endpoint: None,
jwks_uri: None,
registration_endpoint: None,
scopes_supported: None,
response_types_supported: vec!["code".to_string()],
response_modes_supported: None,
grant_types_supported: None,
token_endpoint_auth_methods_supported: None,
revocation_endpoint_auth_methods_supported: None,
introspection_endpoint_auth_methods_supported: None,
code_challenge_methods_supported: None,
revocation_endpoint: None,
introspection_endpoint: None,
authorization_response_iss_parameter_supported: None,
pushed_authorization_request_endpoint: None,
require_pushed_authorization_requests: None,
device_authorization_endpoint: None,
dpop_signing_alg_values_supported: None,
mtls_endpoint_aliases: None,
tls_client_certificate_bound_access_tokens: None,
userinfo_endpoint: None,
subject_types_supported: None,
id_token_signing_alg_values_supported: None,
id_token_encryption_alg_values_supported: None,
id_token_encryption_enc_values_supported: None,
claims_supported: None,
claims_parameter_supported: None,
request_parameter_supported: None,
request_uri_parameter_supported: None,
},
}
}
pub fn authorization_endpoint(mut self, endpoint: String) -> Self {
self.metadata.authorization_endpoint = Some(endpoint);
self
}
pub fn token_endpoint(mut self, endpoint: String) -> Self {
self.metadata.token_endpoint = Some(endpoint);
self
}
pub fn jwks_uri(mut self, uri: String) -> Self {
self.metadata.jwks_uri = Some(uri);
self
}
pub fn scopes_supported(mut self, scopes: Vec<String>) -> Self {
self.metadata.scopes_supported = Some(scopes);
self
}
pub fn response_types_supported(mut self, response_types: Vec<String>) -> Self {
self.metadata.response_types_supported = response_types;
self
}
pub fn grant_types_supported(mut self, grant_types: Vec<String>) -> Self {
self.metadata.grant_types_supported = Some(grant_types);
self
}
pub fn token_endpoint_auth_methods_supported(mut self, methods: Vec<String>) -> Self {
self.metadata.token_endpoint_auth_methods_supported = Some(methods);
self
}
pub fn code_challenge_methods_supported(mut self, methods: Vec<String>) -> Self {
self.metadata.code_challenge_methods_supported = Some(methods);
self
}
pub fn revocation_endpoint(mut self, endpoint: String) -> Self {
self.metadata.revocation_endpoint = Some(endpoint);
self
}
pub fn introspection_endpoint(mut self, endpoint: String) -> Self {
self.metadata.introspection_endpoint = Some(endpoint);
self
}
pub fn enable_par(mut self, endpoint: String, required: bool) -> Self {
self.metadata.pushed_authorization_request_endpoint = Some(endpoint);
self.metadata.require_pushed_authorization_requests = Some(required);
self
}
pub fn device_authorization_endpoint(mut self, endpoint: String) -> Self {
self.metadata.device_authorization_endpoint = Some(endpoint);
self
}
pub fn enable_dpop(mut self, signing_algorithms: Vec<String>) -> Self {
self.metadata.dpop_signing_alg_values_supported = Some(signing_algorithms);
self
}
pub fn enable_mtls(
mut self,
mtls_endpoints: MtlsEndpointAliases,
certificate_bound_tokens: bool,
) -> Self {
self.metadata.mtls_endpoint_aliases = Some(mtls_endpoints);
self.metadata.tls_client_certificate_bound_access_tokens = Some(certificate_bound_tokens);
self
}
pub fn enable_openid_connect(
mut self,
userinfo_endpoint: String,
subject_types: Vec<String>,
id_token_signing_algs: Vec<String>,
) -> Self {
self.metadata.userinfo_endpoint = Some(userinfo_endpoint);
self.metadata.subject_types_supported = Some(subject_types);
self.metadata.id_token_signing_alg_values_supported = Some(id_token_signing_algs);
self
}
pub fn build(self) -> AuthorizationServerMetadata {
self.metadata
}
}
pub struct MetadataProvider {
metadata: AuthorizationServerMetadata,
}
impl MetadataProvider {
pub fn new(metadata: AuthorizationServerMetadata) -> Self {
Self { metadata }
}
pub fn from_oauth2_server(_server: &OAuth2Server, base_url: &str) -> Result<Self> {
let mut builder = MetadataBuilder::new(base_url.to_string())
.authorization_endpoint(format!("{}/oauth2/authorize", base_url))
.token_endpoint(format!("{}/oauth2/token", base_url))
.jwks_uri(format!("{}/.well-known/jwks.json", base_url))
.response_types_supported(vec!["code".to_string()])
.grant_types_supported(vec![
"authorization_code".to_string(),
"client_credentials".to_string(),
"refresh_token".to_string(),
])
.token_endpoint_auth_methods_supported(vec![
"client_secret_basic".to_string(),
"client_secret_post".to_string(),
])
.code_challenge_methods_supported(vec!["S256".to_string()])
.revocation_endpoint(format!("{}/oauth2/revoke", base_url))
.introspection_endpoint(format!("{}/oauth2/introspect", base_url))
.scopes_supported(vec![
"openid".to_string(),
"profile".to_string(),
"email".to_string(),
"address".to_string(),
"phone".to_string(),
]);
builder = builder
.device_authorization_endpoint(format!("{}/oauth2/device_authorization", base_url));
builder = builder.enable_par(format!("{}/oauth2/par", base_url), false);
builder = builder.enable_dpop(vec![
"ES256".to_string(),
"ES384".to_string(),
"ES512".to_string(),
"RS256".to_string(),
]);
let mtls_endpoints = MtlsEndpointAliases {
token_endpoint: Some(format!("{}/oauth2/token", base_url)),
revocation_endpoint: Some(format!("{}/oauth2/revoke", base_url)),
introspection_endpoint: Some(format!("{}/oauth2/introspect", base_url)),
};
builder = builder.enable_mtls(mtls_endpoints, true);
Ok(Self::new(builder.build()))
}
pub fn from_oauth21_server(_server: &OAuth2Server, base_url: &str) -> Result<Self> {
let mut builder = MetadataBuilder::new(base_url.to_string())
.authorization_endpoint(format!("{}/oauth2/authorize", base_url))
.token_endpoint(format!("{}/oauth2/token", base_url))
.jwks_uri(format!("{}/.well-known/jwks.json", base_url))
.response_types_supported(vec!["code".to_string()]) .grant_types_supported(vec![
"authorization_code".to_string(),
"client_credentials".to_string(),
"refresh_token".to_string(),
])
.token_endpoint_auth_methods_supported(vec![
"client_secret_basic".to_string(),
"client_secret_post".to_string(),
"tls_client_auth".to_string(),
"self_signed_tls_client_auth".to_string(),
])
.code_challenge_methods_supported(vec!["S256".to_string()]) .revocation_endpoint(format!("{}/oauth2/revoke", base_url))
.introspection_endpoint(format!("{}/oauth2/introspect", base_url))
.scopes_supported(vec![
"openid".to_string(),
"profile".to_string(),
"email".to_string(),
]);
builder = builder.enable_par(format!("{}/oauth2/par", base_url), true);
builder = builder.enable_dpop(vec![
"ES256".to_string(),
"ES384".to_string(),
"ES512".to_string(),
]);
let mtls_endpoints = MtlsEndpointAliases {
token_endpoint: Some(format!("{}/oauth2/token", base_url)),
revocation_endpoint: Some(format!("{}/oauth2/revoke", base_url)),
introspection_endpoint: Some(format!("{}/oauth2/introspect", base_url)),
};
builder = builder.enable_mtls(mtls_endpoints, true);
Ok(Self::new(builder.build()))
}
pub fn get_metadata(&self) -> &AuthorizationServerMetadata {
&self.metadata
}
pub fn get_metadata_json(&self) -> Result<String> {
serde_json::to_string_pretty(&self.metadata).map_err(|e| {
AuthError::auth_method("metadata", format!("Failed to serialize metadata: {}", e))
})
}
pub fn validate(&self) -> Result<()> {
let mut errors = Vec::new();
if self.metadata.issuer.is_empty() {
errors.push("Issuer is required");
}
if self.metadata.response_types_supported.is_empty() {
errors.push("At least one response type must be supported");
}
let endpoints = [
&self.metadata.authorization_endpoint,
&self.metadata.token_endpoint,
&self.metadata.jwks_uri,
&self.metadata.revocation_endpoint,
&self.metadata.introspection_endpoint,
];
for endpoint in endpoints.iter().filter_map(|ep| ep.as_ref()) {
if url::Url::parse(endpoint).is_err() {
errors.push("Invalid endpoint URL format");
}
}
if self
.metadata
.code_challenge_methods_supported
.as_ref()
.is_some_and(|methods| methods.len() == 1 && methods[0] == "S256")
{
if self.metadata.response_types_supported.len() != 1
|| self.metadata.response_types_supported[0] != "code"
{
errors.push("OAuth 2.1 must only support 'code' response type");
}
}
if !errors.is_empty() {
return Err(AuthError::auth_method("metadata", errors.join(", ")));
}
Ok(())
}
pub fn update_metadata<F>(&mut self, updater: F) -> Result<()>
where
F: FnOnce(&mut AuthorizationServerMetadata),
{
updater(&mut self.metadata);
self.validate()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metadata_builder() {
let metadata = MetadataBuilder::new("https://auth.example.com".to_string())
.authorization_endpoint("https://auth.example.com/oauth2/authorize".to_string())
.token_endpoint("https://auth.example.com/oauth2/token".to_string())
.grant_types_supported(vec!["authorization_code".to_string()])
.code_challenge_methods_supported(vec!["S256".to_string()])
.build();
assert_eq!(metadata.issuer, "https://auth.example.com");
assert_eq!(
metadata.authorization_endpoint,
Some("https://auth.example.com/oauth2/authorize".to_string())
);
assert_eq!(
metadata.grant_types_supported,
Some(vec!["authorization_code".to_string()])
);
}
#[test]
fn test_metadata_provider() {
let metadata = MetadataBuilder::new("https://auth.example.com".to_string())
.authorization_endpoint("https://auth.example.com/oauth2/authorize".to_string())
.token_endpoint("https://auth.example.com/oauth2/token".to_string())
.build();
let provider = MetadataProvider::new(metadata);
let json = provider.get_metadata_json().unwrap();
assert!(json.contains("https://auth.example.com"));
assert!(json.contains("authorization_endpoint"));
}
#[test]
fn test_metadata_validation() {
let metadata = MetadataBuilder::new("https://auth.example.com".to_string())
.authorization_endpoint("https://auth.example.com/oauth2/authorize".to_string())
.token_endpoint("https://auth.example.com/oauth2/token".to_string())
.build();
let provider = MetadataProvider::new(metadata);
provider.validate().unwrap();
}
#[test]
fn test_oauth21_specific_metadata() {
let metadata = MetadataBuilder::new("https://auth.example.com".to_string())
.response_types_supported(vec!["code".to_string()])
.code_challenge_methods_supported(vec!["S256".to_string()])
.enable_par("https://auth.example.com/oauth2/par".to_string(), true)
.enable_dpop(vec!["ES256".to_string()])
.build();
let provider = MetadataProvider::new(metadata);
provider.validate().unwrap();
let metadata = provider.get_metadata();
assert_eq!(metadata.require_pushed_authorization_requests, Some(true));
assert!(metadata.dpop_signing_alg_values_supported.is_some());
}
#[test]
fn test_mtls_metadata() {
let mtls_endpoints = MtlsEndpointAliases {
token_endpoint: Some("https://mtls.auth.example.com/oauth2/token".to_string()),
revocation_endpoint: Some("https://mtls.auth.example.com/oauth2/revoke".to_string()),
introspection_endpoint: Some(
"https://mtls.auth.example.com/oauth2/introspect".to_string(),
),
};
let metadata = MetadataBuilder::new("https://auth.example.com".to_string())
.enable_mtls(mtls_endpoints, true)
.build();
assert!(metadata.mtls_endpoint_aliases.is_some());
assert_eq!(
metadata.tls_client_certificate_bound_access_tokens,
Some(true)
);
}
}