use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
mod plan;
mod ports;
#[cfg(test)]
mod tests;
pub use plan::{acquire_client, plan_registration};
pub use ports::{Available, DynamicRegistrar, MetadataDocumentLoader};
use crate::domain::oauth::registration::{ClientId, ClientIdError, RegisteredClient};
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct ClientMetadata {
redirect_uris: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
client_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
token_endpoint_auth_method: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
application_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
scope: Option<String>,
#[serde(flatten)]
extra: Map<String, Value>,
}
impl ClientMetadata {
pub fn new(redirect_uris: Vec<String>) -> Result<Self, ClientIdError> {
if redirect_uris.is_empty() {
return Err(ClientIdError::DocumentRegistration);
}
for uri in &redirect_uris {
let Ok(parsed) = url::Url::parse(uri) else {
return Err(ClientIdError::NotHttps);
};
if !matches!(parsed.scheme(), "https" | "http") || parsed.host_str().is_none() {
return Err(ClientIdError::NotHttps);
}
}
Ok(Self {
redirect_uris,
..Self::default()
})
}
#[must_use]
pub fn redirect_uris(&self) -> &[String] {
&self.redirect_uris
}
#[must_use]
pub fn client_name(&self) -> Option<&str> {
self.client_name.as_deref()
}
#[must_use]
pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
self.client_name = Some(name.into());
self
}
#[must_use]
pub fn token_endpoint_auth_method(&self) -> Option<&str> {
self.token_endpoint_auth_method.as_deref()
}
#[must_use]
pub fn with_token_endpoint_auth_method(mut self, method: impl Into<String>) -> Self {
self.token_endpoint_auth_method = Some(method.into());
self
}
#[must_use]
pub fn application_type(&self) -> Option<&str> {
self.application_type.as_deref()
}
#[must_use]
pub fn with_application_type(mut self, application_type: impl Into<String>) -> Self {
self.application_type = Some(application_type.into());
self
}
pub fn with_field(
mut self,
name: impl Into<String>,
value: impl Into<Value>,
) -> Result<Self, ClientMetadataError> {
let name = name.into();
if RESERVED_FIELDS.contains(&name.as_str()) {
return Err(ClientMetadataError::ReservedField(name));
}
self.extra.insert(name, value.into());
Ok(self)
}
#[must_use]
pub const fn extra(&self) -> &Map<String, Value> {
&self.extra
}
}
const RESERVED_FIELDS: &[&str] = &[
"redirect_uris",
"client_name",
"token_endpoint_auth_method",
"application_type",
"scope",
];
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ClientMetadataError {
#[error("'{0}' is a modelled client metadata field and cannot be set as an extra")]
ReservedField(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ClientIdentity {
document: Option<ClientId>,
metadata: Option<ClientMetadata>,
}
impl ClientIdentity {
#[must_use]
pub const fn from_document(client_id: ClientId) -> Self {
Self {
document: Some(client_id),
metadata: None,
}
}
#[must_use]
pub const fn from_metadata(metadata: ClientMetadata) -> Self {
Self {
document: None,
metadata: Some(metadata),
}
}
#[must_use]
pub fn with_metadata_fallback(mut self, metadata: ClientMetadata) -> Self {
self.metadata = Some(metadata);
self
}
#[must_use]
pub const fn document(&self) -> Option<&ClientId> {
self.document.as_ref()
}
#[must_use]
pub const fn metadata(&self) -> Option<&ClientMetadata> {
self.metadata.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RegistrationPlan {
Reuse(RegisteredClient),
FetchDocument {
client_id: ClientId,
fallback: Option<ClientMetadata>,
},
RegisterDynamic {
metadata: ClientMetadata,
},
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RegistrationError {
#[error("this revision specifies no client registration mechanism")]
NoMechanism,
#[error("this revision supports neither client id metadata documents nor dynamic registration")]
NoUsableMechanism,
#[error(
"this revision requires application_type on dynamic registration and the metadata does not set it"
)]
ApplicationTypeRequired,
#[error(
"the stored client was issued by a different authorization server and this revision requires issuer binding"
)]
IssuerBound,
#[error("the client id metadata document could not be fetched: {0}")]
DocumentUnavailable(#[from] RegistrarError),
#[error("dynamic client registration failed: {0}")]
DynamicFailed(RegistrarError),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RegistrarError {
#[error("the registration request could not be completed: {0}")]
Unavailable(String),
#[error("the authorization server answered {status} to a client registration request")]
Refused {
status: u16,
},
#[error("the registration response could not be used: {0}")]
Malformed(String),
#[error(transparent)]
Document(#[from] ClientIdError),
}