Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the client registration to comply with MSC2966 #3202

Merged
merged 3 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 55 additions & 16 deletions crates/data-model/src/oauth2/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,17 +42,10 @@ pub struct Client {
/// Array of Redirection URI values used by the Client
pub redirect_uris: Vec<Url>,

/// 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<OAuthAuthorizationEndpointResponseType>,

/// 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<GrantType>,

/// Array of e-mail addresses of people responsible for this Client
pub contacts: Vec<String>,

/// Name of the Client to be presented to the End-User
pub client_name: Option<String>, // TODO: translations

Expand Down Expand Up @@ -126,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<Utc>, rng: &mut impl RngCore) -> Vec<Client> {
vec![
Expand All @@ -139,9 +182,7 @@ 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],
contacts: vec!["foo@client1.example.com".to_owned()],
client_name: Some("Client 1".to_owned()),
client_uri: Some(Url::parse("https://client1.example.com").unwrap()),
logo_uri: Some(Url::parse("https://client1.example.com/logo.png").unwrap()),
Expand All @@ -163,9 +204,7 @@ 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],
contacts: vec!["foo@client2.example.com".to_owned()],
client_name: None,
client_uri: None,
logo_uri: None,
Expand Down
5 changes: 0 additions & 5 deletions crates/handlers/src/graphql/model/oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,6 @@ impl OAuth2Client {
&self.0.redirect_uris
}

/// List of contacts advertised by the client.
pub async fn contacts(&self) -> &[String] {
&self.0.contacts
}

/// The application type advertised by the client.
pub async fn application_type(&self) -> Option<OAuth2ApplicationType> {
match self.0.application_type.as_ref()? {
Expand Down
3 changes: 0 additions & 3 deletions crates/handlers/src/graphql/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ async fn create_test_client(state: &TestState) -> Client {
None,
None,
vec![],
vec![],
None,
None,
None,
Expand Down Expand Up @@ -358,7 +357,6 @@ async fn test_oauth2_client_credentials(pool: PgPool) {
let request =
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"client_uri": "https://example.com/",
"contacts": ["contact@example.com"],
"token_endpoint_auth_method": "client_secret_post",
"grant_types": ["client_credentials"],
}));
Expand Down Expand Up @@ -582,7 +580,6 @@ async fn test_add_user(pool: PgPool) {
let request =
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"client_uri": "https://example.com/",
"contacts": ["contact@example.com"],
"token_endpoint_auth_method": "client_secret_post",
"grant_types": ["client_credentials"],
}));
Expand Down
1 change: 0 additions & 1 deletion crates/handlers/src/oauth2/device/authorize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ mod tests {
let request =
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"client_uri": "https://example.com/",
"contacts": ["contact@example.com"],
"token_endpoint_auth_method": "none",
"grant_types": ["urn:ietf:params:oauth:grant-type:device_code"],
"response_types": [],
Expand Down
3 changes: 0 additions & 3 deletions crates/handlers/src/oauth2/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,6 @@ mod tests {

// Provision a client which will be used to do introspection requests
let request = Request::post(OAuth2RegistrationEndpoint::PATH).json(json!({
"contacts": ["hello@introspecting.com"],
"client_uri": "https://introspecting.com/",
"grant_types": [],
"token_endpoint_auth_method": "client_secret_basic",
Expand All @@ -491,7 +490,6 @@ mod tests {

// Provision a client which will be used to generate tokens
let request = Request::post(OAuth2RegistrationEndpoint::PATH).json(json!({
"contacts": ["hello@client.com"],
"client_uri": "https://client.com/",
"redirect_uris": ["https://client.com/"],
"response_types": ["code"],
Expand Down Expand Up @@ -683,7 +681,6 @@ mod tests {

// Provision a client which will be used to do introspection requests
let request = Request::post(OAuth2RegistrationEndpoint::PATH).json(json!({
"contacts": ["hello@introspecting.com"],
"client_uri": "https://introspecting.com/",
"grant_types": [],
"token_endpoint_auth_method": "client_secret_basic",
Expand Down
29 changes: 19 additions & 10 deletions crates/handlers/src/oauth2/registration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -263,7 +273,6 @@ pub(crate) async fn post(
metadata.application_type.clone(),
//&metadata.response_types(),
metadata.grant_types().to_vec(),
metadata.contacts.clone().unwrap_or_default(),
metadata
.client_name
.clone()
Expand All @@ -283,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)))
}

Expand Down Expand Up @@ -362,7 +377,6 @@ mod tests {
let request =
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"application_type": "web",
"contacts": ["hello@example.com"],
"client_uri": "https://example.com/",
"redirect_uris": ["http://this-is-insecure.com/"],
}));
Expand All @@ -375,7 +389,6 @@ mod tests {
// Incoherent response types
let request =
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"contacts": ["hello@example.com"],
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/"],
"response_types": ["id_token"],
Expand All @@ -390,7 +403,6 @@ mod tests {
// Using a public suffix
let request =
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"contacts": ["hello@example.com"],
"client_uri": "https://github.io/",
"redirect_uris": ["https://github.io/"],
"response_types": ["code"],
Expand All @@ -410,7 +422,6 @@ mod tests {
// Using a public suffix in a translated URL
let request =
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"contacts": ["hello@example.com"],
"client_uri": "https://example.com/",
"client_uri#fr-FR": "https://github.io/",
"redirect_uris": ["https://example.com/"],
Expand Down Expand Up @@ -438,7 +449,6 @@ mod tests {
// secret
let request =
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"contacts": ["hello@example.com"],
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/"],
"response_types": ["code"],
Expand All @@ -455,7 +465,6 @@ mod tests {
// return a client secret
let request =
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"contacts": ["hello@example.com"],
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/"],
"response_types": ["code"],
Expand Down
1 change: 0 additions & 1 deletion crates/handlers/src/oauth2/revoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,6 @@ mod tests {
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
"token_endpoint_auth_method": "client_secret_post",
"response_types": ["code"],
"grant_types": ["authorization_code", "refresh_token"],
Expand Down
5 changes: 0 additions & 5 deletions crates/handlers/src/oauth2/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,6 @@ mod tests {
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
"token_endpoint_auth_method": "none",
"response_types": ["code"],
"grant_types": ["authorization_code"],
Expand Down Expand Up @@ -1011,7 +1010,6 @@ mod tests {
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
"token_endpoint_auth_method": "none",
"response_types": ["code"],
"grant_types": ["authorization_code", "refresh_token"],
Expand Down Expand Up @@ -1133,7 +1131,6 @@ mod tests {
let request =
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"client_uri": "https://example.com/",
"contacts": ["contact@example.com"],
"token_endpoint_auth_method": "client_secret_post",
"grant_types": ["client_credentials"],
}));
Expand Down Expand Up @@ -1260,7 +1257,6 @@ mod tests {
let request =
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"client_uri": "https://example.com/",
"contacts": ["contact@example.com"],
"token_endpoint_auth_method": "none",
"grant_types": ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"],
"response_types": [],
Expand Down Expand Up @@ -1447,7 +1443,6 @@ mod tests {
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
"token_endpoint_auth_method": "client_secret_post",
"grant_types": ["password"],
"response_types": [],
Expand Down
1 change: 0 additions & 1 deletion crates/handlers/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,6 @@ impl TestState {
let request =
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
"client_uri": "https://example.com/",
"contacts": ["contact@example.com"],
"token_endpoint_auth_method": "client_secret_post",
"grant_types": ["client_credentials"],
}));
Expand Down
1 change: 1 addition & 0 deletions crates/policy/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub field: Option<String>,
}

Expand Down
Loading
Loading