use serde::{Deserialize, Serialize};
use crate::grant::DEVICE_CODE_GRANT_URN;
use crate::server::ServerConfig;
pub const WELL_KNOWN_PATH: &str = "/.well-known/oauth-authorization-server";
pub fn issuer_path(issuer: &str) -> &str {
let authority = match issuer.find("://") {
Some(i) => &issuer[i + 3..],
None => issuer,
};
match authority.find('/') {
Some(i) => authority[i..].trim_end_matches('/'),
None => "",
}
}
pub fn well_known_path(issuer: &str) -> String {
let path = issuer_path(issuer);
let mut out = String::with_capacity(WELL_KNOWN_PATH.len() + path.len());
out.push_str(WELL_KNOWN_PATH);
out.push_str(path);
out
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AuthorizationServerMetadata {
pub issuer: String,
pub authorization_endpoint: String,
pub token_endpoint: String,
pub device_authorization_endpoint: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub introspection_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revocation_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registration_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jwks_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scopes_supported: Option<Vec<String>>,
#[cfg(feature = "par")]
#[serde(skip_serializing_if = "Option::is_none")]
pub pushed_authorization_request_endpoint: Option<String>,
#[cfg(feature = "par")]
#[serde(skip_serializing_if = "Option::is_none")]
pub require_pushed_authorization_requests: Option<bool>,
#[cfg(feature = "jar")]
#[serde(skip_serializing_if = "Option::is_none")]
pub request_object_signing_alg_values_supported: Option<Vec<String>>,
#[cfg(feature = "jar")]
#[serde(skip_serializing_if = "Option::is_none")]
pub require_signed_request_object: Option<bool>,
pub response_types_supported: Vec<String>,
pub response_modes_supported: Vec<String>,
pub grant_types_supported: Vec<String>,
pub token_endpoint_auth_methods_supported: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dpop_signing_alg_values_supported: Option<Vec<String>>,
pub code_challenge_methods_supported: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_documentation: Option<String>,
#[cfg(feature = "resource-metadata")]
#[serde(skip_serializing_if = "Option::is_none")]
pub protected_resources: Option<Vec<String>>,
#[cfg(feature = "rar")]
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization_details_types_supported: Option<Vec<String>>,
#[cfg(feature = "cimd")]
pub client_id_metadata_document_supported: bool,
#[serde(default)]
pub authorization_response_iss_parameter_supported: bool,
#[cfg(feature = "mtls")]
#[serde(default)]
pub tls_client_certificate_bound_access_tokens: bool,
}
fn under_issuer(issuer: &str, path: &str) -> String {
format!("{}{}", issuer.trim_end_matches('/'), path)
}
#[cfg(feature = "jwt")]
fn advertised_jwks_uri(config: &ServerConfig) -> Option<String> {
match &config.access_token_format {
crate::jwt::AccessTokenFormat::Opaque => None,
crate::jwt::AccessTokenFormat::Jwt(jwt) => jwt
.jwks_uri()
.map(str::to_string)
.or_else(|| config.jwks_uri.clone()),
}
}
#[cfg(not(feature = "jwt"))]
fn advertised_jwks_uri(config: &ServerConfig) -> Option<String> {
config.jwks_uri.clone()
}
impl AuthorizationServerMetadata {
pub fn from_config(config: &ServerConfig) -> Self {
let iss = config.issuer.trim_end_matches('/').to_string();
let mut grant_types_supported = vec!["authorization_code".to_string()];
if config.issue_refresh_tokens {
grant_types_supported.push("refresh_token".to_string());
}
grant_types_supported.push("client_credentials".to_string());
grant_types_supported.push(DEVICE_CODE_GRANT_URN.to_string());
#[cfg(feature = "token-exchange")]
grant_types_supported.push(crate::grant::TOKEN_EXCHANGE_GRANT_URN.to_string());
let endpoint = |override_: &Option<String>, path: &str| {
override_
.clone()
.unwrap_or_else(|| under_issuer(&iss, path))
};
AuthorizationServerMetadata {
authorization_endpoint: endpoint(&config.authorization_endpoint, "/authorize"),
token_endpoint: endpoint(&config.token_endpoint, "/token"),
device_authorization_endpoint: endpoint(
&config.device_authorization_endpoint,
"/device_authorization",
),
introspection_endpoint: config.introspection_endpoint.clone(),
revocation_endpoint: Some(endpoint(&config.revocation_endpoint, "/revoke")),
registration_endpoint: config
.registration
.as_ref()
.map(|r| endpoint(&r.registration_endpoint, "/register")),
jwks_uri: advertised_jwks_uri(config),
scopes_supported: config.scopes_supported.clone(),
#[cfg(feature = "par")]
pushed_authorization_request_endpoint: config
.par
.as_ref()
.map(|par| par.endpoint(&iss)),
#[cfg(feature = "par")]
require_pushed_authorization_requests: config
.par
.as_ref()
.map(|par| par.require_pushed_authorization_requests),
#[cfg(all(feature = "jar", feature = "jwt-p256"))]
request_object_signing_alg_values_supported: config.jar.as_ref().map(|_| {
crate::par::REQUEST_OBJECT_SIGNING_ALGS
.iter()
.map(|alg| alg.to_string())
.collect()
}),
#[cfg(all(feature = "jar", not(feature = "jwt-p256")))]
request_object_signing_alg_values_supported: None,
#[cfg(feature = "jar")]
require_signed_request_object: config
.jar
.as_ref()
.map(|jar| jar.require_signed_request_object),
issuer: iss,
response_types_supported: vec!["code".to_string()],
response_modes_supported: vec!["query".to_string()],
grant_types_supported,
token_endpoint_auth_methods_supported: {
#[allow(unused_mut)]
let mut methods = vec![
"client_secret_basic".to_string(),
"client_secret_post".to_string(),
"none".to_string(),
];
#[cfg(feature = "client-assertion")]
{
methods.push(crate::client_assertion::CLIENT_SECRET_JWT.to_string());
#[cfg(feature = "jwt-p256")]
methods.push(crate::client_assertion::PRIVATE_KEY_JWT.to_string());
}
#[cfg(feature = "mtls")]
{
methods.push(crate::mtls::TLS_CLIENT_AUTH.to_string());
methods.push(crate::mtls::SELF_SIGNED_TLS_CLIENT_AUTH.to_string());
}
methods
},
#[cfg(all(feature = "client-assertion", feature = "jwt-p256"))]
token_endpoint_auth_signing_alg_values_supported: Some(
crate::client_assertion::ASSERTION_SIGNING_ALGS
.iter()
.map(|a| a.to_string())
.collect(),
),
#[cfg(all(feature = "client-assertion", not(feature = "jwt-p256")))]
token_endpoint_auth_signing_alg_values_supported: Some(vec!["HS256".to_string()]),
#[cfg(not(feature = "client-assertion"))]
token_endpoint_auth_signing_alg_values_supported: None,
#[cfg(all(feature = "dpop", feature = "jwt-p256"))]
dpop_signing_alg_values_supported: Some(
crate::dpop::DPOP_SIGNING_ALG_VALUES_SUPPORTED
.iter()
.map(|a| a.to_string())
.collect(),
),
#[cfg(all(feature = "dpop", not(feature = "jwt-p256")))]
dpop_signing_alg_values_supported: None,
#[cfg(not(feature = "dpop"))]
dpop_signing_alg_values_supported: None,
code_challenge_methods_supported: vec!["S256".to_string()],
service_documentation: config.service_documentation.clone(),
#[cfg(feature = "resource-metadata")]
protected_resources: config.protected_resources.clone(),
#[cfg(feature = "rar")]
authorization_details_types_supported: config
.authorization_details_types_supported
.clone(),
#[cfg(feature = "cimd")]
client_id_metadata_document_supported: config.cimd.is_some(),
authorization_response_iss_parameter_supported: true,
#[cfg(feature = "mtls")]
tls_client_certificate_bound_access_tokens: true,
}
}
#[cfg(any(feature = "client-assertion", feature = "jar", feature = "dpop"))]
pub(crate) fn es256_verification_is_available(&mut self) {
#[cfg(feature = "client-assertion")]
{
let method = crate::client_assertion::PRIVATE_KEY_JWT.to_string();
if !self.token_endpoint_auth_methods_supported.contains(&method) {
self.token_endpoint_auth_methods_supported.push(method);
}
let algs = self
.token_endpoint_auth_signing_alg_values_supported
.get_or_insert_with(Vec::new);
if !algs.iter().any(|a| a == "ES256") {
algs.push("ES256".to_string());
}
}
#[cfg(feature = "jar")]
{
if self.require_signed_request_object.is_some() {
self.request_object_signing_alg_values_supported = Some(
crate::par::REQUEST_OBJECT_SIGNING_ALGS
.iter()
.map(|alg| alg.to_string())
.collect(),
);
}
}
#[cfg(feature = "dpop")]
{
self.dpop_signing_alg_values_supported = Some(
crate::dpop::DPOP_SIGNING_ALG_VALUES_SUPPORTED
.iter()
.map(|a| a.to_string())
.collect(),
);
}
}
}
#[cfg(test)]
#[path = "tests/metadata.rs"]
mod tests;