use serde::{Deserialize, Serialize};
use crate::client::{Client, ClientAuth, ClientId, DynamicRegistration, SecretHash};
use crate::events::ClientAuthFailure;
use crate::grant::GrantType;
use crate::scope::ScopeSet;
use crate::server::{AuthorizationServer, Clock, ServerConfig};
use crate::store::{Storage, StorageError};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClientMetadata {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub redirect_uris: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token_endpoint_auth_method: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grant_types: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_types: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub software_statement: Option<String>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClientInformation {
pub client_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_secret: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_id_issued_at: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_secret_expires_at: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registration_access_token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registration_client_uri: Option<String>,
#[serde(flatten)]
pub metadata: ClientMetadata,
}
impl std::fmt::Debug for ClientInformation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn redact_opt<T>(value: &Option<T>) -> Option<&'static str> {
value.as_ref().map(|_| "[redacted]")
}
f.debug_struct("ClientInformation")
.field("client_id", &self.client_id)
.field("client_secret", &redact_opt(&self.client_secret))
.field("client_id_issued_at", &self.client_id_issued_at)
.field("client_secret_expires_at", &self.client_secret_expires_at)
.field(
"registration_access_token",
&redact_opt(&self.registration_access_token),
)
.field("registration_client_uri", &self.registration_client_uri)
.field("metadata", &self.metadata)
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum RegistrationErrorCode {
InvalidRedirectUri,
InvalidClientMetadata,
InvalidSoftwareStatement,
}
impl RegistrationErrorCode {
pub fn as_str(self) -> &'static str {
match self {
RegistrationErrorCode::InvalidRedirectUri => "invalid_redirect_uri",
RegistrationErrorCode::InvalidClientMetadata => "invalid_client_metadata",
RegistrationErrorCode::InvalidSoftwareStatement => "invalid_software_statement",
}
}
}
impl std::fmt::Display for RegistrationErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegistrationErrorResponse {
pub error: RegistrationErrorCode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_description: Option<std::borrow::Cow<'static, str>>,
}
impl RegistrationErrorResponse {
pub fn new(
error: RegistrationErrorCode,
description: impl Into<std::borrow::Cow<'static, str>>,
) -> Self {
RegistrationErrorResponse {
error,
error_description: Some(description.into()),
}
}
}
impl std::fmt::Display for RegistrationErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.error_description {
Some(d) => write!(f, "{}: {d}", self.error),
None => f.write_str(self.error.as_str()),
}
}
}
impl std::error::Error for RegistrationErrorResponse {}
#[derive(Debug)]
#[non_exhaustive]
pub enum RegistrationFailure {
Disabled,
Unauthorized,
Invalid(RegistrationErrorResponse),
Storage(StorageError),
}
impl RegistrationFailure {
pub fn http_status(&self) -> u16 {
match self {
RegistrationFailure::Disabled => 404,
RegistrationFailure::Unauthorized => 401,
RegistrationFailure::Invalid(_) => 400,
RegistrationFailure::Storage(_) => 500,
}
}
}
impl std::fmt::Display for RegistrationFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RegistrationFailure::Disabled => f.write_str("dynamic client registration is disabled"),
RegistrationFailure::Unauthorized => f.write_str("not authorized to register"),
RegistrationFailure::Invalid(e) => write!(f, "{e}"),
RegistrationFailure::Storage(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for RegistrationFailure {}
fn randomness_failure() -> RegistrationFailure {
RegistrationFailure::Storage(StorageError::new(
"the OS would not provide randomness for a registration artifact",
))
}
#[non_exhaustive]
pub struct RegistrationAttempt<'a> {
pub initial_access_token: Option<&'a str>,
pub metadata: &'a ClientMetadata,
}
impl std::fmt::Debug for RegistrationAttempt<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RegistrationAttempt")
.field(
"initial_access_token",
&self.initial_access_token.map(|_| "[redacted]"),
)
.field("metadata", self.metadata)
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RegistrationDecision {
Allow,
Deny,
}
pub trait RegistrationPolicy: Send + Sync {
fn authorize(&self, attempt: &RegistrationAttempt<'_>) -> RegistrationDecision;
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RegistrationConfig {
pub registration_endpoint: Option<String>,
pub allowed_grant_types: Vec<GrantType>,
pub allowed_scopes: ScopeSet,
pub client_secret_ttl: Option<std::time::Duration>,
pub management_enabled: bool,
}
impl Default for RegistrationConfig {
fn default() -> Self {
RegistrationConfig::new()
}
}
impl RegistrationConfig {
pub fn new() -> Self {
RegistrationConfig {
registration_endpoint: None,
allowed_grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
allowed_scopes: ScopeSet::default(),
client_secret_ttl: None,
management_enabled: true,
}
}
pub fn endpoint(&self, issuer: &str) -> String {
match &self.registration_endpoint {
Some(url) => url.clone(),
None => format!("{}/register", issuer.trim_end_matches('/')),
}
}
}
pub const MAX_REGISTERED_REDIRECT_URIS: usize = 16;
const AUTH_METHOD_NONE: &str = "none";
const AUTH_METHOD_BASIC: &str = "client_secret_basic";
const AUTH_METHOD_POST: &str = "client_secret_post";
const RESPONSE_TYPE_CODE: &str = "code";
fn redirect_uri_is_registerable(value: &str) -> bool {
crate::authorization::is_valid_resource_indicator(value)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Registered {
pub(crate) redirect_uris: Vec<String>,
pub(crate) grant_types: Vec<GrantType>,
pub(crate) response_types: Vec<String>,
pub(crate) token_endpoint_auth_method: String,
pub(crate) scope: ScopeSet,
pub(crate) client_name: Option<String>,
}
pub(crate) fn validate(
metadata: &ClientMetadata,
config: &RegistrationConfig,
) -> Result<Registered, RegistrationFailure> {
if metadata.software_statement.is_some() {
return Err(RegistrationFailure::Invalid(
RegistrationErrorResponse::new(
RegistrationErrorCode::InvalidSoftwareStatement,
"this server does not evaluate software statements (RFC 7591 s2.3)",
),
));
}
let grant_types: Vec<GrantType> =
match metadata.grant_types.as_deref() {
None => vec![GrantType::AuthorizationCode],
Some(values) => {
let mut out = Vec::with_capacity(values.len());
for value in values {
let grant: GrantType = GrantType::parse(value).ok_or_else(|| {
RegistrationFailure::Invalid(RegistrationErrorResponse::new(
RegistrationErrorCode::InvalidClientMetadata,
"grant_types names a grant this server does not implement",
))
})?;
if !config.allowed_grant_types.contains(&grant) {
return Err(RegistrationFailure::Invalid(RegistrationErrorResponse::new(
RegistrationErrorCode::InvalidClientMetadata,
"grant_types names a grant this deployment does not offer registrants",
)));
}
if !out.contains(&grant) {
out.push(grant);
}
}
out
}
};
let uses_code = grant_types.contains(&GrantType::AuthorizationCode);
let response_types: Vec<String> =
match metadata.response_types.as_deref() {
None => match uses_code {
true => vec![RESPONSE_TYPE_CODE.to_string()],
false => Vec::new(),
},
Some(values) => {
for value in values {
if value != RESPONSE_TYPE_CODE {
return Err(RegistrationFailure::Invalid(RegistrationErrorResponse::new(
RegistrationErrorCode::InvalidClientMetadata,
"this server issues authorization codes only; OAuth 2.1 removes the \
implicit grant",
)));
}
}
let asks_for_code = !values.is_empty();
if asks_for_code != uses_code {
return Err(RegistrationFailure::Invalid(RegistrationErrorResponse::new(
RegistrationErrorCode::InvalidClientMetadata,
"grant_types and response_types must correspond: authorization_code with \
code (RFC 7591 s2)",
)));
}
match asks_for_code {
true => vec![RESPONSE_TYPE_CODE.to_string()],
false => Vec::new(),
}
}
};
if uses_code && metadata.redirect_uris.is_empty() {
return Err(RegistrationFailure::Invalid(
RegistrationErrorResponse::new(
RegistrationErrorCode::InvalidRedirectUri,
"the authorization_code grant requires at least one redirect_uri",
),
));
}
if metadata.redirect_uris.len() > MAX_REGISTERED_REDIRECT_URIS {
return Err(RegistrationFailure::Invalid(
RegistrationErrorResponse::new(
RegistrationErrorCode::InvalidRedirectUri,
"too many redirect_uris",
),
));
}
for uri in &metadata.redirect_uris {
if !redirect_uri_is_registerable(uri) {
return Err(RegistrationFailure::Invalid(
RegistrationErrorResponse::new(
RegistrationErrorCode::InvalidRedirectUri,
"each redirect_uri must be an absolute URI with no fragment (RFC 6749 s3.1.2)",
),
));
}
}
let token_endpoint_auth_method = metadata
.token_endpoint_auth_method
.clone()
.unwrap_or_else(|| AUTH_METHOD_BASIC.to_string());
if !matches!(
token_endpoint_auth_method.as_str(),
AUTH_METHOD_NONE | AUTH_METHOD_BASIC | AUTH_METHOD_POST
) {
return Err(RegistrationFailure::Invalid(
RegistrationErrorResponse::new(
RegistrationErrorCode::InvalidClientMetadata,
"token_endpoint_auth_method is not one this server can REGISTER; RFC 8414 \
token_endpoint_auth_methods_supported describes the token endpoint, which \
accepts more",
),
));
}
if token_endpoint_auth_method == AUTH_METHOD_NONE
&& grant_types.contains(&GrantType::ClientCredentials)
{
return Err(RegistrationFailure::Invalid(
RegistrationErrorResponse::new(
RegistrationErrorCode::InvalidClientMetadata,
"client_credentials requires a confidential client (RFC 6749 s4.4)",
),
));
}
let scope =
match metadata.scope.as_deref() {
None => ScopeSet::empty(),
Some(s) => {
let requested = ScopeSet::parse(s).map_err(|_| {
RegistrationFailure::Invalid(RegistrationErrorResponse::new(
RegistrationErrorCode::InvalidClientMetadata,
"scope is not a space-delimited RFC 6749 s3.3 token list",
))
})?;
if !requested.is_subset(&config.allowed_scopes) {
return Err(RegistrationFailure::Invalid(RegistrationErrorResponse::new(
RegistrationErrorCode::InvalidClientMetadata,
"scope exceeds what this deployment offers dynamically registered clients",
)));
}
requested
}
};
Ok(Registered {
redirect_uris: metadata.redirect_uris.clone(),
grant_types,
response_types,
token_endpoint_auth_method,
scope,
client_name: metadata.client_name.clone(),
})
}
fn registered_metadata(client: &Client, registration: &DynamicRegistration) -> ClientMetadata {
ClientMetadata {
redirect_uris: client.redirect_uris.clone(),
token_endpoint_auth_method: Some(registration.token_endpoint_auth_method.clone()),
grant_types: Some(client.grant_types.iter().map(|g| g.to_string()).collect()),
response_types: Some(match client.allows_grant(GrantType::AuthorizationCode) {
true => vec![RESPONSE_TYPE_CODE.to_string()],
false => Vec::new(),
}),
client_name: client.name.clone(),
scope: (!client.allowed_scopes.is_empty()).then(|| client.allowed_scopes.to_string()),
software_statement: None,
}
}
impl<S: Storage, C: Clock> AuthorizationServer<S, C> {
fn registration_config(&self) -> Result<&RegistrationConfig, RegistrationFailure> {
self.config()
.registration
.as_deref()
.ok_or(RegistrationFailure::Disabled)
}
pub async fn register_dynamic_client(
&self,
metadata: &ClientMetadata,
initial_access_token: Option<&str>,
) -> Result<ClientInformation, RegistrationFailure> {
self.admit_registration()?;
self.register_admitted_client(metadata, initial_access_token)
.await
}
pub(crate) fn admit_registration(&self) -> Result<(), RegistrationFailure> {
self.registration_config()?;
if self
.hooks()
.check(crate::events::Attempt::ClientRegistration)
== crate::events::RateLimitDecision::Deny
{
return Err(RegistrationFailure::Unauthorized);
}
Ok(())
}
pub(crate) async fn register_admitted_client(
&self,
metadata: &ClientMetadata,
initial_access_token: Option<&str>,
) -> Result<ClientInformation, RegistrationFailure> {
let config = self.registration_config()?;
let limited = crate::events::Attempt::ClientRegistration;
let attempt = RegistrationAttempt {
initial_access_token,
metadata,
};
match self.hooks().registration_policy() {
Some(policy) if policy.authorize(&attempt) == RegistrationDecision::Allow => {}
_ => {
self.hooks()
.emit(|| crate::events::Event::ClientRegistrationRefusedByPolicy {
client_id: None,
});
self.hooks()
.record(limited, crate::events::AttemptOutcome::Failed);
return Err(RegistrationFailure::Unauthorized);
}
}
let registered = match validate(metadata, config) {
Ok(registered) => registered,
Err(refusal) => {
self.hooks()
.record(limited, crate::events::AttemptOutcome::Failed);
return Err(refusal);
}
};
let now = crate::server::unix_seconds(self.now());
let client_id =
ClientId::new(crate::server::try_random_hex(16).ok_or_else(randomness_failure)?);
let secret = if registered.token_endpoint_auth_method != AUTH_METHOD_NONE {
Some(crate::server::try_random_hex(32).ok_or_else(randomness_failure)?)
} else {
None
};
let secret_expires_at = secret.as_ref().map(|_| match config.client_secret_ttl {
None => 0,
Some(ttl) => now.unwrap_or_default().saturating_add(ttl.as_secs()),
});
let registration_access_token =
crate::server::try_random_hex(32).ok_or_else(randomness_failure)?;
let client = Client {
client_id: client_id.clone(),
auth: match &secret {
None => ClientAuth::Public,
Some(s) => ClientAuth::ConfidentialSecretHash {
hash: SecretHash::sha256(s),
},
},
grant_types: registered.grant_types.clone(),
redirect_uris: registered.redirect_uris.clone(),
allowed_scopes: registered.scope.clone(),
default_scopes: registered.scope.clone(),
name: registered.client_name.clone(),
registration: Some(Box::new(DynamicRegistration {
registration_access_token_hash: SecretHash::sha256(®istration_access_token),
client_id_issued_at: now,
client_secret_expires_at: secret_expires_at,
token_endpoint_auth_method: registered.token_endpoint_auth_method.clone(),
})),
};
self.store()
.put_client(client)
.await
.map_err(RegistrationFailure::Storage)?;
self.hooks()
.emit(|| crate::events::Event::ClientRegistered {
client_id: client_id.as_str(),
});
self.hooks()
.record(limited, crate::events::AttemptOutcome::Succeeded);
Ok(ClientInformation {
client_id: client_id.as_str().to_string(),
client_secret: secret,
client_id_issued_at: now,
client_secret_expires_at: secret_expires_at,
registration_access_token: config
.management_enabled
.then_some(registration_access_token),
registration_client_uri: config
.management_enabled
.then(|| registration_client_uri(config, self.config(), client_id.as_str())),
metadata: ClientMetadata {
redirect_uris: registered.redirect_uris,
token_endpoint_auth_method: Some(registered.token_endpoint_auth_method),
grant_types: Some(
registered
.grant_types
.iter()
.map(|g| g.to_string())
.collect(),
),
response_types: Some(registered.response_types),
client_name: registered.client_name,
scope: (!registered.scope.is_empty()).then(|| registered.scope.to_string()),
software_statement: None,
},
})
}
pub(crate) async fn authenticate_registration(
&self,
client_id: &ClientId,
registration_access_token: &str,
) -> Result<(std::sync::Arc<Client>, DynamicRegistration), RegistrationFailure> {
let config = self.registration_config()?;
if !config.management_enabled {
return Err(RegistrationFailure::Disabled);
}
let attempt = crate::events::Attempt::ClientAuthentication {
client_id: client_id.as_str(),
};
if self.hooks().check(attempt) == crate::events::RateLimitDecision::Deny {
return Err(self.registration_auth_failed(
client_id,
ClientAuthFailure::RateLimited,
false,
));
}
let found = self
.store()
.get_client(client_id)
.await
.map_err(RegistrationFailure::Storage)?;
let client = match found {
Some(client) => client,
None => {
return Err(self.registration_auth_failed(
client_id,
ClientAuthFailure::UnknownClient,
true,
))
}
};
let registration = match client.registration.as_deref() {
Some(registration) => registration.clone(),
None => {
return Err(self.registration_auth_failed(
client_id,
ClientAuthFailure::NoDynamicRegistration,
true,
))
}
};
if !registration
.registration_access_token_hash
.verify(registration_access_token, self.hooks().secret_verifier())
{
return Err(self.registration_auth_failed(
client_id,
ClientAuthFailure::SecretMismatch,
true,
));
}
self.hooks()
.record(attempt, crate::events::AttemptOutcome::Succeeded);
Ok((client, registration))
}
fn registration_auth_failed(
&self,
client_id: &ClientId,
failure: ClientAuthFailure,
attempted: bool,
) -> RegistrationFailure {
self.hooks().emit(
|| crate::events::Event::ClientRegistrationAuthenticationFailed {
client_id: client_id.as_str(),
failure,
},
);
if attempted {
self.hooks().record(
crate::events::Attempt::ClientAuthentication {
client_id: client_id.as_str(),
},
crate::events::AttemptOutcome::Failed,
);
}
RegistrationFailure::Unauthorized
}
pub async fn read_registration(
&self,
client_id: &ClientId,
registration_access_token: &str,
) -> Result<ClientInformation, RegistrationFailure> {
let (client, registration) = self
.authenticate_registration(client_id, registration_access_token)
.await?;
let config = self.registration_config()?;
Ok(ClientInformation {
client_id: client.client_id.as_str().to_string(),
client_secret: None,
client_id_issued_at: registration.client_id_issued_at,
client_secret_expires_at: registration.client_secret_expires_at,
registration_access_token: None,
registration_client_uri: Some(registration_client_uri(
config,
self.config(),
client.client_id.as_str(),
)),
metadata: registered_metadata(&client, ®istration),
})
}
pub async fn update_registration(
&self,
client_id: &ClientId,
registration_access_token: &str,
metadata: &ClientMetadata,
) -> Result<ClientInformation, RegistrationFailure> {
let (client, registration) = self
.authenticate_registration(client_id, registration_access_token)
.await?;
let config = self.registration_config()?;
let attempt = RegistrationAttempt {
initial_access_token: None,
metadata,
};
match self.hooks().registration_policy() {
Some(policy) if policy.authorize(&attempt) == RegistrationDecision::Allow => {}
_ => {
self.hooks()
.emit(|| crate::events::Event::ClientRegistrationRefusedByPolicy {
client_id: Some(client_id.as_str()),
});
return Err(RegistrationFailure::Unauthorized);
}
}
let registered = validate(metadata, config)?;
let wants_secret = registered.token_endpoint_auth_method != AUTH_METHOD_NONE;
let had_secret = client.auth.is_confidential();
let new_secret = if wants_secret && !had_secret {
Some(crate::server::try_random_hex(32).ok_or_else(randomness_failure)?)
} else {
None
};
let auth = match (&new_secret, wants_secret) {
(Some(s), _) => ClientAuth::ConfidentialSecretHash {
hash: SecretHash::sha256(s),
},
(None, true) => client.auth.clone(),
(None, false) => ClientAuth::Public,
};
let client_secret_expires_at = match (&new_secret, wants_secret) {
(Some(_), _) => Some(match config.client_secret_ttl {
None => 0,
Some(ttl) => {
crate::server::unix_seconds(self.now())
.unwrap_or_default()
.saturating_add(ttl.as_secs())
}
}),
(None, true) => registration.client_secret_expires_at,
(None, false) => None,
};
let updated_registration = DynamicRegistration {
registration_access_token_hash: registration.registration_access_token_hash.clone(),
client_id_issued_at: registration.client_id_issued_at,
client_secret_expires_at,
token_endpoint_auth_method: registered.token_endpoint_auth_method.clone(),
};
let updated = Client {
client_id: client.client_id.clone(),
auth,
grant_types: registered.grant_types.clone(),
redirect_uris: registered.redirect_uris.clone(),
allowed_scopes: registered.scope.clone(),
default_scopes: registered.scope.clone(),
name: registered.client_name.clone(),
registration: Some(Box::new(updated_registration.clone())),
};
let applied = self
.store()
.compare_and_swap_client(&client, updated.clone())
.await
.map_err(RegistrationFailure::Storage)?;
if !applied {
return Err(RegistrationFailure::Unauthorized);
}
self.hooks()
.emit(|| crate::events::Event::ClientRegistrationUpdated {
client_id: client_id.as_str(),
});
Ok(ClientInformation {
client_id: client_id.as_str().to_string(),
client_secret: new_secret,
client_id_issued_at: updated_registration.client_id_issued_at,
client_secret_expires_at: updated_registration.client_secret_expires_at,
registration_access_token: None,
registration_client_uri: Some(registration_client_uri(
config,
self.config(),
client_id.as_str(),
)),
metadata: registered_metadata(&updated, &updated_registration),
})
}
pub async fn delete_registration(
&self,
client_id: &ClientId,
registration_access_token: &str,
) -> Result<(), RegistrationFailure> {
self.authenticate_registration(client_id, registration_access_token)
.await?;
self.store()
.delete_client(client_id, self.revocation_window())
.await
.map_err(RegistrationFailure::Storage)?;
self.hooks()
.emit(|| crate::events::Event::ClientRegistrationDeleted {
client_id: client_id.as_str(),
});
Ok(())
}
}
fn registration_client_uri(
config: &RegistrationConfig,
server: &ServerConfig,
client_id: &str,
) -> String {
let endpoint = config.endpoint(&server.issuer);
let mut url = String::with_capacity(endpoint.len() + 1 + client_id.len());
url.push_str(&endpoint);
url.push('/');
for byte in client_id.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
url.push(byte as char)
}
other => url.push_str(&format!("%{other:02X}")),
}
}
url
}
#[cfg(test)]
#[path = "tests/registration.rs"]
mod tests;