Deduplicate client registrations by hashing the metadata
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -3298,6 +3298,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"governor",
|
||||
"headers",
|
||||
"hex",
|
||||
"hyper",
|
||||
"indexmap 2.8.0",
|
||||
"insta",
|
||||
@@ -3336,6 +3337,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"serde_with",
|
||||
"sha2",
|
||||
"sqlx",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
|
||||
@@ -148,6 +148,10 @@ version = "0.8.0"
|
||||
[workspace.dependencies.headers]
|
||||
version = "0.4.0"
|
||||
|
||||
# Hex encoding and decoding
|
||||
[workspace.dependencies.hex]
|
||||
version = "0.4.3"
|
||||
|
||||
# HTTP request/response
|
||||
[workspace.dependencies.http]
|
||||
version = "1.3.1"
|
||||
@@ -278,6 +282,11 @@ version = "0.5.1"
|
||||
version = "0.8.22"
|
||||
features = ["url", "chrono", "preserve_order"]
|
||||
|
||||
# SHA-2 cryptographic hash algorithm
|
||||
[workspace.dependencies.sha2]
|
||||
version = "0.10.8"
|
||||
features = ["oid"]
|
||||
|
||||
# Query builder
|
||||
[workspace.dependencies.sea-query]
|
||||
version = "0.32.3"
|
||||
|
||||
@@ -35,6 +35,9 @@ pub struct Client {
|
||||
/// Client identifier
|
||||
pub client_id: String,
|
||||
|
||||
/// Hash of the client metadata
|
||||
pub metadata_digest: Option<String>,
|
||||
|
||||
pub encrypted_client_secret: Option<String>,
|
||||
|
||||
pub application_type: Option<ApplicationType>,
|
||||
@@ -177,6 +180,7 @@ impl Client {
|
||||
Self {
|
||||
id: Ulid::from_datetime_with_source(now.into(), rng),
|
||||
client_id: "client1".to_owned(),
|
||||
metadata_digest: None,
|
||||
encrypted_client_secret: None,
|
||||
application_type: Some(ApplicationType::Web),
|
||||
redirect_uris: vec![
|
||||
@@ -202,6 +206,7 @@ impl Client {
|
||||
Self {
|
||||
id: Ulid::from_datetime_with_source(now.into(), rng),
|
||||
client_id: "client2".to_owned(),
|
||||
metadata_digest: None,
|
||||
encrypted_client_secret: None,
|
||||
application_type: Some(ApplicationType::Native),
|
||||
redirect_uris: vec![Url::parse("https://client2.example.com/redirect").unwrap()],
|
||||
|
||||
@@ -72,10 +72,12 @@ base64ct.workspace = true
|
||||
camino.workspace = true
|
||||
chrono.workspace = true
|
||||
elliptic-curve.workspace = true
|
||||
hex.workspace = true
|
||||
governor.workspace = true
|
||||
indexmap = "2.8.0"
|
||||
pkcs8.workspace = true
|
||||
psl = "2.1.96"
|
||||
sha2.workspace = true
|
||||
time = "0.3.41"
|
||||
url.workspace = true
|
||||
mime = "0.3.17"
|
||||
|
||||
@@ -37,6 +37,7 @@ async fn create_test_client(state: &TestState) -> Client {
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -22,6 +22,7 @@ use oauth2_types::{
|
||||
use psl::Psl;
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use serde::Serialize;
|
||||
use sha2::Digest as _;
|
||||
use thiserror::Error;
|
||||
use tracing::info;
|
||||
use url::Url;
|
||||
@@ -50,6 +51,7 @@ impl_from_error_for_route!(mas_storage::RepositoryError);
|
||||
impl_from_error_for_route!(mas_policy::LoadError);
|
||||
impl_from_error_for_route!(mas_policy::EvaluationError);
|
||||
impl_from_error_for_route!(mas_keystore::aead::Error);
|
||||
impl_from_error_for_route!(serde_json::Error);
|
||||
|
||||
impl IntoResponse for RouteError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
@@ -204,7 +206,10 @@ pub(crate) async fn post(
|
||||
// Propagate any JSON extraction error
|
||||
let Json(body) = body?;
|
||||
|
||||
info!(?body, "Client registration");
|
||||
// We need to serialize the body to compute the hash, and to log it
|
||||
let body_json = serde_json::to_string(&body)?;
|
||||
|
||||
info!(body = body_json, "Client registration");
|
||||
|
||||
let user_agent = user_agent.map(|ua| ua.to_string());
|
||||
|
||||
@@ -276,34 +281,59 @@ pub(crate) async fn post(
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
let client = repo
|
||||
.oauth2_client()
|
||||
.add(
|
||||
&mut rng,
|
||||
&clock,
|
||||
metadata.redirect_uris().to_vec(),
|
||||
encrypted_client_secret,
|
||||
metadata.application_type.clone(),
|
||||
//&metadata.response_types(),
|
||||
metadata.grant_types().to_vec(),
|
||||
metadata
|
||||
.client_name
|
||||
.clone()
|
||||
.map(Localized::to_non_localized),
|
||||
metadata.logo_uri.clone().map(Localized::to_non_localized),
|
||||
metadata.client_uri.clone().map(Localized::to_non_localized),
|
||||
metadata.policy_uri.clone().map(Localized::to_non_localized),
|
||||
metadata.tos_uri.clone().map(Localized::to_non_localized),
|
||||
metadata.jwks_uri.clone(),
|
||||
metadata.jwks.clone(),
|
||||
// XXX: those might not be right, should be function calls
|
||||
metadata.id_token_signed_response_alg.clone(),
|
||||
metadata.userinfo_signed_response_alg.clone(),
|
||||
metadata.token_endpoint_auth_method.clone(),
|
||||
metadata.token_endpoint_auth_signing_alg.clone(),
|
||||
metadata.initiate_login_uri.clone(),
|
||||
)
|
||||
.await?;
|
||||
// If the client doesn't have a secret, we may be able to deduplicate it. To
|
||||
// do so, we hash the client metadata, and look for it in the database
|
||||
let (digest_hash, existing_client) = if client_secret.is_none() {
|
||||
// XXX: One interesting caveat is that we hash *before* saving to the database.
|
||||
// It means it takes into account fields that we don't care about *yet*.
|
||||
//
|
||||
// This means that if later we start supporting a particular field, we
|
||||
// will still serve the 'old' client_id, without updating the client in the
|
||||
// database
|
||||
let hash = sha2::Sha256::digest(body_json);
|
||||
let hash = hex::encode(hash);
|
||||
let client = repo.oauth2_client().find_by_metadata_digest(&hash).await?;
|
||||
(Some(hash), client)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let client = if let Some(client) = existing_client {
|
||||
tracing::info!(%client.id, "Reusing existing client");
|
||||
client
|
||||
} else {
|
||||
let client = repo
|
||||
.oauth2_client()
|
||||
.add(
|
||||
&mut rng,
|
||||
&clock,
|
||||
metadata.redirect_uris().to_vec(),
|
||||
digest_hash,
|
||||
encrypted_client_secret,
|
||||
metadata.application_type.clone(),
|
||||
//&metadata.response_types(),
|
||||
metadata.grant_types().to_vec(),
|
||||
metadata
|
||||
.client_name
|
||||
.clone()
|
||||
.map(Localized::to_non_localized),
|
||||
metadata.logo_uri.clone().map(Localized::to_non_localized),
|
||||
metadata.client_uri.clone().map(Localized::to_non_localized),
|
||||
metadata.policy_uri.clone().map(Localized::to_non_localized),
|
||||
metadata.tos_uri.clone().map(Localized::to_non_localized),
|
||||
metadata.jwks_uri.clone(),
|
||||
metadata.jwks.clone(),
|
||||
// XXX: those might not be right, should be function calls
|
||||
metadata.id_token_signed_response_alg.clone(),
|
||||
metadata.userinfo_signed_response_alg.clone(),
|
||||
metadata.token_endpoint_auth_method.clone(),
|
||||
metadata.token_endpoint_auth_signing_alg.clone(),
|
||||
metadata.initiate_login_uri.clone(),
|
||||
)
|
||||
.await?;
|
||||
tracing::info!(%client.id, "Registered new client");
|
||||
client
|
||||
};
|
||||
|
||||
let response = ClientRegistrationResponse {
|
||||
client_id: client.client_id.clone(),
|
||||
@@ -490,4 +520,51 @@ mod tests {
|
||||
let response: ClientRegistrationResponse = response.json();
|
||||
assert!(response.client_secret.is_some());
|
||||
}
|
||||
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
||||
async fn test_registration_dedupe(pool: PgPool) {
|
||||
setup();
|
||||
let state = TestState::from_pool(pool).await.unwrap();
|
||||
|
||||
// Post a client registration twice, we should get the same client ID
|
||||
let request =
|
||||
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["https://example.com/"],
|
||||
"response_types": ["code"],
|
||||
"grant_types": ["authorization_code"],
|
||||
"token_endpoint_auth_method": "none",
|
||||
}));
|
||||
|
||||
let response = state.request(request.clone()).await;
|
||||
response.assert_status(StatusCode::CREATED);
|
||||
let response: ClientRegistrationResponse = response.json();
|
||||
let client_id = response.client_id;
|
||||
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::CREATED);
|
||||
let response: ClientRegistrationResponse = response.json();
|
||||
assert_eq!(response.client_id, client_id);
|
||||
|
||||
// Doing that with a client that has a client_secret should not deduplicate
|
||||
let request =
|
||||
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["https://example.com/"],
|
||||
"response_types": ["code"],
|
||||
"grant_types": ["authorization_code"],
|
||||
"token_endpoint_auth_method": "client_secret_basic",
|
||||
}));
|
||||
|
||||
let response = state.request(request.clone()).await;
|
||||
response.assert_status(StatusCode::CREATED);
|
||||
let response: ClientRegistrationResponse = response.json();
|
||||
// Sanity check that the client_id is different
|
||||
assert_ne!(response.client_id, client_id);
|
||||
let client_id = response.client_id;
|
||||
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::CREATED);
|
||||
let response: ClientRegistrationResponse = response.json();
|
||||
assert_ne!(response.client_id, client_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ sec1 = "0.7.3"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_with = "3.12.0"
|
||||
sha2 = { version = "0.10.8", features = ["oid"] }
|
||||
sha2.workspace = true
|
||||
signature = "2.2.0"
|
||||
thiserror.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
@@ -19,7 +19,7 @@ language-tags = { version = "0.3.2", features = ["serde"] }
|
||||
url.workspace = true
|
||||
serde_with = { version = "3.12.0", features = ["chrono"] }
|
||||
chrono.workspace = true
|
||||
sha2 = "0.10.8"
|
||||
sha2.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
mas-iana.workspace = true
|
||||
|
||||
@@ -125,48 +125,51 @@ pub struct ClientMetadataSerdeHelper {
|
||||
|
||||
impl From<VerifiedClientMetadata> for ClientMetadataSerdeHelper {
|
||||
fn from(metadata: VerifiedClientMetadata) -> Self {
|
||||
let VerifiedClientMetadata {
|
||||
inner:
|
||||
ClientMetadata {
|
||||
redirect_uris,
|
||||
response_types,
|
||||
grant_types,
|
||||
application_type,
|
||||
contacts,
|
||||
client_name,
|
||||
logo_uri,
|
||||
client_uri,
|
||||
policy_uri,
|
||||
tos_uri,
|
||||
jwks_uri,
|
||||
jwks,
|
||||
software_id,
|
||||
software_version,
|
||||
sector_identifier_uri,
|
||||
subject_type,
|
||||
token_endpoint_auth_method,
|
||||
token_endpoint_auth_signing_alg,
|
||||
id_token_signed_response_alg,
|
||||
id_token_encrypted_response_alg,
|
||||
id_token_encrypted_response_enc,
|
||||
userinfo_signed_response_alg,
|
||||
userinfo_encrypted_response_alg,
|
||||
userinfo_encrypted_response_enc,
|
||||
request_object_signing_alg,
|
||||
request_object_encryption_alg,
|
||||
request_object_encryption_enc,
|
||||
default_max_age,
|
||||
require_auth_time,
|
||||
default_acr_values,
|
||||
initiate_login_uri,
|
||||
request_uris,
|
||||
require_signed_request_object,
|
||||
require_pushed_authorization_requests,
|
||||
introspection_signed_response_alg,
|
||||
introspection_encrypted_response_alg,
|
||||
introspection_encrypted_response_enc,
|
||||
post_logout_redirect_uris,
|
||||
},
|
||||
metadata.inner.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientMetadata> for ClientMetadataSerdeHelper {
|
||||
fn from(metadata: ClientMetadata) -> Self {
|
||||
let ClientMetadata {
|
||||
redirect_uris,
|
||||
response_types,
|
||||
grant_types,
|
||||
application_type,
|
||||
contacts,
|
||||
client_name,
|
||||
logo_uri,
|
||||
client_uri,
|
||||
policy_uri,
|
||||
tos_uri,
|
||||
jwks_uri,
|
||||
jwks,
|
||||
software_id,
|
||||
software_version,
|
||||
sector_identifier_uri,
|
||||
subject_type,
|
||||
token_endpoint_auth_method,
|
||||
token_endpoint_auth_signing_alg,
|
||||
id_token_signed_response_alg,
|
||||
id_token_encrypted_response_alg,
|
||||
id_token_encrypted_response_enc,
|
||||
userinfo_signed_response_alg,
|
||||
userinfo_encrypted_response_alg,
|
||||
userinfo_encrypted_response_enc,
|
||||
request_object_signing_alg,
|
||||
request_object_encryption_alg,
|
||||
request_object_encryption_enc,
|
||||
default_max_age,
|
||||
require_auth_time,
|
||||
default_acr_values,
|
||||
initiate_login_uri,
|
||||
request_uris,
|
||||
require_signed_request_object,
|
||||
require_pushed_authorization_requests,
|
||||
introspection_signed_response_alg,
|
||||
introspection_encrypted_response_alg,
|
||||
introspection_encrypted_response_enc,
|
||||
post_logout_redirect_uris,
|
||||
} = metadata;
|
||||
|
||||
ClientMetadataSerdeHelper {
|
||||
|
||||
@@ -118,8 +118,8 @@ impl<T> From<(T, HashMap<LanguageTag, T>)> for Localized<T> {
|
||||
/// All the fields with a default value are accessible via methods.
|
||||
///
|
||||
/// [IANA registry]: https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#client-metadata
|
||||
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
|
||||
#[serde(from = "ClientMetadataSerdeHelper")]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
|
||||
#[serde(from = "ClientMetadataSerdeHelper", into = "ClientMetadataSerdeHelper")]
|
||||
pub struct ClientMetadata {
|
||||
/// Array of redirection URIs for use in redirect-based flows such as the
|
||||
/// [authorization code flow].
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT oauth2_client_id\n , encrypted_client_secret\n , application_type\n , redirect_uris\n , grant_type_authorization_code\n , grant_type_refresh_token\n , grant_type_client_credentials\n , grant_type_device_code\n , client_name\n , logo_uri\n , client_uri\n , policy_uri\n , tos_uri\n , jwks_uri\n , jwks\n , id_token_signed_response_alg\n , userinfo_signed_response_alg\n , token_endpoint_auth_method\n , token_endpoint_auth_signing_alg\n , initiate_login_uri\n FROM oauth2_clients c\n\n WHERE oauth2_client_id = $1\n ",
|
||||
"query": "\n SELECT oauth2_client_id\n , metadata_digest\n , encrypted_client_secret\n , application_type\n , redirect_uris\n , grant_type_authorization_code\n , grant_type_refresh_token\n , grant_type_client_credentials\n , grant_type_device_code\n , client_name\n , logo_uri\n , client_uri\n , policy_uri\n , tos_uri\n , jwks_uri\n , jwks\n , id_token_signed_response_alg\n , userinfo_signed_response_alg\n , token_endpoint_auth_method\n , token_endpoint_auth_signing_alg\n , initiate_login_uri\n FROM oauth2_clients c\n\n WHERE oauth2_client_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -10,96 +10,101 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "encrypted_client_secret",
|
||||
"name": "metadata_digest",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "application_type",
|
||||
"name": "encrypted_client_secret",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "application_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "redirect_uris",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 5,
|
||||
"name": "grant_type_authorization_code",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "grant_type_refresh_token",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "grant_type_client_credentials",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "grant_type_device_code",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "client_name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "logo_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 11,
|
||||
"name": "client_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 12,
|
||||
"name": "policy_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 13,
|
||||
"name": "tos_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 14,
|
||||
"name": "jwks_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 15,
|
||||
"name": "jwks",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 16,
|
||||
"name": "id_token_signed_response_alg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 17,
|
||||
"name": "userinfo_signed_response_alg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 18,
|
||||
"name": "token_endpoint_auth_method",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 19,
|
||||
"name": "token_endpoint_auth_signing_alg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"ordinal": 20,
|
||||
"name": "initiate_login_uri",
|
||||
"type_info": "Text"
|
||||
}
|
||||
@@ -113,6 +118,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -132,5 +138,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "afef7e8248b415dd1fbf86748cbf5b37fbfeaf6fd0fbdaddfdf4db7feb7546b3"
|
||||
"hash": "38d0608b7d8ba30927f939491c1d43cfd962c729298ad07ee1ade2f2880c0eb3"
|
||||
}
|
||||
34
crates/storage-pg/.sqlx/query-4d0386ad2fe47f1aded46917abe6141752ba90d36467693a68318573171d57b0.json
generated
Normal file
34
crates/storage-pg/.sqlx/query-4d0386ad2fe47f1aded46917abe6141752ba90d36467693a68318573171d57b0.json
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth2_clients\n ( oauth2_client_id\n , metadata_digest\n , encrypted_client_secret\n , application_type\n , redirect_uris\n , grant_type_authorization_code\n , grant_type_refresh_token\n , grant_type_client_credentials\n , grant_type_device_code\n , client_name\n , logo_uri\n , client_uri\n , policy_uri\n , tos_uri\n , jwks_uri\n , jwks\n , id_token_signed_response_alg\n , userinfo_signed_response_alg\n , token_endpoint_auth_method\n , token_endpoint_auth_signing_alg\n , initiate_login_uri\n , is_static\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,\n $14, $15, $16, $17, $18, $19, $20, $21, FALSE)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4d0386ad2fe47f1aded46917abe6141752ba90d36467693a68318573171d57b0"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth2_clients\n ( oauth2_client_id\n , encrypted_client_secret\n , application_type\n , redirect_uris\n , grant_type_authorization_code\n , grant_type_refresh_token\n , grant_type_client_credentials\n , grant_type_device_code\n , client_name\n , logo_uri\n , client_uri\n , policy_uri\n , tos_uri\n , jwks_uri\n , jwks\n , id_token_signed_response_alg\n , userinfo_signed_response_alg\n , token_endpoint_auth_method\n , token_endpoint_auth_signing_alg\n , initiate_login_uri\n , is_static\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, FALSE)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "92fb511938dff21e5e0f7800c742b852b8c4468d1770c4cbc0b51611ce50e922"
|
||||
}
|
||||
142
crates/storage-pg/.sqlx/query-bb0f782756c274c06c1b63af6fc3ac2a7cedfd4247b57f062d348b4b1b36bef1.json
generated
Normal file
142
crates/storage-pg/.sqlx/query-bb0f782756c274c06c1b63af6fc3ac2a7cedfd4247b57f062d348b4b1b36bef1.json
generated
Normal file
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT oauth2_client_id\n , metadata_digest\n , encrypted_client_secret\n , application_type\n , redirect_uris\n , grant_type_authorization_code\n , grant_type_refresh_token\n , grant_type_client_credentials\n , grant_type_device_code\n , client_name\n , logo_uri\n , client_uri\n , policy_uri\n , tos_uri\n , jwks_uri\n , jwks\n , id_token_signed_response_alg\n , userinfo_signed_response_alg\n , token_endpoint_auth_method\n , token_endpoint_auth_signing_alg\n , initiate_login_uri\n FROM oauth2_clients c\n\n WHERE oauth2_client_id = ANY($1::uuid[])\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "oauth2_client_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "metadata_digest",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "encrypted_client_secret",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "application_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "redirect_uris",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "grant_type_authorization_code",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "grant_type_refresh_token",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "grant_type_client_credentials",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "grant_type_device_code",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "client_name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "logo_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "client_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "policy_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "tos_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "jwks_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "jwks",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "id_token_signed_response_alg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "userinfo_signed_response_alg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "token_endpoint_auth_method",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "token_endpoint_auth_signing_alg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "initiate_login_uri",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bb0f782756c274c06c1b63af6fc3ac2a7cedfd4247b57f062d348b4b1b36bef1"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT oauth2_client_id\n , encrypted_client_secret\n , application_type\n , redirect_uris\n , grant_type_authorization_code\n , grant_type_refresh_token\n , grant_type_client_credentials\n , grant_type_device_code\n , client_name\n , logo_uri\n , client_uri\n , policy_uri\n , tos_uri\n , jwks_uri\n , jwks\n , id_token_signed_response_alg\n , userinfo_signed_response_alg\n , token_endpoint_auth_method\n , token_endpoint_auth_signing_alg\n , initiate_login_uri\n FROM oauth2_clients c\n\n WHERE oauth2_client_id = ANY($1::uuid[])\n ",
|
||||
"query": "\n SELECT oauth2_client_id\n , metadata_digest\n , encrypted_client_secret\n , application_type\n , redirect_uris\n , grant_type_authorization_code\n , grant_type_refresh_token\n , grant_type_client_credentials\n , grant_type_device_code\n , client_name\n , logo_uri\n , client_uri\n , policy_uri\n , tos_uri\n , jwks_uri\n , jwks\n , id_token_signed_response_alg\n , userinfo_signed_response_alg\n , token_endpoint_auth_method\n , token_endpoint_auth_signing_alg\n , initiate_login_uri\n FROM oauth2_clients\n WHERE metadata_digest = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -10,109 +10,115 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "encrypted_client_secret",
|
||||
"name": "metadata_digest",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "application_type",
|
||||
"name": "encrypted_client_secret",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "application_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "redirect_uris",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 5,
|
||||
"name": "grant_type_authorization_code",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "grant_type_refresh_token",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "grant_type_client_credentials",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "grant_type_device_code",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "client_name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "logo_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 11,
|
||||
"name": "client_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 12,
|
||||
"name": "policy_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 13,
|
||||
"name": "tos_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 14,
|
||||
"name": "jwks_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 15,
|
||||
"name": "jwks",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 16,
|
||||
"name": "id_token_signed_response_alg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 17,
|
||||
"name": "userinfo_signed_response_alg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 18,
|
||||
"name": "token_endpoint_auth_method",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 19,
|
||||
"name": "token_endpoint_auth_signing_alg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"ordinal": 20,
|
||||
"name": "initiate_login_uri",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray"
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -132,5 +138,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1aa4c541af7e12431a58f43a1882a14314cc1833a6be272056e09d07c21ba9ef"
|
||||
"hash": "cf654533cfed946e9ac52dbcea1f50be3dfdac0fbfb1e8a0204c0c9c103ba5b0"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT oauth2_client_id\n , encrypted_client_secret\n , application_type\n , redirect_uris\n , grant_type_authorization_code\n , grant_type_refresh_token\n , grant_type_client_credentials\n , grant_type_device_code\n , client_name\n , logo_uri\n , client_uri\n , policy_uri\n , tos_uri\n , jwks_uri\n , jwks\n , id_token_signed_response_alg\n , userinfo_signed_response_alg\n , token_endpoint_auth_method\n , token_endpoint_auth_signing_alg\n , initiate_login_uri\n FROM oauth2_clients c\n WHERE is_static = TRUE\n ",
|
||||
"query": "\n SELECT oauth2_client_id\n , metadata_digest\n , encrypted_client_secret\n , application_type\n , redirect_uris\n , grant_type_authorization_code\n , grant_type_refresh_token\n , grant_type_client_credentials\n , grant_type_device_code\n , client_name\n , logo_uri\n , client_uri\n , policy_uri\n , tos_uri\n , jwks_uri\n , jwks\n , id_token_signed_response_alg\n , userinfo_signed_response_alg\n , token_endpoint_auth_method\n , token_endpoint_auth_signing_alg\n , initiate_login_uri\n FROM oauth2_clients c\n WHERE is_static = TRUE\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -10,96 +10,101 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "encrypted_client_secret",
|
||||
"name": "metadata_digest",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "application_type",
|
||||
"name": "encrypted_client_secret",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "application_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "redirect_uris",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 5,
|
||||
"name": "grant_type_authorization_code",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "grant_type_refresh_token",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "grant_type_client_credentials",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "grant_type_device_code",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "client_name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "logo_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 11,
|
||||
"name": "client_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 12,
|
||||
"name": "policy_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 13,
|
||||
"name": "tos_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 14,
|
||||
"name": "jwks_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 15,
|
||||
"name": "jwks",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 16,
|
||||
"name": "id_token_signed_response_alg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 17,
|
||||
"name": "userinfo_signed_response_alg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 18,
|
||||
"name": "token_endpoint_auth_method",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 19,
|
||||
"name": "token_endpoint_auth_signing_alg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"ordinal": 20,
|
||||
"name": "initiate_login_uri",
|
||||
"type_info": "Text"
|
||||
}
|
||||
@@ -111,6 +116,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -130,5 +136,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "199819516dce285771a75a48a687b285225aeae7a4d1ca91084ae84f25dcbbec"
|
||||
"hash": "fc9925e19000d79c0bb020ea44e13cbb364b3505626d34550e38f6f7397b9d42"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- Adds a column which stores a hash of the client metadata, so that we can
|
||||
-- deduplicate client registrations
|
||||
--
|
||||
-- This hash is a SHA-256 hash of the JSON-encoded client metadata. Note that we
|
||||
-- don't retroactively hash existing clients, so this will only be populated for
|
||||
-- new clients.
|
||||
ALTER TABLE oauth2_clients
|
||||
ADD COLUMN metadata_digest TEXT UNIQUE;
|
||||
@@ -570,6 +570,7 @@ mod tests {
|
||||
vec!["https://example.com/redirect".parse().unwrap()],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
vec![GrantType::AuthorizationCode],
|
||||
Some("First client".to_owned()),
|
||||
Some("https://example.com/logo.png".parse().unwrap()),
|
||||
|
||||
@@ -47,6 +47,7 @@ impl<'c> PgOAuth2ClientRepository<'c> {
|
||||
#[derive(Debug)]
|
||||
struct OAuth2ClientLookup {
|
||||
oauth2_client_id: Uuid,
|
||||
metadata_digest: Option<String>,
|
||||
encrypted_client_secret: Option<String>,
|
||||
application_type: Option<String>,
|
||||
redirect_uris: Vec<String>,
|
||||
@@ -231,6 +232,7 @@ impl TryInto<Client> for OAuth2ClientLookup {
|
||||
Ok(Client {
|
||||
id,
|
||||
client_id: id.to_string(),
|
||||
metadata_digest: self.metadata_digest,
|
||||
encrypted_client_secret: self.encrypted_client_secret,
|
||||
application_type,
|
||||
redirect_uris,
|
||||
@@ -268,6 +270,7 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> {
|
||||
OAuth2ClientLookup,
|
||||
r#"
|
||||
SELECT oauth2_client_id
|
||||
, metadata_digest
|
||||
, encrypted_client_secret
|
||||
, application_type
|
||||
, redirect_uris
|
||||
@@ -302,6 +305,56 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> {
|
||||
Ok(Some(res.try_into()?))
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.oauth2_client.find_by_metadata_digest",
|
||||
skip_all,
|
||||
fields(
|
||||
db.query.text,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn find_by_metadata_digest(
|
||||
&mut self,
|
||||
digest: &str,
|
||||
) -> Result<Option<Client>, Self::Error> {
|
||||
let res = sqlx::query_as!(
|
||||
OAuth2ClientLookup,
|
||||
r#"
|
||||
SELECT oauth2_client_id
|
||||
, metadata_digest
|
||||
, encrypted_client_secret
|
||||
, application_type
|
||||
, redirect_uris
|
||||
, grant_type_authorization_code
|
||||
, grant_type_refresh_token
|
||||
, grant_type_client_credentials
|
||||
, grant_type_device_code
|
||||
, client_name
|
||||
, logo_uri
|
||||
, client_uri
|
||||
, policy_uri
|
||||
, tos_uri
|
||||
, jwks_uri
|
||||
, jwks
|
||||
, id_token_signed_response_alg
|
||||
, userinfo_signed_response_alg
|
||||
, token_endpoint_auth_method
|
||||
, token_endpoint_auth_signing_alg
|
||||
, initiate_login_uri
|
||||
FROM oauth2_clients
|
||||
WHERE metadata_digest = $1
|
||||
"#,
|
||||
digest,
|
||||
)
|
||||
.traced()
|
||||
.fetch_optional(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
let Some(res) = res else { return Ok(None) };
|
||||
|
||||
Ok(Some(res.try_into()?))
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.oauth2_client.load_batch",
|
||||
skip_all,
|
||||
@@ -319,6 +372,7 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> {
|
||||
OAuth2ClientLookup,
|
||||
r#"
|
||||
SELECT oauth2_client_id
|
||||
, metadata_digest
|
||||
, encrypted_client_secret
|
||||
, application_type
|
||||
, redirect_uris
|
||||
@@ -373,6 +427,7 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> {
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
redirect_uris: Vec<Url>,
|
||||
metadata_digest: Option<String>,
|
||||
encrypted_client_secret: Option<String>,
|
||||
application_type: Option<ApplicationType>,
|
||||
grant_types: Vec<GrantType>,
|
||||
@@ -405,6 +460,7 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> {
|
||||
r#"
|
||||
INSERT INTO oauth2_clients
|
||||
( oauth2_client_id
|
||||
, metadata_digest
|
||||
, encrypted_client_secret
|
||||
, application_type
|
||||
, redirect_uris
|
||||
@@ -427,9 +483,11 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> {
|
||||
, is_static
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, FALSE)
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
|
||||
$14, $15, $16, $17, $18, $19, $20, $21, FALSE)
|
||||
"#,
|
||||
Uuid::from(id),
|
||||
metadata_digest,
|
||||
encrypted_client_secret,
|
||||
application_type.as_ref().map(ToString::to_string),
|
||||
&redirect_uris_array,
|
||||
@@ -470,6 +528,7 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> {
|
||||
Ok(Client {
|
||||
id,
|
||||
client_id: id.to_string(),
|
||||
metadata_digest: None,
|
||||
encrypted_client_secret,
|
||||
application_type,
|
||||
redirect_uris,
|
||||
@@ -570,6 +629,7 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> {
|
||||
Ok(Client {
|
||||
id: client_id,
|
||||
client_id: client_id.to_string(),
|
||||
metadata_digest: None,
|
||||
encrypted_client_secret,
|
||||
application_type: None,
|
||||
redirect_uris,
|
||||
@@ -605,6 +665,7 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> {
|
||||
OAuth2ClientLookup,
|
||||
r#"
|
||||
SELECT oauth2_client_id
|
||||
, metadata_digest
|
||||
, encrypted_client_secret
|
||||
, application_type
|
||||
, redirect_uris
|
||||
|
||||
@@ -68,6 +68,7 @@ mod tests {
|
||||
vec!["https://example.com/redirect".parse().unwrap()],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
vec![GrantType::AuthorizationCode],
|
||||
Some("Test client".to_owned()),
|
||||
Some("https://example.com/logo.png".parse().unwrap()),
|
||||
@@ -435,6 +436,7 @@ mod tests {
|
||||
vec!["https://first.example.com/redirect".parse().unwrap()],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
vec![GrantType::AuthorizationCode],
|
||||
Some("First client".to_owned()),
|
||||
Some("https://first.example.com/logo.png".parse().unwrap()),
|
||||
@@ -459,6 +461,7 @@ mod tests {
|
||||
vec!["https://second.example.com/redirect".parse().unwrap()],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
vec![GrantType::AuthorizationCode],
|
||||
Some("Second client".to_owned()),
|
||||
Some("https://second.example.com/logo.png".parse().unwrap()),
|
||||
@@ -758,6 +761,7 @@ mod tests {
|
||||
vec!["https://example.com/redirect".parse().unwrap()],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
vec![GrantType::AuthorizationCode],
|
||||
Some("Example".to_owned()),
|
||||
Some("https://example.com/logo.png".parse().unwrap()),
|
||||
|
||||
@@ -45,6 +45,23 @@ pub trait OAuth2ClientRepository: Send + Sync {
|
||||
self.lookup(id).await
|
||||
}
|
||||
|
||||
/// Find an OAuth2 client by its metadata digest
|
||||
///
|
||||
/// Returns `None` if the client does not exist
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `digest`: The metadata digest (SHA-256 hash encoded in hex) of the
|
||||
/// client to find
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails
|
||||
async fn find_by_metadata_digest(
|
||||
&mut self,
|
||||
digest: &str,
|
||||
) -> Result<Option<Client>, Self::Error>;
|
||||
|
||||
/// Load a batch of OAuth2 clients by their IDs
|
||||
///
|
||||
/// Returns a map of client IDs to clients. If a client does not exist, it
|
||||
@@ -71,6 +88,7 @@ pub trait OAuth2ClientRepository: Send + Sync {
|
||||
/// * `rng`: The random number generator to use
|
||||
/// * `clock`: The clock used to generate timestamps
|
||||
/// * `redirect_uris`: The list of redirect URIs used by this client
|
||||
/// * `metadata_digest`: The hash of the client metadata, if computed
|
||||
/// * `encrypted_client_secret`: The encrypted client secret, if any
|
||||
/// * `application_type`: The application type of this client
|
||||
/// * `grant_types`: The list of grant types this client can use
|
||||
@@ -101,6 +119,7 @@ pub trait OAuth2ClientRepository: Send + Sync {
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
redirect_uris: Vec<Url>,
|
||||
metadata_digest: Option<String>,
|
||||
encrypted_client_secret: Option<String>,
|
||||
application_type: Option<ApplicationType>,
|
||||
grant_types: Vec<GrantType>,
|
||||
@@ -221,6 +240,11 @@ pub trait OAuth2ClientRepository: Send + Sync {
|
||||
repository_impl!(OAuth2ClientRepository:
|
||||
async fn lookup(&mut self, id: Ulid) -> Result<Option<Client>, Self::Error>;
|
||||
|
||||
async fn find_by_metadata_digest(
|
||||
&mut self,
|
||||
digest: &str,
|
||||
) -> Result<Option<Client>, Self::Error>;
|
||||
|
||||
async fn load_batch(
|
||||
&mut self,
|
||||
ids: BTreeSet<Ulid>,
|
||||
@@ -231,6 +255,7 @@ repository_impl!(OAuth2ClientRepository:
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
redirect_uris: Vec<Url>,
|
||||
metadata_digest: Option<String>,
|
||||
encrypted_client_secret: Option<String>,
|
||||
application_type: Option<ApplicationType>,
|
||||
grant_types: Vec<GrantType>,
|
||||
|
||||
Reference in New Issue
Block a user