From 87f34522c8a8a2f1d187f14540ee1ac01211837d Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 13 Sep 2024 17:23:28 +0200 Subject: [PATCH] Output the registered client metadata in the registration endpoint Fixes #2848 --- crates/data-model/src/oauth2/client.rs | 66 ++++++++++++++++++---- crates/handlers/src/oauth2/registration.rs | 22 +++++++- crates/policy/src/model.rs | 1 + crates/storage-pg/src/oauth2/client.rs | 32 +---------- 4 files changed, 76 insertions(+), 45 deletions(-) diff --git a/crates/data-model/src/oauth2/client.rs b/crates/data-model/src/oauth2/client.rs index d4159b088..73fa0dc0d 100644 --- a/crates/data-model/src/oauth2/client.rs +++ b/crates/data-model/src/oauth2/client.rs @@ -5,12 +5,13 @@ // Please see LICENSE in the repository root for full details. use chrono::{DateTime, Utc}; -use mas_iana::{ - jose::JsonWebSignatureAlg, - oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod}, -}; +use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod}; use mas_jose::jwk::PublicJsonWebKeySet; -use oauth2_types::{oidc::ApplicationType, requests::GrantType}; +use oauth2_types::{ + oidc::ApplicationType, + registration::{ClientMetadata, Localized}, + requests::GrantType, +}; use rand::RngCore; use serde::Serialize; use thiserror::Error; @@ -41,10 +42,6 @@ pub struct Client { /// Array of Redirection URI values used by the Client pub redirect_uris: Vec, - /// Array containing a list of the OAuth 2.0 `response_type` values that the - /// Client is declaring that it will restrict itself to using - pub response_types: Vec, - /// Array containing a list of the OAuth 2.0 Grant Types that the Client is /// declaring that it will restrict itself to using. pub grant_types: Vec, @@ -123,6 +120,55 @@ impl Client { } } + /// Create a client metadata object for this client + pub fn into_metadata(self) -> ClientMetadata { + let (jwks, jwks_uri) = match self.jwks { + Some(JwksOrJwksUri::Jwks(jwks)) => (Some(jwks), None), + Some(JwksOrJwksUri::JwksUri(jwks_uri)) => (None, Some(jwks_uri)), + _ => (None, None), + }; + ClientMetadata { + redirect_uris: Some(self.redirect_uris.clone()), + response_types: None, + grant_types: Some(self.grant_types.into_iter().map(Into::into).collect()), + application_type: self.application_type.clone(), + client_name: self.client_name.map(|n| Localized::new(n, [])), + logo_uri: self.logo_uri.map(|n| Localized::new(n, [])), + client_uri: self.client_uri.map(|n| Localized::new(n, [])), + policy_uri: self.policy_uri.map(|n| Localized::new(n, [])), + tos_uri: self.tos_uri.map(|n| Localized::new(n, [])), + jwks_uri, + jwks, + id_token_signed_response_alg: self.id_token_signed_response_alg, + userinfo_signed_response_alg: self.userinfo_signed_response_alg, + token_endpoint_auth_method: self.token_endpoint_auth_method, + token_endpoint_auth_signing_alg: self.token_endpoint_auth_signing_alg, + initiate_login_uri: self.initiate_login_uri, + contacts: None, + software_id: None, + software_version: None, + sector_identifier_uri: None, + subject_type: None, + id_token_encrypted_response_alg: None, + id_token_encrypted_response_enc: 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, + default_max_age: None, + require_auth_time: None, + default_acr_values: None, + request_uris: None, + require_signed_request_object: None, + require_pushed_authorization_requests: None, + introspection_signed_response_alg: None, + introspection_encrypted_response_alg: None, + introspection_encrypted_response_enc: None, + post_logout_redirect_uris: None, + } + } + #[doc(hidden)] pub fn samples(now: DateTime, rng: &mut impl RngCore) -> Vec { vec![ @@ -136,7 +182,6 @@ impl Client { Url::parse("https://client1.example.com/redirect").unwrap(), Url::parse("https://client1.example.com/redirect2").unwrap(), ], - response_types: vec![OAuthAuthorizationEndpointResponseType::Code], grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken], client_name: Some("Client 1".to_owned()), client_uri: Some(Url::parse("https://client1.example.com").unwrap()), @@ -159,7 +204,6 @@ impl Client { encrypted_client_secret: None, application_type: Some(ApplicationType::Native), redirect_uris: vec![Url::parse("https://client2.example.com/redirect").unwrap()], - response_types: vec![OAuthAuthorizationEndpointResponseType::Code], grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken], client_name: None, client_uri: None, diff --git a/crates/handlers/src/oauth2/registration.rs b/crates/handlers/src/oauth2/registration.rs index 727320dc2..bd608dd69 100644 --- a/crates/handlers/src/oauth2/registration.rs +++ b/crates/handlers/src/oauth2/registration.rs @@ -15,10 +15,12 @@ use oauth2_types::{ errors::{ClientError, ClientErrorCode}, registration::{ ClientMetadata, ClientMetadataVerificationError, ClientRegistrationResponse, Localized, + VerifiedClientMetadata, }, }; use psl::Psl; use rand::distributions::{Alphanumeric, DistString}; +use serde::Serialize; use thiserror::Error; use tracing::info; use url::Url; @@ -149,6 +151,14 @@ impl IntoResponse for RouteError { } } +#[derive(Serialize)] +struct RouteResponse { + #[serde(flatten)] + response: ClientRegistrationResponse, + #[serde(flatten)] + metadata: VerifiedClientMetadata, +} + /// Check if the host of the given URL is a public suffix fn host_is_public_suffix(url: &Url) -> bool { let host = url.host_str().unwrap_or_default().as_bytes(); @@ -282,16 +292,22 @@ pub(crate) async fn post( ) .await?; - repo.save().await?; - let response = ClientRegistrationResponse { - client_id: client.client_id, + client_id: client.client_id.clone(), client_secret, // XXX: we should have a `created_at` field on the clients client_id_issued_at: Some(client.id.datetime().into()), client_secret_expires_at: None, }; + // We round-trip back to the metadata to output it in the response + // This should never fail, as the client is valid + let metadata = client.into_metadata().validate()?; + + repo.save().await?; + + let response = RouteResponse { response, metadata }; + Ok((StatusCode::CREATED, Json(response))) } diff --git a/crates/policy/src/model.rs b/crates/policy/src/model.rs index 02eef65a2..c8d46599d 100644 --- a/crates/policy/src/model.rs +++ b/crates/policy/src/model.rs @@ -18,6 +18,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub struct Violation { pub msg: String, + pub redirect_uri: Option, pub field: Option, } diff --git a/crates/storage-pg/src/oauth2/client.rs b/crates/storage-pg/src/oauth2/client.rs index e34316a9f..dbfa24d25 100644 --- a/crates/storage-pg/src/oauth2/client.rs +++ b/crates/storage-pg/src/oauth2/client.rs @@ -12,10 +12,7 @@ use std::{ use async_trait::async_trait; use mas_data_model::{Client, JwksOrJwksUri, User}; -use mas_iana::{ - jose::JsonWebSignatureAlg, - oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod}, -}; +use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod}; use mas_jose::jwk::PublicJsonWebKeySet; use mas_storage::{oauth2::OAuth2ClientRepository, Clock}; use oauth2_types::{ @@ -46,7 +43,6 @@ impl<'c> PgOAuth2ClientRepository<'c> { } } -// XXX: response_types #[allow(clippy::struct_excessive_bools)] #[derive(Debug)] struct OAuth2ClientLookup { @@ -54,7 +50,6 @@ struct OAuth2ClientLookup { encrypted_client_secret: Option, application_type: Option, redirect_uris: Vec, - // response_types: Vec, grant_type_authorization_code: bool, grant_type_refresh_token: bool, grant_type_client_credentials: bool, @@ -100,20 +95,6 @@ impl TryInto for OAuth2ClientLookup { .source(e) })?; - let response_types = vec![ - OAuthAuthorizationEndpointResponseType::Code, - OAuthAuthorizationEndpointResponseType::IdToken, - OAuthAuthorizationEndpointResponseType::None, - ]; - /* XXX - let response_types: Result, _> = - self.response_types.iter().map(|s| s.parse()).collect(); - let response_types = response_types.map_err(|source| ClientFetchError::ParseField { - field: "response_types", - source, - })?; - */ - let mut grant_types = Vec::new(); if self.grant_type_authorization_code { grant_types.push(GrantType::AuthorizationCode); @@ -253,7 +234,6 @@ impl TryInto for OAuth2ClientLookup { encrypted_client_secret: self.encrypted_client_secret, application_type, redirect_uris, - response_types, grant_types, client_name: self.client_name, logo_uri, @@ -493,11 +473,6 @@ impl<'c> OAuth2ClientRepository for PgOAuth2ClientRepository<'c> { encrypted_client_secret, application_type, redirect_uris, - response_types: vec![ - OAuthAuthorizationEndpointResponseType::Code, - OAuthAuthorizationEndpointResponseType::IdToken, - OAuthAuthorizationEndpointResponseType::None, - ], grant_types, client_name, logo_uri, @@ -598,11 +573,6 @@ impl<'c> OAuth2ClientRepository for PgOAuth2ClientRepository<'c> { encrypted_client_secret, application_type: None, redirect_uris, - response_types: vec![ - OAuthAuthorizationEndpointResponseType::Code, - OAuthAuthorizationEndpointResponseType::IdToken, - OAuthAuthorizationEndpointResponseType::None, - ], grant_types: vec![ GrantType::AuthorizationCode, GrantType::RefreshToken,