mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-10-13 16:55:05 +02:00
1537 lines
60 KiB
Rust
1537 lines
60 KiB
Rust
use std::fmt::{Debug, Formatter, Result as FormatterResult};
|
|
use std::future::Future;
|
|
use std::marker::PhantomData;
|
|
use std::time::Duration;
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use http::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE};
|
|
use http::method::Method;
|
|
use http::status::StatusCode;
|
|
use serde::de::{Deserialize, DeserializeOwned, Deserializer, MapAccess, Visitor};
|
|
use serde::ser::SerializeMap;
|
|
use serde::{Serialize, Serializer};
|
|
use thiserror::Error;
|
|
|
|
use super::http_utils::{auth_bearer, check_content_type, MIME_TYPE_JSON};
|
|
use super::types::helpers::{serde_utc_seconds_opt, split_language_tag_key};
|
|
use super::types::{
|
|
ApplicationType, AuthenticationContextClass, ClientAuthMethod, ClientConfigUrl,
|
|
ClientContactEmail, ClientName, ClientUrl, GrantType, InitiateLoginUrl, JsonWebKeySetUrl,
|
|
JsonWebKeyType, JsonWebKeyUse, JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm,
|
|
JwsSigningAlgorithm, LocalizedClaim, LogoUrl, PolicyUrl, RegistrationAccessToken,
|
|
RegistrationUrl, RequestUrl, ResponseType, ResponseTypes, SectorIdentifierUrl,
|
|
SubjectIdentifierType, ToSUrl,
|
|
};
|
|
use super::{
|
|
AccessToken, ClientId, ClientSecret, ErrorResponseType, HttpRequest, HttpResponse, JsonWebKey,
|
|
JsonWebKeySet, RedirectUrl, StandardErrorResponse,
|
|
};
|
|
|
|
///
|
|
/// Trait for adding extra fields to [`ClientMetadata`].
|
|
///
|
|
pub trait AdditionalClientMetadata: Debug + DeserializeOwned + Serialize {}
|
|
|
|
// In order to support serde flatten, this must be an empty struct rather than an empty
|
|
// tuple struct.
|
|
///
|
|
/// Empty (default) extra [`ClientMetadata`] fields.
|
|
///
|
|
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
|
|
pub struct EmptyAdditionalClientMetadata {}
|
|
impl AdditionalClientMetadata for EmptyAdditionalClientMetadata {}
|
|
|
|
///
|
|
/// Client metadata used in dynamic client registration.
|
|
///
|
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
|
pub struct ClientMetadata<A, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>
|
|
where
|
|
A: AdditionalClientMetadata,
|
|
AT: ApplicationType,
|
|
CA: ClientAuthMethod,
|
|
G: GrantType,
|
|
JE: JweContentEncryptionAlgorithm<JT>,
|
|
JK: JweKeyManagementAlgorithm,
|
|
JS: JwsSigningAlgorithm<JT>,
|
|
JT: JsonWebKeyType,
|
|
JU: JsonWebKeyUse,
|
|
K: JsonWebKey<JS, JT, JU>,
|
|
RT: ResponseType,
|
|
S: SubjectIdentifierType,
|
|
{
|
|
// To avoid implementing a custom deserializer that handles both language tags and flatten,
|
|
// we wrap the language tag handling in its own flattened struct.
|
|
#[serde(bound = "AT: ApplicationType", flatten)]
|
|
standard_metadata: StandardClientMetadata<AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>,
|
|
|
|
#[serde(bound = "A: AdditionalClientMetadata", flatten)]
|
|
additional_metadata: A,
|
|
}
|
|
impl<A, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>
|
|
ClientMetadata<A, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>
|
|
where
|
|
A: AdditionalClientMetadata,
|
|
AT: ApplicationType,
|
|
CA: ClientAuthMethod,
|
|
G: GrantType,
|
|
JE: JweContentEncryptionAlgorithm<JT>,
|
|
JK: JweKeyManagementAlgorithm,
|
|
JS: JwsSigningAlgorithm<JT>,
|
|
JT: JsonWebKeyType,
|
|
JU: JsonWebKeyUse,
|
|
K: JsonWebKey<JS, JT, JU>,
|
|
RT: ResponseType,
|
|
S: SubjectIdentifierType,
|
|
{
|
|
///
|
|
/// Instantiates new client metadata.
|
|
///
|
|
pub fn new(redirect_uris: Vec<RedirectUrl>, additional_metadata: A) -> Self {
|
|
Self {
|
|
standard_metadata: StandardClientMetadata {
|
|
redirect_uris,
|
|
response_types: None,
|
|
grant_types: None,
|
|
application_type: None,
|
|
contacts: None,
|
|
client_name: None,
|
|
logo_uri: None,
|
|
client_uri: None,
|
|
policy_uri: None,
|
|
tos_uri: None,
|
|
jwks_uri: None,
|
|
jwks: None,
|
|
sector_identifier_uri: None,
|
|
subject_type: None,
|
|
id_token_signed_response_alg: None,
|
|
id_token_encrypted_response_alg: None,
|
|
id_token_encrypted_response_enc: None,
|
|
userinfo_signed_response_alg: None,
|
|
userinfo_encrypted_response_alg: None,
|
|
userinfo_encrypted_response_enc: None,
|
|
request_object_signing_alg: None,
|
|
request_object_encryption_alg: None,
|
|
request_object_encryption_enc: None,
|
|
token_endpoint_auth_method: None,
|
|
token_endpoint_auth_signing_alg: None,
|
|
default_max_age: None,
|
|
require_auth_time: None,
|
|
default_acr_values: None,
|
|
initiate_login_uri: None,
|
|
request_uris: None,
|
|
},
|
|
additional_metadata,
|
|
}
|
|
}
|
|
field_getters_setters![
|
|
pub self [self.standard_metadata] ["client metadata value"] {
|
|
set_redirect_uris -> redirect_uris[Vec<RedirectUrl>],
|
|
set_response_types -> response_types[Option<Vec<ResponseTypes<RT>>>],
|
|
set_grant_types -> grant_types[Option<Vec<G>>],
|
|
set_application_type -> application_type[Option<AT>],
|
|
set_contacts -> contacts[Option<Vec<ClientContactEmail>>],
|
|
set_client_name -> client_name[Option<LocalizedClaim<ClientName>>],
|
|
set_logo_uri -> logo_uri[Option<LocalizedClaim<LogoUrl>>],
|
|
set_client_uri -> client_uri[Option<LocalizedClaim<ClientUrl>>],
|
|
set_policy_uri -> policy_uri[Option<LocalizedClaim<PolicyUrl>>],
|
|
set_tos_uri -> tos_uri[Option<LocalizedClaim<ToSUrl>>],
|
|
set_jwks_uri -> jwks_uri[Option<JsonWebKeySetUrl>],
|
|
set_jwks -> jwks[Option<JsonWebKeySet<JS, JT, JU, K>>],
|
|
set_sector_identifier_uri -> sector_identifier_uri[Option<SectorIdentifierUrl>],
|
|
set_subject_type -> subject_type[Option<S>],
|
|
set_id_token_signed_response_alg -> id_token_signed_response_alg[Option<JS>],
|
|
set_id_token_encrypted_response_alg -> id_token_encrypted_response_alg[Option<JK>],
|
|
set_id_token_encrypted_response_enc -> id_token_encrypted_response_enc[Option<JE>],
|
|
set_userinfo_signed_response_alg -> userinfo_signed_response_alg[Option<JS>],
|
|
set_userinfo_encrypted_response_alg -> userinfo_encrypted_response_alg[Option<JK>],
|
|
set_userinfo_encrypted_response_enc -> userinfo_encrypted_response_enc[Option<JE>],
|
|
set_request_object_signing_alg -> request_object_signing_alg[Option<JS>],
|
|
set_request_object_encryption_alg -> request_object_encryption_alg[Option<JK>],
|
|
set_request_object_encryption_enc -> request_object_encryption_enc[Option<JE>],
|
|
set_token_endpoint_auth_method -> token_endpoint_auth_method[Option<CA>],
|
|
set_token_endpoint_auth_signing_alg -> token_endpoint_auth_signing_alg[Option<JS>],
|
|
set_default_max_age -> default_max_age[Option<Duration>],
|
|
set_require_auth_time -> require_auth_time[Option<bool>],
|
|
set_default_acr_values -> default_acr_values[Option<Vec<AuthenticationContextClass>>],
|
|
set_initiate_login_uri -> initiate_login_uri[Option<InitiateLoginUrl>],
|
|
set_request_uris -> request_uris[Option<Vec<RequestUrl>>],
|
|
}
|
|
];
|
|
|
|
///
|
|
/// Returns additional client metadata fields.
|
|
///
|
|
pub fn additional_metadata(&self) -> &A {
|
|
&self.additional_metadata
|
|
}
|
|
///
|
|
/// Returns mutable additional client metadata fields.
|
|
///
|
|
pub fn additional_metadata_mut(&mut self) -> &mut A {
|
|
&mut self.additional_metadata
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
struct StandardClientMetadata<AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>
|
|
where
|
|
AT: ApplicationType,
|
|
CA: ClientAuthMethod,
|
|
G: GrantType,
|
|
JE: JweContentEncryptionAlgorithm<JT>,
|
|
JK: JweKeyManagementAlgorithm,
|
|
JS: JwsSigningAlgorithm<JT>,
|
|
JT: JsonWebKeyType,
|
|
JU: JsonWebKeyUse,
|
|
K: JsonWebKey<JS, JT, JU>,
|
|
RT: ResponseType,
|
|
S: SubjectIdentifierType,
|
|
{
|
|
redirect_uris: Vec<RedirectUrl>,
|
|
response_types: Option<Vec<ResponseTypes<RT>>>,
|
|
grant_types: Option<Vec<G>>,
|
|
application_type: Option<AT>,
|
|
contacts: Option<Vec<ClientContactEmail>>,
|
|
client_name: Option<LocalizedClaim<ClientName>>,
|
|
logo_uri: Option<LocalizedClaim<LogoUrl>>,
|
|
client_uri: Option<LocalizedClaim<ClientUrl>>,
|
|
policy_uri: Option<LocalizedClaim<PolicyUrl>>,
|
|
tos_uri: Option<LocalizedClaim<ToSUrl>>,
|
|
jwks_uri: Option<JsonWebKeySetUrl>,
|
|
jwks: Option<JsonWebKeySet<JS, JT, JU, K>>,
|
|
sector_identifier_uri: Option<SectorIdentifierUrl>,
|
|
subject_type: Option<S>,
|
|
id_token_signed_response_alg: Option<JS>,
|
|
id_token_encrypted_response_alg: Option<JK>,
|
|
id_token_encrypted_response_enc: Option<JE>,
|
|
userinfo_signed_response_alg: Option<JS>,
|
|
userinfo_encrypted_response_alg: Option<JK>,
|
|
userinfo_encrypted_response_enc: Option<JE>,
|
|
request_object_signing_alg: Option<JS>,
|
|
request_object_encryption_alg: Option<JK>,
|
|
request_object_encryption_enc: Option<JE>,
|
|
token_endpoint_auth_method: Option<CA>,
|
|
token_endpoint_auth_signing_alg: Option<JS>,
|
|
default_max_age: Option<Duration>,
|
|
require_auth_time: Option<bool>,
|
|
default_acr_values: Option<Vec<AuthenticationContextClass>>,
|
|
initiate_login_uri: Option<InitiateLoginUrl>,
|
|
request_uris: Option<Vec<RequestUrl>>,
|
|
}
|
|
impl<'de, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S> Deserialize<'de>
|
|
for StandardClientMetadata<AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>
|
|
where
|
|
AT: ApplicationType,
|
|
CA: ClientAuthMethod,
|
|
G: GrantType,
|
|
JE: JweContentEncryptionAlgorithm<JT>,
|
|
JK: JweKeyManagementAlgorithm,
|
|
JS: JwsSigningAlgorithm<JT>,
|
|
JT: JsonWebKeyType,
|
|
JU: JsonWebKeyUse,
|
|
K: JsonWebKey<JS, JT, JU>,
|
|
RT: ResponseType,
|
|
S: SubjectIdentifierType,
|
|
{
|
|
///
|
|
/// Special deserializer that supports [RFC 5646](https://tools.ietf.org/html/rfc5646) language
|
|
/// tags associated with human-readable client metadata fields.
|
|
///
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
struct MetadataVisitor<
|
|
AT: ApplicationType,
|
|
CA: ClientAuthMethod,
|
|
G: GrantType,
|
|
JE: JweContentEncryptionAlgorithm<JT>,
|
|
JK: JweKeyManagementAlgorithm,
|
|
JS: JwsSigningAlgorithm<JT>,
|
|
JT: JsonWebKeyType,
|
|
JU: JsonWebKeyUse,
|
|
K: JsonWebKey<JS, JT, JU>,
|
|
RT: ResponseType,
|
|
S: SubjectIdentifierType,
|
|
>(
|
|
PhantomData<AT>,
|
|
PhantomData<CA>,
|
|
PhantomData<G>,
|
|
PhantomData<JE>,
|
|
PhantomData<JK>,
|
|
PhantomData<JS>,
|
|
PhantomData<JT>,
|
|
PhantomData<JU>,
|
|
PhantomData<K>,
|
|
PhantomData<RT>,
|
|
PhantomData<S>,
|
|
);
|
|
impl<'de, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S> Visitor<'de>
|
|
for MetadataVisitor<AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>
|
|
where
|
|
AT: ApplicationType,
|
|
CA: ClientAuthMethod,
|
|
G: GrantType,
|
|
JE: JweContentEncryptionAlgorithm<JT>,
|
|
JK: JweKeyManagementAlgorithm,
|
|
JS: JwsSigningAlgorithm<JT>,
|
|
JT: JsonWebKeyType,
|
|
JU: JsonWebKeyUse,
|
|
K: JsonWebKey<JS, JT, JU>,
|
|
RT: ResponseType,
|
|
S: SubjectIdentifierType,
|
|
{
|
|
type Value = StandardClientMetadata<AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>;
|
|
|
|
fn expecting(&self, formatter: &mut Formatter) -> FormatterResult {
|
|
formatter.write_str("struct StandardClientMetadata")
|
|
}
|
|
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
|
|
where
|
|
V: MapAccess<'de>,
|
|
{
|
|
deserialize_fields! {
|
|
map {
|
|
[redirect_uris]
|
|
[Option(response_types)]
|
|
[Option(grant_types)]
|
|
[Option(application_type)]
|
|
[Option(contacts)]
|
|
[LanguageTag(client_name)]
|
|
[LanguageTag(logo_uri)]
|
|
[LanguageTag(client_uri)]
|
|
[LanguageTag(policy_uri)]
|
|
[LanguageTag(tos_uri)]
|
|
[Option(jwks_uri)]
|
|
[Option(jwks)]
|
|
[Option(sector_identifier_uri)]
|
|
[Option(subject_type)]
|
|
[Option(id_token_signed_response_alg)]
|
|
[Option(id_token_encrypted_response_alg)]
|
|
[Option(id_token_encrypted_response_enc)]
|
|
[Option(userinfo_signed_response_alg)]
|
|
[Option(userinfo_encrypted_response_alg)]
|
|
[Option(userinfo_encrypted_response_enc)]
|
|
[Option(request_object_signing_alg)]
|
|
[Option(request_object_encryption_alg)]
|
|
[Option(request_object_encryption_enc)]
|
|
[Option(token_endpoint_auth_method)]
|
|
[Option(token_endpoint_auth_signing_alg)]
|
|
[Option(Seconds(default_max_age))]
|
|
[Option(require_auth_time)]
|
|
[Option(default_acr_values)]
|
|
[Option(initiate_login_uri)]
|
|
[Option(request_uris)]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
deserializer.deserialize_map(MetadataVisitor(
|
|
PhantomData,
|
|
PhantomData,
|
|
PhantomData,
|
|
PhantomData,
|
|
PhantomData,
|
|
PhantomData,
|
|
PhantomData,
|
|
PhantomData,
|
|
PhantomData,
|
|
PhantomData,
|
|
PhantomData,
|
|
))
|
|
}
|
|
}
|
|
impl<AT, CA, G, JE, JK, JS, JT, JU, K, RT, S> Serialize
|
|
for StandardClientMetadata<AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>
|
|
where
|
|
AT: ApplicationType,
|
|
CA: ClientAuthMethod,
|
|
G: GrantType,
|
|
JE: JweContentEncryptionAlgorithm<JT>,
|
|
JK: JweKeyManagementAlgorithm,
|
|
JS: JwsSigningAlgorithm<JT>,
|
|
JT: JsonWebKeyType,
|
|
JU: JsonWebKeyUse,
|
|
K: JsonWebKey<JS, JT, JU>,
|
|
RT: ResponseType,
|
|
S: SubjectIdentifierType,
|
|
{
|
|
#[allow(clippy::cognitive_complexity)]
|
|
fn serialize<SE>(&self, serializer: SE) -> Result<SE::Ok, SE::Error>
|
|
where
|
|
SE: Serializer,
|
|
{
|
|
serialize_fields! {
|
|
self -> serializer {
|
|
[redirect_uris]
|
|
[Option(response_types)]
|
|
[Option(grant_types)]
|
|
[Option(application_type)]
|
|
[Option(contacts)]
|
|
[LanguageTag(client_name)]
|
|
[LanguageTag(logo_uri)]
|
|
[LanguageTag(client_uri)]
|
|
[LanguageTag(policy_uri)]
|
|
[LanguageTag(tos_uri)]
|
|
[Option(jwks_uri)]
|
|
[Option(jwks)]
|
|
[Option(sector_identifier_uri)]
|
|
[Option(subject_type)]
|
|
[Option(id_token_signed_response_alg)]
|
|
[Option(id_token_encrypted_response_alg)]
|
|
[Option(id_token_encrypted_response_enc)]
|
|
[Option(userinfo_signed_response_alg)]
|
|
[Option(userinfo_encrypted_response_alg)]
|
|
[Option(userinfo_encrypted_response_enc)]
|
|
[Option(request_object_signing_alg)]
|
|
[Option(request_object_encryption_alg)]
|
|
[Option(request_object_encryption_enc)]
|
|
[Option(token_endpoint_auth_method)]
|
|
[Option(token_endpoint_auth_signing_alg)]
|
|
[Option(Seconds(default_max_age))]
|
|
[Option(require_auth_time)]
|
|
[Option(default_acr_values)]
|
|
[Option(initiate_login_uri)]
|
|
[Option(request_uris)]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
///
|
|
/// Dynamic client registration request.
|
|
///
|
|
#[derive(Clone, Debug)]
|
|
pub struct ClientRegistrationRequest<AC, AR, AT, CA, ET, G, JE, JK, JS, JT, JU, K, RT, S>
|
|
where
|
|
AC: AdditionalClientMetadata,
|
|
AR: AdditionalClientRegistrationResponse,
|
|
AT: ApplicationType,
|
|
CA: ClientAuthMethod,
|
|
ET: RegisterErrorResponseType,
|
|
G: GrantType,
|
|
JE: JweContentEncryptionAlgorithm<JT>,
|
|
JK: JweKeyManagementAlgorithm,
|
|
JS: JwsSigningAlgorithm<JT>,
|
|
JT: JsonWebKeyType,
|
|
JU: JsonWebKeyUse,
|
|
K: JsonWebKey<JS, JT, JU>,
|
|
RT: ResponseType,
|
|
S: SubjectIdentifierType,
|
|
{
|
|
client_metadata: ClientMetadata<AC, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>,
|
|
initial_access_token: Option<AccessToken>,
|
|
_phantom: PhantomData<(AR, ET)>,
|
|
}
|
|
impl<AC, AR, AT, CA, ET, G, JE, JK, JS, JT, JU, K, RT, S>
|
|
ClientRegistrationRequest<AC, AR, AT, CA, ET, G, JE, JK, JS, JT, JU, K, RT, S>
|
|
where
|
|
AC: AdditionalClientMetadata,
|
|
AR: AdditionalClientRegistrationResponse,
|
|
AT: ApplicationType,
|
|
CA: ClientAuthMethod,
|
|
ET: RegisterErrorResponseType + Send + Sync,
|
|
G: GrantType,
|
|
JE: JweContentEncryptionAlgorithm<JT>,
|
|
JK: JweKeyManagementAlgorithm,
|
|
JS: JwsSigningAlgorithm<JT>,
|
|
JT: JsonWebKeyType,
|
|
JU: JsonWebKeyUse,
|
|
K: JsonWebKey<JS, JT, JU>,
|
|
RT: ResponseType,
|
|
S: SubjectIdentifierType + Send + Sync,
|
|
{
|
|
///
|
|
/// Instantiates a new dynamic client registration request.
|
|
///
|
|
pub fn new(redirect_uris: Vec<RedirectUrl>, additional_metadata: AC) -> Self {
|
|
Self {
|
|
client_metadata: ClientMetadata::new(redirect_uris, additional_metadata),
|
|
initial_access_token: None,
|
|
_phantom: PhantomData,
|
|
}
|
|
}
|
|
|
|
///
|
|
/// Submits this request to the specified registration endpoint using the specified synchronous
|
|
/// HTTP client.
|
|
///
|
|
pub fn register<HC, RE>(
|
|
&self,
|
|
registration_endpoint: &RegistrationUrl,
|
|
http_client: HC,
|
|
) -> Result<
|
|
ClientRegistrationResponse<AC, AR, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>,
|
|
ClientRegistrationError<ET, RE>,
|
|
>
|
|
where
|
|
HC: FnOnce(HttpRequest) -> Result<HttpResponse, RE>,
|
|
RE: std::error::Error + 'static,
|
|
{
|
|
self.prepare_registration(registration_endpoint)
|
|
.and_then(|http_request| {
|
|
http_client(http_request).map_err(ClientRegistrationError::Request)
|
|
})
|
|
.and_then(Self::register_response)
|
|
}
|
|
|
|
///
|
|
/// Submits this request to the specified registration endpoint using the specified asynchronous
|
|
/// HTTP client.
|
|
///
|
|
pub async fn register_async<F, HC, RE>(
|
|
&self,
|
|
registration_endpoint: &RegistrationUrl,
|
|
http_client: HC,
|
|
) -> Result<
|
|
ClientRegistrationResponse<AC, AR, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>,
|
|
ClientRegistrationError<ET, RE>,
|
|
>
|
|
where
|
|
F: Future<Output = Result<HttpResponse, RE>>,
|
|
HC: FnOnce(HttpRequest) -> F,
|
|
RE: std::error::Error + 'static,
|
|
{
|
|
let http_request = self.prepare_registration(registration_endpoint)?;
|
|
let http_response = http_client(http_request)
|
|
.await
|
|
.map_err(ClientRegistrationError::Request)?;
|
|
Self::register_response(http_response)
|
|
}
|
|
|
|
fn prepare_registration<RE>(
|
|
&self,
|
|
registration_endpoint: &RegistrationUrl,
|
|
) -> Result<HttpRequest, ClientRegistrationError<ET, RE>>
|
|
where
|
|
RE: std::error::Error + 'static,
|
|
{
|
|
let request_json = serde_json::to_string(self.client_metadata())
|
|
.map_err(ClientRegistrationError::Serialize)?
|
|
.into_bytes();
|
|
|
|
let auth_header_opt = self.initial_access_token().map(auth_bearer);
|
|
|
|
let mut headers = HeaderMap::new();
|
|
headers.append(ACCEPT, HeaderValue::from_static(MIME_TYPE_JSON));
|
|
headers.append(CONTENT_TYPE, HeaderValue::from_static(MIME_TYPE_JSON));
|
|
if let Some((header, value)) = auth_header_opt {
|
|
headers.append(header, value);
|
|
}
|
|
|
|
Ok(HttpRequest {
|
|
url: registration_endpoint.url().clone(),
|
|
method: Method::POST,
|
|
headers,
|
|
body: request_json,
|
|
})
|
|
}
|
|
|
|
fn register_response<RE>(
|
|
http_response: HttpResponse,
|
|
) -> Result<
|
|
ClientRegistrationResponse<AC, AR, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>,
|
|
ClientRegistrationError<ET, RE>,
|
|
>
|
|
where
|
|
RE: std::error::Error + 'static,
|
|
{
|
|
// TODO: check for WWW-Authenticate response header if bearer auth was used (see
|
|
// https://tools.ietf.org/html/rfc6750#section-3)
|
|
// TODO: other necessary response validation? check spec
|
|
|
|
// Spec says that a successful response SHOULD use 201 Created, and a registration error
|
|
// condition returns (no "SHOULD") 400 Bad Request. For now, only accept these two status
|
|
// codes. We may need to relax the success status to improve interoperability.
|
|
if http_response.status_code != StatusCode::CREATED
|
|
&& http_response.status_code != StatusCode::BAD_REQUEST
|
|
{
|
|
return Err(ClientRegistrationError::Response(
|
|
http_response.status_code,
|
|
http_response.body,
|
|
"unexpected HTTP status code".to_string(),
|
|
));
|
|
}
|
|
|
|
check_content_type(&http_response.headers, MIME_TYPE_JSON).map_err(|err_msg| {
|
|
ClientRegistrationError::Response(
|
|
http_response.status_code,
|
|
http_response.body.clone(),
|
|
err_msg,
|
|
)
|
|
})?;
|
|
|
|
let response_body = String::from_utf8(http_response.body).map_err(|parse_error| {
|
|
ClientRegistrationError::Other(format!(
|
|
"couldn't parse response as UTF-8: {}",
|
|
parse_error
|
|
))
|
|
})?;
|
|
|
|
if http_response.status_code == StatusCode::BAD_REQUEST {
|
|
let response_error: StandardErrorResponse<ET> = serde_path_to_error::deserialize(
|
|
&mut serde_json::Deserializer::from_str(&response_body),
|
|
)
|
|
.map_err(ClientRegistrationError::Parse)?;
|
|
return Err(ClientRegistrationError::ServerResponse(response_error));
|
|
}
|
|
|
|
serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_str(&response_body))
|
|
.map_err(ClientRegistrationError::Parse)
|
|
}
|
|
|
|
///
|
|
/// Returns the client metadata associated with this registration request.
|
|
///
|
|
pub fn client_metadata(&self) -> &ClientMetadata<AC, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S> {
|
|
&self.client_metadata
|
|
}
|
|
|
|
///
|
|
/// Returns the initial access token associated with this registration request.
|
|
///
|
|
pub fn initial_access_token(&self) -> Option<&AccessToken> {
|
|
self.initial_access_token.as_ref()
|
|
}
|
|
///
|
|
/// Sets the initial access token for this request.
|
|
///
|
|
pub fn set_initial_access_token(mut self, access_token: Option<AccessToken>) -> Self {
|
|
self.initial_access_token = access_token;
|
|
self
|
|
}
|
|
|
|
field_getters_setters![
|
|
pub self [self.client_metadata.standard_metadata] ["client metadata value"] {
|
|
set_redirect_uris -> redirect_uris[Vec<RedirectUrl>],
|
|
set_response_types -> response_types[Option<Vec<ResponseTypes<RT>>>],
|
|
set_grant_types -> grant_types[Option<Vec<G>>],
|
|
set_application_type -> application_type[Option<AT>],
|
|
set_contacts -> contacts[Option<Vec<ClientContactEmail>>],
|
|
set_client_name -> client_name[Option<LocalizedClaim<ClientName>>],
|
|
set_logo_uri -> logo_uri[Option<LocalizedClaim<LogoUrl>>],
|
|
set_client_uri -> client_uri[Option<LocalizedClaim<ClientUrl>>],
|
|
set_policy_uri -> policy_uri[Option<LocalizedClaim<PolicyUrl>>],
|
|
set_tos_uri -> tos_uri[Option<LocalizedClaim<ToSUrl>>],
|
|
set_jwks_uri -> jwks_uri[Option<JsonWebKeySetUrl>],
|
|
set_jwks -> jwks[Option<JsonWebKeySet<JS, JT, JU, K>>],
|
|
set_sector_identifier_uri -> sector_identifier_uri[Option<SectorIdentifierUrl>],
|
|
set_subject_type -> subject_type[Option<S>],
|
|
set_id_token_signed_response_alg -> id_token_signed_response_alg[Option<JS>],
|
|
set_id_token_encrypted_response_alg -> id_token_encrypted_response_alg[Option<JK>],
|
|
set_id_token_encrypted_response_enc -> id_token_encrypted_response_enc[Option<JE>],
|
|
set_userinfo_signed_response_alg -> userinfo_signed_response_alg[Option<JS>],
|
|
set_userinfo_encrypted_response_alg -> userinfo_encrypted_response_alg[Option<JK>],
|
|
set_userinfo_encrypted_response_enc -> userinfo_encrypted_response_enc[Option<JE>],
|
|
set_request_object_signing_alg -> request_object_signing_alg[Option<JS>],
|
|
set_request_object_encryption_alg -> request_object_encryption_alg[Option<JK>],
|
|
set_request_object_encryption_enc -> request_object_encryption_enc[Option<JE>],
|
|
set_token_endpoint_auth_method -> token_endpoint_auth_method[Option<CA>],
|
|
set_token_endpoint_auth_signing_alg -> token_endpoint_auth_signing_alg[Option<JS>],
|
|
set_default_max_age -> default_max_age[Option<Duration>],
|
|
set_require_auth_time -> require_auth_time[Option<bool>],
|
|
set_default_acr_values -> default_acr_values[Option<Vec<AuthenticationContextClass>>],
|
|
set_initiate_login_uri -> initiate_login_uri[Option<InitiateLoginUrl>],
|
|
set_request_uris -> request_uris[Option<Vec<RequestUrl>>],
|
|
}
|
|
];
|
|
|
|
///
|
|
/// Returns additional client metadata fields.
|
|
///
|
|
pub fn additional_metadata(&self) -> &AC {
|
|
&self.client_metadata.additional_metadata
|
|
}
|
|
///
|
|
/// Returns mutable additional client metadata fields.
|
|
///
|
|
pub fn additional_metadata_mut(&mut self) -> &mut AC {
|
|
&mut self.client_metadata.additional_metadata
|
|
}
|
|
}
|
|
|
|
///
|
|
/// Trait for adding extra fields to the [`ClientRegistrationResponse`].
|
|
///
|
|
pub trait AdditionalClientRegistrationResponse: Debug + DeserializeOwned + Serialize {}
|
|
|
|
// In order to support serde flatten, this must be an empty struct rather than an empty
|
|
// tuple struct.
|
|
///
|
|
/// Empty (default) extra [`ClientRegistrationResponse`] fields.
|
|
///
|
|
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
|
|
pub struct EmptyAdditionalClientRegistrationResponse {}
|
|
impl AdditionalClientRegistrationResponse for EmptyAdditionalClientRegistrationResponse {}
|
|
|
|
///
|
|
/// Response to a dynamic client registration request.
|
|
///
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct ClientRegistrationResponse<AC, AR, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>
|
|
where
|
|
AC: AdditionalClientMetadata,
|
|
AR: AdditionalClientRegistrationResponse,
|
|
AT: ApplicationType,
|
|
CA: ClientAuthMethod,
|
|
G: GrantType,
|
|
JE: JweContentEncryptionAlgorithm<JT>,
|
|
JK: JweKeyManagementAlgorithm,
|
|
JS: JwsSigningAlgorithm<JT>,
|
|
JT: JsonWebKeyType,
|
|
JU: JsonWebKeyUse,
|
|
K: JsonWebKey<JS, JT, JU>,
|
|
RT: ResponseType,
|
|
S: SubjectIdentifierType,
|
|
{
|
|
client_id: ClientId,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
client_secret: Option<ClientSecret>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
registration_access_token: Option<RegistrationAccessToken>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
registration_client_uri: Option<ClientConfigUrl>,
|
|
#[serde(
|
|
skip_serializing_if = "Option::is_none",
|
|
with = "serde_utc_seconds_opt",
|
|
default
|
|
)]
|
|
client_id_issued_at: Option<DateTime<Utc>>,
|
|
#[serde(
|
|
skip_serializing_if = "Option::is_none",
|
|
with = "serde_utc_seconds_opt",
|
|
default
|
|
)]
|
|
client_secret_expires_at: Option<DateTime<Utc>>,
|
|
#[serde(bound = "AC: AdditionalClientMetadata", flatten)]
|
|
client_metadata: ClientMetadata<AC, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>,
|
|
|
|
#[serde(bound = "AR: AdditionalClientRegistrationResponse", flatten)]
|
|
additional_response: AR,
|
|
}
|
|
impl<AC, AR, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>
|
|
ClientRegistrationResponse<AC, AR, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>
|
|
where
|
|
AC: AdditionalClientMetadata,
|
|
AR: AdditionalClientRegistrationResponse,
|
|
AT: ApplicationType,
|
|
CA: ClientAuthMethod,
|
|
G: GrantType,
|
|
JE: JweContentEncryptionAlgorithm<JT>,
|
|
JK: JweKeyManagementAlgorithm,
|
|
JS: JwsSigningAlgorithm<JT>,
|
|
JT: JsonWebKeyType,
|
|
JU: JsonWebKeyUse,
|
|
K: JsonWebKey<JS, JT, JU>,
|
|
RT: ResponseType,
|
|
S: SubjectIdentifierType,
|
|
{
|
|
///
|
|
/// Instantiates a new dynamic client registration response.
|
|
///
|
|
pub fn new(
|
|
client_id: ClientId,
|
|
redirect_uris: Vec<RedirectUrl>,
|
|
additional_metadata: AC,
|
|
additional_response: AR,
|
|
) -> Self {
|
|
Self {
|
|
client_id,
|
|
client_secret: None,
|
|
registration_access_token: None,
|
|
registration_client_uri: None,
|
|
client_id_issued_at: None,
|
|
client_secret_expires_at: None,
|
|
client_metadata: ClientMetadata::new(redirect_uris, additional_metadata),
|
|
additional_response,
|
|
}
|
|
}
|
|
|
|
///
|
|
/// Instantiates a new dynamic client registration response using the specified client metadata.
|
|
///
|
|
pub fn from_client_metadata(
|
|
client_id: ClientId,
|
|
client_metadata: ClientMetadata<AC, AT, CA, G, JE, JK, JS, JT, JU, K, RT, S>,
|
|
additional_response: AR,
|
|
) -> Self {
|
|
Self {
|
|
client_id,
|
|
client_secret: None,
|
|
registration_access_token: None,
|
|
registration_client_uri: None,
|
|
client_id_issued_at: None,
|
|
client_secret_expires_at: None,
|
|
client_metadata,
|
|
additional_response,
|
|
}
|
|
}
|
|
|
|
field_getters_setters![
|
|
pub self [self] ["response field"] {
|
|
set_client_id -> client_id[ClientId],
|
|
set_client_secret -> client_secret[Option<ClientSecret>],
|
|
set_registration_access_token
|
|
-> registration_access_token[Option<RegistrationAccessToken>],
|
|
set_registration_client_uri -> registration_client_uri[Option<ClientConfigUrl>],
|
|
set_client_id_issued_at -> client_id_issued_at[Option<DateTime<Utc>>],
|
|
set_client_secret_expires_at -> client_secret_expires_at[Option<DateTime<Utc>>],
|
|
}
|
|
];
|
|
|
|
field_getters_setters![
|
|
pub self [self.client_metadata.standard_metadata] ["client metadata value"] {
|
|
set_redirect_uris -> redirect_uris[Vec<RedirectUrl>],
|
|
set_response_types -> response_types[Option<Vec<ResponseTypes<RT>>>],
|
|
set_grant_types -> grant_types[Option<Vec<G>>],
|
|
set_application_type -> application_type[Option<AT>],
|
|
set_contacts -> contacts[Option<Vec<ClientContactEmail>>],
|
|
set_client_name -> client_name[Option<LocalizedClaim<ClientName>>],
|
|
set_logo_uri -> logo_uri[Option<LocalizedClaim<LogoUrl>>],
|
|
set_client_uri -> client_uri[Option<LocalizedClaim<ClientUrl>>],
|
|
set_policy_uri -> policy_uri[Option<LocalizedClaim<PolicyUrl>>],
|
|
set_tos_uri -> tos_uri[Option<LocalizedClaim<ToSUrl>>],
|
|
set_jwks_uri -> jwks_uri[Option<JsonWebKeySetUrl>],
|
|
set_jwks -> jwks[Option<JsonWebKeySet<JS, JT, JU, K>>],
|
|
set_sector_identifier_uri -> sector_identifier_uri[Option<SectorIdentifierUrl>],
|
|
set_subject_type -> subject_type[Option<S>],
|
|
set_id_token_signed_response_alg -> id_token_signed_response_alg[Option<JS>],
|
|
set_id_token_encrypted_response_alg -> id_token_encrypted_response_alg[Option<JK>],
|
|
set_id_token_encrypted_response_enc -> id_token_encrypted_response_enc[Option<JE>],
|
|
set_userinfo_signed_response_alg -> userinfo_signed_response_alg[Option<JS>],
|
|
set_userinfo_encrypted_response_alg -> userinfo_encrypted_response_alg[Option<JK>],
|
|
set_userinfo_encrypted_response_enc -> userinfo_encrypted_response_enc[Option<JE>],
|
|
set_request_object_signing_alg -> request_object_signing_alg[Option<JS>],
|
|
set_request_object_encryption_alg -> request_object_encryption_alg[Option<JK>],
|
|
set_request_object_encryption_enc -> request_object_encryption_enc[Option<JE>],
|
|
set_token_endpoint_auth_method -> token_endpoint_auth_method[Option<CA>],
|
|
set_token_endpoint_auth_signing_alg -> token_endpoint_auth_signing_alg[Option<JS>],
|
|
set_default_max_age -> default_max_age[Option<Duration>],
|
|
set_require_auth_time -> require_auth_time[Option<bool>],
|
|
set_default_acr_values -> default_acr_values[Option<Vec<AuthenticationContextClass>>],
|
|
set_initiate_login_uri -> initiate_login_uri[Option<InitiateLoginUrl>],
|
|
set_request_uris -> request_uris[Option<Vec<RequestUrl>>],
|
|
}
|
|
];
|
|
|
|
///
|
|
/// Returns additional client metadata fields.
|
|
///
|
|
pub fn additional_metadata(&self) -> &AC {
|
|
&self.client_metadata.additional_metadata
|
|
}
|
|
///
|
|
/// Returns mutable additional client metadata fields.
|
|
///
|
|
pub fn additional_metadata_mut(&mut self) -> &mut AC {
|
|
&mut self.client_metadata.additional_metadata
|
|
}
|
|
|
|
///
|
|
/// Returns additional response fields.
|
|
///
|
|
pub fn additional_response(&self) -> &AR {
|
|
&self.additional_response
|
|
}
|
|
///
|
|
/// Returns mutable additional response fields.
|
|
///
|
|
pub fn additional_response_mut(&mut self) -> &mut AR {
|
|
&mut self.additional_response
|
|
}
|
|
}
|
|
|
|
// TODO: implement client configuration endpoint request (Section 4)
|
|
|
|
///
|
|
/// Trait representing an error returned by the dynamic client registration endpoint.
|
|
///
|
|
pub trait RegisterErrorResponseType: ErrorResponseType + 'static {}
|
|
|
|
///
|
|
/// Error registering a client.
|
|
///
|
|
#[derive(Debug, Error)]
|
|
#[non_exhaustive]
|
|
pub enum ClientRegistrationError<T, RE>
|
|
where
|
|
RE: std::error::Error + 'static,
|
|
T: RegisterErrorResponseType,
|
|
{
|
|
///
|
|
/// An unexpected error occurred.
|
|
///
|
|
#[error("Other error: {0}")]
|
|
Other(String),
|
|
///
|
|
/// Failed to parse server response.
|
|
///
|
|
#[error("Failed to parse server response")]
|
|
Parse(#[source] serde_path_to_error::Error<serde_json::Error>),
|
|
///
|
|
/// An error occurred while sending the request or receiving the response (e.g., network
|
|
/// connectivity failed).
|
|
///
|
|
#[error("Request failed")]
|
|
Request(#[source] RE),
|
|
///
|
|
/// Server returned an invalid response.
|
|
///
|
|
#[error("Server returned invalid response: {2}")]
|
|
Response(StatusCode, Vec<u8>, String),
|
|
///
|
|
/// Failed to serialize client metadata.
|
|
///
|
|
#[error("Failed to serialize client metadata")]
|
|
Serialize(#[source] serde_json::Error),
|
|
///
|
|
/// Server returned an error.
|
|
///
|
|
#[error("Server returned error")]
|
|
ServerResponse(StandardErrorResponse<T>),
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::time::Duration;
|
|
|
|
use chrono::{TimeZone, Utc};
|
|
use itertools::sorted;
|
|
use oauth2::{ClientId, RedirectUrl};
|
|
|
|
use crate::core::{
|
|
CoreApplicationType, CoreClientAuthMethod, CoreClientMetadata,
|
|
CoreClientRegistrationResponse, CoreGrantType, CoreJweContentEncryptionAlgorithm,
|
|
CoreJweKeyManagementAlgorithm, CoreJwsSigningAlgorithm, CoreResponseType,
|
|
CoreSubjectIdentifierType,
|
|
};
|
|
use crate::jwt::tests::TEST_RSA_PUB_KEY;
|
|
use crate::{
|
|
AuthenticationContextClass, ClientConfigUrl, ClientContactEmail, ClientName, ClientUrl,
|
|
JsonWebKeySet, JsonWebKeySetUrl, LanguageTag, LogoUrl, PolicyUrl, RequestUrl,
|
|
ResponseTypes, SectorIdentifierUrl, ToSUrl,
|
|
};
|
|
|
|
#[test]
|
|
fn test_metadata_serialization() {
|
|
// `jwks_uri` and `jwks` aren't supposed to be used together, but this test is just for
|
|
// serialization/deserialization.
|
|
let json_response = format!("{{
|
|
\"redirect_uris\": [\"https://example.com/redirect-1\", \"https://example.com/redirect-2\"],
|
|
\"response_types\": [\"code\", \"code token id_token\"],
|
|
\"grant_types\": [\"authorization_code\", \"client_credentials\", \"implicit\", \
|
|
\"password\", \"refresh_token\"],
|
|
\"application_type\": \"web\",
|
|
\"contacts\": [\"user@example.com\", \"admin@openidconnect.local\"],
|
|
\"client_name\": \"Example\",
|
|
\"client_name#es\": \"Ejemplo\",
|
|
\"logo_uri\": \"https://example.com/logo.png\",
|
|
\"logo_uri#fr\": \"https://example.com/logo-fr.png\",
|
|
\"client_uri\": \"https://example.com/client-app\",
|
|
\"client_uri#de\": \"https://example.com/client-app-de\",
|
|
\"policy_uri\": \"https://example.com/policy\",
|
|
\"policy_uri#sr-Latn\": \"https://example.com/policy-sr-latin\",
|
|
\"tos_uri\": \"https://example.com/tos\",
|
|
\"tos_uri#sr-Cyrl\": \"https://example.com/tos-sr-cyrl\",
|
|
\"jwks_uri\": \"https://example.com/jwks\",
|
|
\"jwks\": {{\"keys\": [{}]}},
|
|
\"sector_identifier_uri\": \"https://example.com/sector\",
|
|
\"subject_type\": \"pairwise\",
|
|
\"id_token_signed_response_alg\": \"HS256\",
|
|
\"id_token_encrypted_response_alg\": \"RSA1_5\",
|
|
\"id_token_encrypted_response_enc\": \"A128CBC-HS256\",
|
|
\"userinfo_signed_response_alg\": \"RS384\",
|
|
\"userinfo_encrypted_response_alg\": \"RSA-OAEP\",
|
|
\"userinfo_encrypted_response_enc\": \"A256CBC-HS512\",
|
|
\"request_object_signing_alg\": \"ES512\",
|
|
\"request_object_encryption_alg\": \"ECDH-ES+A128KW\",
|
|
\"request_object_encryption_enc\": \"A256GCM\",
|
|
\"token_endpoint_auth_method\": \"client_secret_basic\",
|
|
\"token_endpoint_auth_signing_alg\": \"PS512\",
|
|
\"default_max_age\": 3600,
|
|
\"require_auth_time\": true,
|
|
\"default_acr_values\": [\"0\", \"urn:mace:incommon:iap:silver\", \
|
|
\"urn:mace:incommon:iap:bronze\"],
|
|
\"initiate_login_uri\": \"https://example.com/login\",
|
|
\"request_uris\": [\"https://example.com/request-1\", \"https://example.com/request-2\"]
|
|
}}", TEST_RSA_PUB_KEY);
|
|
|
|
let client_metadata: CoreClientMetadata = serde_json::from_str(&json_response).unwrap();
|
|
|
|
assert_eq!(
|
|
*client_metadata.redirect_uris(),
|
|
vec![
|
|
RedirectUrl::new("https://example.com/redirect-1".to_string()).unwrap(),
|
|
RedirectUrl::new("https://example.com/redirect-2".to_string()).unwrap(),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.response_types().unwrap(),
|
|
vec![
|
|
ResponseTypes::new(vec![CoreResponseType::Code]),
|
|
ResponseTypes::new(vec![
|
|
CoreResponseType::Code,
|
|
CoreResponseType::Token,
|
|
CoreResponseType::IdToken,
|
|
]),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
client_metadata.grant_types().unwrap(),
|
|
&vec![
|
|
CoreGrantType::AuthorizationCode,
|
|
CoreGrantType::ClientCredentials,
|
|
CoreGrantType::Implicit,
|
|
CoreGrantType::Password,
|
|
CoreGrantType::RefreshToken,
|
|
]
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.application_type().unwrap(),
|
|
CoreApplicationType::Web
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.contacts().unwrap(),
|
|
vec![
|
|
ClientContactEmail::new("user@example.com".to_string()),
|
|
ClientContactEmail::new("admin@openidconnect.local".to_string()),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
sorted(client_metadata.client_name().unwrap().clone())
|
|
.collect::<Vec<(Option<LanguageTag>, ClientName)>>(),
|
|
vec![
|
|
(None, ClientName::new("Example".to_string())),
|
|
(
|
|
Some(LanguageTag::new("es".to_string())),
|
|
ClientName::new("Ejemplo".to_string()),
|
|
),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
sorted(client_metadata.logo_uri().unwrap().clone())
|
|
.collect::<Vec<(Option<LanguageTag>, LogoUrl)>>(),
|
|
vec![
|
|
(
|
|
None,
|
|
LogoUrl::new("https://example.com/logo.png".to_string()).unwrap(),
|
|
),
|
|
(
|
|
Some(LanguageTag::new("fr".to_string())),
|
|
LogoUrl::new("https://example.com/logo-fr.png".to_string()).unwrap(),
|
|
),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
sorted(client_metadata.client_uri().unwrap().clone())
|
|
.collect::<Vec<(Option<LanguageTag>, ClientUrl)>>(),
|
|
vec![
|
|
(
|
|
None,
|
|
ClientUrl::new("https://example.com/client-app".to_string()).unwrap(),
|
|
),
|
|
(
|
|
Some(LanguageTag::new("de".to_string())),
|
|
ClientUrl::new("https://example.com/client-app-de".to_string()).unwrap(),
|
|
),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
sorted(client_metadata.policy_uri().unwrap().clone())
|
|
.collect::<Vec<(Option<LanguageTag>, PolicyUrl)>>(),
|
|
vec![
|
|
(
|
|
None,
|
|
PolicyUrl::new("https://example.com/policy".to_string()).unwrap(),
|
|
),
|
|
(
|
|
Some(LanguageTag::new("sr-Latn".to_string())),
|
|
PolicyUrl::new("https://example.com/policy-sr-latin".to_string()).unwrap(),
|
|
),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
sorted(client_metadata.tos_uri().unwrap().clone())
|
|
.collect::<Vec<(Option<LanguageTag>, ToSUrl)>>(),
|
|
vec![
|
|
(
|
|
None,
|
|
ToSUrl::new("https://example.com/tos".to_string()).unwrap(),
|
|
),
|
|
(
|
|
Some(LanguageTag::new("sr-Cyrl".to_string())),
|
|
ToSUrl::new("https://example.com/tos-sr-cyrl".to_string()).unwrap(),
|
|
),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.jwks_uri().unwrap(),
|
|
JsonWebKeySetUrl::new("https://example.com/jwks".to_string()).unwrap()
|
|
);
|
|
assert_eq!(
|
|
client_metadata.jwks(),
|
|
Some(&JsonWebKeySet::new(vec![serde_json::from_str(
|
|
TEST_RSA_PUB_KEY
|
|
)
|
|
.unwrap()],))
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.sector_identifier_uri().unwrap(),
|
|
SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap()
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.subject_type().unwrap(),
|
|
CoreSubjectIdentifierType::Pairwise
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.id_token_signed_response_alg().unwrap(),
|
|
CoreJwsSigningAlgorithm::HmacSha256
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.id_token_encrypted_response_alg().unwrap(),
|
|
CoreJweKeyManagementAlgorithm::RsaPkcs1V15
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.id_token_encrypted_response_enc().unwrap(),
|
|
CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.userinfo_signed_response_alg().unwrap(),
|
|
CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.userinfo_encrypted_response_alg().unwrap(),
|
|
CoreJweKeyManagementAlgorithm::RsaOaep
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.userinfo_encrypted_response_enc().unwrap(),
|
|
CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.request_object_signing_alg().unwrap(),
|
|
CoreJwsSigningAlgorithm::EcdsaP521Sha512
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.request_object_encryption_alg().unwrap(),
|
|
CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.request_object_encryption_enc().unwrap(),
|
|
CoreJweContentEncryptionAlgorithm::Aes256Gcm
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.token_endpoint_auth_method().unwrap(),
|
|
CoreClientAuthMethod::ClientSecretBasic
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.token_endpoint_auth_signing_alg().unwrap(),
|
|
CoreJwsSigningAlgorithm::RsaSsaPssSha512
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.default_max_age().unwrap(),
|
|
Duration::from_secs(3600)
|
|
);
|
|
assert!(client_metadata.require_auth_time().unwrap());
|
|
assert_eq!(
|
|
*client_metadata.default_acr_values().unwrap(),
|
|
vec![
|
|
AuthenticationContextClass::new("0".to_string()),
|
|
AuthenticationContextClass::new("urn:mace:incommon:iap:silver".to_string()),
|
|
AuthenticationContextClass::new("urn:mace:incommon:iap:bronze".to_string()),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.sector_identifier_uri().unwrap(),
|
|
SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap()
|
|
);
|
|
assert_eq!(
|
|
*client_metadata.request_uris().unwrap(),
|
|
vec![
|
|
RequestUrl::new("https://example.com/request-1".to_string()).unwrap(),
|
|
RequestUrl::new("https://example.com/request-2".to_string()).unwrap(),
|
|
]
|
|
);
|
|
let serialized_json = serde_json::to_string(&client_metadata).unwrap();
|
|
|
|
assert_eq!(
|
|
client_metadata,
|
|
serde_json::from_str(&serialized_json).unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_metadata_serialization_minimal() {
|
|
let json_response = "{\"redirect_uris\": [\"https://example.com/redirect-1\"]}";
|
|
|
|
let client_metadata: CoreClientMetadata = serde_json::from_str(json_response).unwrap();
|
|
|
|
assert_eq!(
|
|
*client_metadata.redirect_uris(),
|
|
vec![RedirectUrl::new("https://example.com/redirect-1".to_string()).unwrap(),]
|
|
);
|
|
assert_eq!(client_metadata.response_types(), None);
|
|
assert_eq!(client_metadata.grant_types(), None);
|
|
assert_eq!(client_metadata.application_type(), None);
|
|
assert_eq!(client_metadata.contacts(), None);
|
|
assert_eq!(client_metadata.client_name(), None);
|
|
assert_eq!(client_metadata.logo_uri(), None);
|
|
assert_eq!(client_metadata.client_uri(), None);
|
|
assert_eq!(client_metadata.policy_uri(), None);
|
|
assert_eq!(client_metadata.tos_uri(), None);
|
|
assert_eq!(client_metadata.jwks_uri(), None);
|
|
assert_eq!(client_metadata.jwks(), None);
|
|
assert_eq!(client_metadata.sector_identifier_uri(), None);
|
|
assert_eq!(client_metadata.subject_type(), None);
|
|
assert_eq!(client_metadata.id_token_signed_response_alg(), None);
|
|
assert_eq!(client_metadata.id_token_encrypted_response_alg(), None);
|
|
assert_eq!(client_metadata.id_token_encrypted_response_enc(), None);
|
|
assert_eq!(client_metadata.userinfo_signed_response_alg(), None);
|
|
assert_eq!(client_metadata.userinfo_encrypted_response_alg(), None);
|
|
assert_eq!(client_metadata.userinfo_encrypted_response_enc(), None);
|
|
assert_eq!(client_metadata.request_object_signing_alg(), None);
|
|
assert_eq!(client_metadata.request_object_encryption_alg(), None);
|
|
assert_eq!(client_metadata.request_object_encryption_enc(), None);
|
|
assert_eq!(client_metadata.token_endpoint_auth_method(), None);
|
|
assert_eq!(client_metadata.token_endpoint_auth_signing_alg(), None);
|
|
assert_eq!(client_metadata.default_max_age(), None);
|
|
assert_eq!(client_metadata.require_auth_time(), None);
|
|
assert_eq!(client_metadata.default_acr_values(), None);
|
|
assert_eq!(client_metadata.sector_identifier_uri(), None);
|
|
assert_eq!(client_metadata.request_uris(), None);
|
|
|
|
let serialized_json = serde_json::to_string(&client_metadata).unwrap();
|
|
|
|
assert_eq!(
|
|
client_metadata,
|
|
serde_json::from_str(&serialized_json).unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_response_serialization() {
|
|
let json_response = format!("{{
|
|
\"client_id\": \"abcdefgh\",
|
|
\"client_secret\": \"shhhh\",
|
|
\"registration_access_token\": \"use_me_to_update_registration\",
|
|
\"registration_client_uri\": \"https://example-provider.com/registration\",
|
|
\"client_id_issued_at\": 1523953306,
|
|
\"client_secret_expires_at\": 1526545306,
|
|
\"redirect_uris\": [\"https://example.com/redirect-1\", \"https://example.com/redirect-2\"],
|
|
\"response_types\": [\"code\", \"code token id_token\"],
|
|
\"grant_types\": [\"authorization_code\", \"client_credentials\", \"implicit\", \
|
|
\"password\", \"refresh_token\"],
|
|
\"application_type\": \"web\",
|
|
\"contacts\": [\"user@example.com\", \"admin@openidconnect.local\"],
|
|
\"client_name\": \"Example\",
|
|
\"client_name#es\": \"Ejemplo\",
|
|
\"logo_uri\": \"https://example.com/logo.png\",
|
|
\"logo_uri#fr\": \"https://example.com/logo-fr.png\",
|
|
\"client_uri\": \"https://example.com/client-app\",
|
|
\"client_uri#de\": \"https://example.com/client-app-de\",
|
|
\"policy_uri\": \"https://example.com/policy\",
|
|
\"policy_uri#sr-Latn\": \"https://example.com/policy-sr-latin\",
|
|
\"tos_uri\": \"https://example.com/tos\",
|
|
\"tos_uri#sr-Cyrl\": \"https://example.com/tos-sr-cyrl\",
|
|
\"jwks_uri\": \"https://example.com/jwks\",
|
|
\"jwks\": {{\"keys\": [{}]}},
|
|
\"sector_identifier_uri\": \"https://example.com/sector\",
|
|
\"subject_type\": \"pairwise\",
|
|
\"id_token_signed_response_alg\": \"HS256\",
|
|
\"id_token_encrypted_response_alg\": \"RSA1_5\",
|
|
\"id_token_encrypted_response_enc\": \"A128CBC-HS256\",
|
|
\"userinfo_signed_response_alg\": \"RS384\",
|
|
\"userinfo_encrypted_response_alg\": \"RSA-OAEP\",
|
|
\"userinfo_encrypted_response_enc\": \"A256CBC-HS512\",
|
|
\"request_object_signing_alg\": \"ES512\",
|
|
\"request_object_encryption_alg\": \"ECDH-ES+A128KW\",
|
|
\"request_object_encryption_enc\": \"A256GCM\",
|
|
\"token_endpoint_auth_method\": \"client_secret_basic\",
|
|
\"token_endpoint_auth_signing_alg\": \"PS512\",
|
|
\"default_max_age\": 3600,
|
|
\"require_auth_time\": true,
|
|
\"default_acr_values\": [\"0\", \"urn:mace:incommon:iap:silver\", \
|
|
\"urn:mace:incommon:iap:bronze\"],
|
|
\"initiate_login_uri\": \"https://example.com/login\",
|
|
\"request_uris\": [\"https://example.com/request-1\", \"https://example.com/request-2\"]
|
|
}}", TEST_RSA_PUB_KEY);
|
|
|
|
let registration_response: CoreClientRegistrationResponse =
|
|
serde_json::from_str(&json_response).unwrap();
|
|
|
|
assert_eq!(
|
|
*registration_response.client_id(),
|
|
ClientId::new("abcdefgh".to_string())
|
|
);
|
|
assert_eq!(
|
|
*registration_response.client_secret().unwrap().secret(),
|
|
"shhhh"
|
|
);
|
|
assert_eq!(
|
|
*registration_response
|
|
.registration_access_token()
|
|
.unwrap()
|
|
.secret(),
|
|
"use_me_to_update_registration",
|
|
);
|
|
assert_eq!(
|
|
*registration_response.registration_client_uri().unwrap(),
|
|
ClientConfigUrl::new("https://example-provider.com/registration".to_string()).unwrap()
|
|
);
|
|
assert_eq!(
|
|
registration_response.client_id_issued_at().unwrap(),
|
|
Utc.timestamp(1523953306, 0)
|
|
);
|
|
assert_eq!(
|
|
registration_response.client_secret_expires_at().unwrap(),
|
|
Utc.timestamp(1526545306, 0)
|
|
);
|
|
assert_eq!(
|
|
*registration_response.redirect_uris(),
|
|
vec![
|
|
RedirectUrl::new("https://example.com/redirect-1".to_string()).unwrap(),
|
|
RedirectUrl::new("https://example.com/redirect-2".to_string()).unwrap(),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
*registration_response.response_types().unwrap(),
|
|
vec![
|
|
ResponseTypes::new(vec![CoreResponseType::Code]),
|
|
ResponseTypes::new(vec![
|
|
CoreResponseType::Code,
|
|
CoreResponseType::Token,
|
|
CoreResponseType::IdToken,
|
|
]),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
registration_response.grant_types().unwrap(),
|
|
&vec![
|
|
CoreGrantType::AuthorizationCode,
|
|
CoreGrantType::ClientCredentials,
|
|
CoreGrantType::Implicit,
|
|
CoreGrantType::Password,
|
|
CoreGrantType::RefreshToken,
|
|
]
|
|
);
|
|
assert_eq!(
|
|
*registration_response.application_type().unwrap(),
|
|
CoreApplicationType::Web
|
|
);
|
|
assert_eq!(
|
|
*registration_response.contacts().unwrap(),
|
|
vec![
|
|
ClientContactEmail::new("user@example.com".to_string()),
|
|
ClientContactEmail::new("admin@openidconnect.local".to_string()),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
sorted(registration_response.client_name().unwrap().clone())
|
|
.collect::<Vec<(Option<LanguageTag>, ClientName)>>(),
|
|
vec![
|
|
(None, ClientName::new("Example".to_string())),
|
|
(
|
|
Some(LanguageTag::new("es".to_string())),
|
|
ClientName::new("Ejemplo".to_string()),
|
|
),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
sorted(registration_response.logo_uri().unwrap().clone())
|
|
.collect::<Vec<(Option<LanguageTag>, LogoUrl)>>(),
|
|
vec![
|
|
(
|
|
None,
|
|
LogoUrl::new("https://example.com/logo.png".to_string()).unwrap(),
|
|
),
|
|
(
|
|
Some(LanguageTag::new("fr".to_string())),
|
|
LogoUrl::new("https://example.com/logo-fr.png".to_string()).unwrap(),
|
|
),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
sorted(registration_response.client_uri().unwrap().clone())
|
|
.collect::<Vec<(Option<LanguageTag>, ClientUrl)>>(),
|
|
vec![
|
|
(
|
|
None,
|
|
ClientUrl::new("https://example.com/client-app".to_string()).unwrap(),
|
|
),
|
|
(
|
|
Some(LanguageTag::new("de".to_string())),
|
|
ClientUrl::new("https://example.com/client-app-de".to_string()).unwrap(),
|
|
),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
sorted(registration_response.policy_uri().unwrap().clone())
|
|
.collect::<Vec<(Option<LanguageTag>, PolicyUrl)>>(),
|
|
vec![
|
|
(
|
|
None,
|
|
PolicyUrl::new("https://example.com/policy".to_string()).unwrap(),
|
|
),
|
|
(
|
|
Some(LanguageTag::new("sr-Latn".to_string())),
|
|
PolicyUrl::new("https://example.com/policy-sr-latin".to_string()).unwrap(),
|
|
),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
sorted(registration_response.tos_uri().unwrap().clone())
|
|
.collect::<Vec<(Option<LanguageTag>, ToSUrl)>>(),
|
|
vec![
|
|
(
|
|
None,
|
|
ToSUrl::new("https://example.com/tos".to_string()).unwrap(),
|
|
),
|
|
(
|
|
Some(LanguageTag::new("sr-Cyrl".to_string())),
|
|
ToSUrl::new("https://example.com/tos-sr-cyrl".to_string()).unwrap(),
|
|
),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
*registration_response.jwks_uri().unwrap(),
|
|
JsonWebKeySetUrl::new("https://example.com/jwks".to_string()).unwrap()
|
|
);
|
|
assert_eq!(
|
|
registration_response.jwks(),
|
|
Some(&JsonWebKeySet::new(vec![serde_json::from_str(
|
|
TEST_RSA_PUB_KEY
|
|
)
|
|
.unwrap()],)),
|
|
);
|
|
assert_eq!(
|
|
*registration_response.sector_identifier_uri().unwrap(),
|
|
SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap()
|
|
);
|
|
assert_eq!(
|
|
*registration_response.subject_type().unwrap(),
|
|
CoreSubjectIdentifierType::Pairwise
|
|
);
|
|
assert_eq!(
|
|
*registration_response
|
|
.id_token_signed_response_alg()
|
|
.unwrap(),
|
|
CoreJwsSigningAlgorithm::HmacSha256
|
|
);
|
|
assert_eq!(
|
|
*registration_response
|
|
.id_token_encrypted_response_alg()
|
|
.unwrap(),
|
|
CoreJweKeyManagementAlgorithm::RsaPkcs1V15
|
|
);
|
|
assert_eq!(
|
|
*registration_response
|
|
.id_token_encrypted_response_enc()
|
|
.unwrap(),
|
|
CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256
|
|
);
|
|
assert_eq!(
|
|
*registration_response
|
|
.userinfo_signed_response_alg()
|
|
.unwrap(),
|
|
CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384
|
|
);
|
|
assert_eq!(
|
|
*registration_response
|
|
.userinfo_encrypted_response_alg()
|
|
.unwrap(),
|
|
CoreJweKeyManagementAlgorithm::RsaOaep
|
|
);
|
|
assert_eq!(
|
|
*registration_response
|
|
.userinfo_encrypted_response_enc()
|
|
.unwrap(),
|
|
CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512
|
|
);
|
|
assert_eq!(
|
|
*registration_response.request_object_signing_alg().unwrap(),
|
|
CoreJwsSigningAlgorithm::EcdsaP521Sha512
|
|
);
|
|
assert_eq!(
|
|
*registration_response
|
|
.request_object_encryption_alg()
|
|
.unwrap(),
|
|
CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128
|
|
);
|
|
assert_eq!(
|
|
*registration_response
|
|
.request_object_encryption_enc()
|
|
.unwrap(),
|
|
CoreJweContentEncryptionAlgorithm::Aes256Gcm
|
|
);
|
|
assert_eq!(
|
|
*registration_response.token_endpoint_auth_method().unwrap(),
|
|
CoreClientAuthMethod::ClientSecretBasic
|
|
);
|
|
assert_eq!(
|
|
*registration_response
|
|
.token_endpoint_auth_signing_alg()
|
|
.unwrap(),
|
|
CoreJwsSigningAlgorithm::RsaSsaPssSha512
|
|
);
|
|
assert_eq!(
|
|
*registration_response.default_max_age().unwrap(),
|
|
Duration::from_secs(3600)
|
|
);
|
|
assert!(registration_response.require_auth_time().unwrap());
|
|
assert_eq!(
|
|
*registration_response.default_acr_values().unwrap(),
|
|
vec![
|
|
AuthenticationContextClass::new("0".to_string()),
|
|
AuthenticationContextClass::new("urn:mace:incommon:iap:silver".to_string()),
|
|
AuthenticationContextClass::new("urn:mace:incommon:iap:bronze".to_string()),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
*registration_response.sector_identifier_uri().unwrap(),
|
|
SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap()
|
|
);
|
|
assert_eq!(
|
|
*registration_response.request_uris().unwrap(),
|
|
vec![
|
|
RequestUrl::new("https://example.com/request-1".to_string()).unwrap(),
|
|
RequestUrl::new("https://example.com/request-2".to_string()).unwrap(),
|
|
]
|
|
);
|
|
let serialized_json = serde_json::to_string(®istration_response).unwrap();
|
|
|
|
let deserialized: CoreClientRegistrationResponse =
|
|
serde_json::from_str(&serialized_json).unwrap();
|
|
assert_eq!(registration_response.client_id, deserialized.client_id);
|
|
assert_eq!(
|
|
registration_response.client_secret.unwrap().secret(),
|
|
deserialized.client_secret.unwrap().secret(),
|
|
);
|
|
assert_eq!(
|
|
registration_response
|
|
.registration_access_token
|
|
.unwrap()
|
|
.secret(),
|
|
deserialized.registration_access_token.unwrap().secret(),
|
|
);
|
|
assert_eq!(
|
|
registration_response.registration_client_uri,
|
|
deserialized.registration_client_uri,
|
|
);
|
|
assert_eq!(
|
|
registration_response.client_id_issued_at,
|
|
deserialized.client_id_issued_at,
|
|
);
|
|
assert_eq!(
|
|
registration_response.client_secret_expires_at,
|
|
deserialized.client_secret_expires_at,
|
|
);
|
|
assert_eq!(
|
|
registration_response.client_metadata,
|
|
deserialized.client_metadata,
|
|
);
|
|
assert_eq!(
|
|
registration_response.additional_response,
|
|
deserialized.additional_response,
|
|
);
|
|
}
|
|
}
|