From 6d139723acb8d9c22e2ef27ecd0aaf3de920a05f Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Tue, 25 Feb 2025 17:44:52 +0100 Subject: [PATCH 01/37] Add BCrypt as encoder for client secret --- .../iam/config/AuthorizationServerConfig.java | 22 ++++++++++++++----- .../config/security/IamApiSecurityConfig.java | 6 +++-- .../IamTokenEndointSecurityConfig.java | 5 +++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java index 42a412c445..46d2889dfd 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java @@ -17,6 +17,8 @@ import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.mitre.oauth2.service.ClientDetailsEntityService; import org.mitre.oauth2.service.DeviceCodeService; @@ -33,6 +35,8 @@ import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; @@ -66,6 +70,8 @@ @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { + private static final short DEFAULT_ROUND = 12; + @Autowired @Qualifier("iamUserDetailsService") private UserDetailsService iamUserDetailsService; @@ -115,10 +121,20 @@ AuthenticationEventPublisher iamAuthenticationEventPublisher() { return new IamAuthenticationEventPublisher(); } + @Bean(name = "passwordEncoder") + public PasswordEncoder passwordEncoder() { + String encodingId = "bcrypt"; + Map encoders = new HashMap<>(); + encoders.put(encodingId, new BCryptPasswordEncoder(DEFAULT_ROUND)); + // encoders.put("noop", NoOpPasswordEncoder.getInstance()); + return new DelegatingPasswordEncoder(encodingId, encoders); + } + @Bean(name = "authenticationManager") AuthenticationManager authenticationManager() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(passwordEncoder()); provider.setUserDetailsService(iamUserDetailsService); provider.setPasswordEncoder(passwordEncoder); @@ -127,7 +143,6 @@ AuthenticationManager authenticationManager() { pm.setAuthenticationEventPublisher(iamAuthenticationEventPublisher()); return pm; - } @Bean @@ -187,16 +202,11 @@ public void configure(final AuthorizationServerEndpointsConfigurer endpoints) th public void configure(final ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetailsService); - } @Override public void configure(final AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients(); - - } - - } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java index b5289d7be1..c5ab07e67d 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java @@ -31,7 +31,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter; import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler; @@ -47,6 +47,8 @@ @Configuration public class IamApiSecurityConfig { + private static final short DEFAULT_ROUND = 12; + @Configuration @Order(20) public static class IamProxyCertificateApiConfig extends WebSecurityConfigurerAdapter { @@ -66,7 +68,7 @@ public static class IamProxyCertificateApiConfig extends WebSecurityConfigurerAd @Override protected void configure(final AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) - .passwordEncoder(NoOpPasswordEncoder.getInstance()); + .passwordEncoder(new BCryptPasswordEncoder(DEFAULT_ROUND)); } @Override diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamTokenEndointSecurityConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamTokenEndointSecurityConfig.java index 37c8cb621d..1f271f99dd 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamTokenEndointSecurityConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamTokenEndointSecurityConfig.java @@ -32,7 +32,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter; import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler; import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; @@ -50,6 +50,7 @@ public class IamTokenEndointSecurityConfig extends WebSecurityConfigurerAdapter { public static final String TOKEN_ENDPOINT = "/token"; + private static final short DEFAULT_ROUND = 12; @Autowired private OAuth2AuthenticationEntryPoint authenticationEntryPoint; @@ -71,7 +72,7 @@ public class IamTokenEndointSecurityConfig extends WebSecurityConfigurerAdapter protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) - .passwordEncoder(NoOpPasswordEncoder.getInstance()); + .passwordEncoder(new BCryptPasswordEncoder(DEFAULT_ROUND)); } @Bean From a6207b7e51ab3ef9912109d9c9be0d735097e73a Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Tue, 25 Feb 2025 17:46:48 +0100 Subject: [PATCH 02/37] Edit test data with hashed client secret --- .../db/migration/test/V100000___test_data.sql | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql b/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql index c9adfd210b..6049b0bdd2 100644 --- a/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql +++ b/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql @@ -10,33 +10,32 @@ INSERT INTO system_scope(scope, description, icon, restricted, default_scope, st INSERT INTO client_details (id, client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection, token_endpoint_auth_method, require_auth_time, device_code_validity_seconds, created_at, active) VALUES - (1, 'client', 'secret', 'Test Client', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), - (2, 'tasks-app', 'secret', 'Tasks App', false, null, 0, 0, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), - (3, 'post-client', 'secret', 'Post client', false, null, 3600,600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (4, 'client-cred', 'secret', 'Client credentials', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), - (5, 'password-grant', 'secret', 'Password grant client', false, null, 3600, 600, true, 'SECRET_BASIC',true, null, CURRENT_TIMESTAMP(), true), - (6, 'scim-client-ro', 'secret', 'SCIM client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP(), true), - (7, 'scim-client-rw', 'secret', 'SCIM client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP(), true), - (8, 'token-exchange-actor', 'secret', 'Token Exchange grant client actor', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (9, 'token-exchange-subject', 'secret', 'Token Exchange grant client subject', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (10, 'registration-client', 'secret', 'Registration service test client', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (11, 'token-lookup-client', 'secret', 'Token lookup client', false, null, 3600, 600, true, 'SECRET_BASIC', false, null, CURRENT_TIMESTAMP(), true), - (12, 'device-code-client', 'secret', 'Device code client', false, null, 3600, 600, true, 'SECRET_BASIC', false, 600, CURRENT_TIMESTAMP(), true), + (1, 'client', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Test Client', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), + (2, 'tasks-app', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Tasks App', false, null, 0, 0, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), + (3, 'post-client', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Post client', false, null, 3600,600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (4, 'client-cred', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Client credentials', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), + (5, 'password-grant', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Password grant client', false, null, 3600, 600, true, 'SECRET_BASIC',true, null, CURRENT_TIMESTAMP(), true), + (6, 'scim-client-ro', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'SCIM client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP(), true), + (7, 'scim-client-rw', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'SCIM client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP(), true), + (8, 'token-exchange-actor', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Token Exchange grant client actor', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (9, 'token-exchange-subject', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Token Exchange grant client subject', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (10, 'registration-client', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Registration service test client', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (11, 'token-lookup-client', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Token lookup client', false, null, 3600, 600, true, 'SECRET_BASIC', false, null, CURRENT_TIMESTAMP(), true), + (12, 'device-code-client', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Device code client', false, null, 3600, 600, true, 'SECRET_BASIC', false, 600, CURRENT_TIMESTAMP(), true), (13, 'implicit-flow-client', null, 'Implicit Flow client', false, null, 3600, 600, false, null, false, 600, CURRENT_TIMESTAMP(), true), (14, 'public-dc-client', null, 'Public Device Code client', false, null, 3600, 600, false, null, false, 600, CURRENT_TIMESTAMP(), true), - (17, 'admin-client-ro', 'secret', 'Admin client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (18, 'admin-client-rw', 'secret', 'Admin client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (19, 'public-client', null, 'Public client', false, 3600, 3600, 600, true, 'NONE', false, null, CURRENT_TIMESTAMP(), true), - (20, 'refresh-client', 'secret', 'Refresh Flow client', false, 36000, 3600, 600, true, 'SECRET_BASIC', true, 30, CURRENT_TIMESTAMP(), true); + (17, 'admin-client-ro', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Admin client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (18, 'admin-client-rw', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Admin client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (19, 'public-client', null, 'Public client', false, 3600, 3600, 600, true, 'NONE', false, null, CURRENT_TIMESTAMP(), true); INSERT INTO client_details (id, client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection, - token_endpoint_auth_method, require_auth_time, token_endpoint_auth_signing_alg, jwks) VALUES - (15, 'jwt-auth-client_secret_jwt', 'c8e9eed0-e6e4-4a66-b16e-6f37096356a7', 'JWT Bearer Auth Client (client_secret_jwt)', - false, null, 3600, 600, true, 'SECRET_JWT', false, 'HS256', null), - (16, 'jwt-auth-private_key_jwt', 'secret', 'JWT Bearer Auth Client (private_key_jwt)', + token_endpoint_auth_method, require_auth_time, token_endpoint_auth_signing_alg, jwks, active) VALUES + (15, 'jwt-auth-client_secret_jwt', '$2a$10$aeZw.BWWE2.qgNbZvcY28uXSlcuC9ykeb6xsfLlPtItPV2PAKoI62', 'JWT Bearer Auth Client (client_secret_jwt)', + false, null, 3600, 600, true, 'SECRET_JWT', false, 'HS256', null, true), + (16, 'jwt-auth-private_key_jwt', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'JWT Bearer Auth Client (private_key_jwt)', false, null, 3600, 600, true,'PRIVATE_KEY', false, 'RS256', - '{"keys":[{"kty":"RSA","e":"AQAB","kid":"rsa1","n":"1y1CP181zqPNPlV1JDM7Xv0QnGswhSTHe8_XPZHxDTJkykpk_1BmgA3ovP62QRE2ORgsv5oSBI_Z_RaOc4Zx2FonjEJF2oBHtBjsAiF-pxGkM5ZPjFNgFTGp1yUUBjFDcEeIGCwPEyYSt93sQIP_0DRbViMUnpyn3xgM_a1dO5brEWR2n1Uqff1yA5NXfLS03qpl2dpH4HFY5-Zs4bvtJykpAOhoHuIQbz-hmxb9MZ3uTAwsx2HiyEJtz-suyTBHO3BM2o8UcCeyfa34ShPB8i86-sf78fOk2KeRIW1Bju3ANmdV3sxL0j29cesxKCZ06u2ZiGR3Srbft8EdLPzf-w"}]}'); + '{"keys":[{"kty":"RSA","e":"AQAB","kid":"rsa1","n":"1y1CP181zqPNPlV1JDM7Xv0QnGswhSTHe8_XPZHxDTJkykpk_1BmgA3ovP62QRE2ORgsv5oSBI_Z_RaOc4Zx2FonjEJF2oBHtBjsAiF-pxGkM5ZPjFNgFTGp1yUUBjFDcEeIGCwPEyYSt93sQIP_0DRbViMUnpyn3xgM_a1dO5brEWR2n1Uqff1yA5NXfLS03qpl2dpH4HFY5-Zs4bvtJykpAOhoHuIQbz-hmxb9MZ3uTAwsx2HiyEJtz-suyTBHO3BM2o8UcCeyfa34ShPB8i86-sf78fOk2KeRIW1Bju3ANmdV3sxL0j29cesxKCZ06u2ZiGR3Srbft8EdLPzf-w"}]}', true); INSERT INTO client_scope (owner_id, scope) VALUES (1, 'openid'), @@ -1561,7 +1560,7 @@ insert into iam_account_client(id, account_id, client_id, creation_time) VALUES -- TOTP multi-factor secrets insert into iam_totp_mfa(active, secret, creation_time, last_update_time, account_id) VALUES -(true, 'secret', CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP(), 1000); +(true, '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP(), 1000); -- Client last used dates insert into client_last_used(client_details_id, last_used) VALUES From e0875af6851ebe914e01e3e246b6c1606908e0b3 Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Wed, 26 Feb 2025 17:45:03 +0100 Subject: [PATCH 03/37] Add migrations for update existed clients --- .../mysql/V106__HashClientSecret.java | 53 +++++++++++++++++++ .../test/V100000_9__hash_client_test.sql | 2 + .../db/migration/test/V100000___test_data.sql | 34 ++++++------ 3 files changed, 72 insertions(+), 17 deletions(-) create mode 100644 iam-persistence/src/main/java/db/migration/mysql/V106__HashClientSecret.java create mode 100644 iam-persistence/src/main/resources/db/migration/test/V100000_9__hash_client_test.sql diff --git a/iam-persistence/src/main/java/db/migration/mysql/V106__HashClientSecret.java b/iam-persistence/src/main/java/db/migration/mysql/V106__HashClientSecret.java new file mode 100644 index 0000000000..fe1fc7acba --- /dev/null +++ b/iam-persistence/src/main/java/db/migration/mysql/V106__HashClientSecret.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2018 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package db.migration.mysql; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + + +import it.infn.mw.iam.persistence.migrations.BaseFlywayJavaMigrationAdapter; + +public class V106__HashClientSecret extends BaseFlywayJavaMigrationAdapter { + + @Override + public void migrate(JdbcTemplate jdbcTemplate) throws DataAccessException { + + final short DEFAULT_ROUND = 12; + + PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(DEFAULT_ROUND); + + SqlRowSet clientList = jdbcTemplate.queryForRowSet("SELECT id, client_secret FROM client_details"); + + while (clientList.next()) { + String clientSecret = clientList.getString("client_secret"); + if (clientSecret == null) { + continue; + } + Long id = clientList.getLong("id"); + + String PassEncrypted = passwordEncoder.encode(clientSecret); + + if (passwordEncoder.matches(clientSecret, PassEncrypted)) { + jdbcTemplate.update("UPDATE client_details SET client_secret=? WHERE id=?", PassEncrypted, id); + } + } + } + +} \ No newline at end of file diff --git a/iam-persistence/src/main/resources/db/migration/test/V100000_9__hash_client_test.sql b/iam-persistence/src/main/resources/db/migration/test/V100000_9__hash_client_test.sql new file mode 100644 index 0000000000..0f43144d6e --- /dev/null +++ b/iam-persistence/src/main/resources/db/migration/test/V100000_9__hash_client_test.sql @@ -0,0 +1,2 @@ +UPDATE client_details SET client_secret = '$2a$12$P6AOS2.9DS6L.VaI1qjWnuVFbqUlpU449WDpISytV23H2ANUq0Gtu' WHERE client_secret = 'secret'; +UPDATE client_details SET client_secret = '$2a$12$I4lKKnqnZ0FPqiIf/5WyzONevtVwA/RjftzmhMDhRZPTT4NiRcMsa' WHERE client_secret = 'c8e9eed0-e6e4-4a66-b16e-6f37096356a7'; \ No newline at end of file diff --git a/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql b/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql index 6049b0bdd2..e7821e0261 100644 --- a/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql +++ b/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql @@ -10,30 +10,30 @@ INSERT INTO system_scope(scope, description, icon, restricted, default_scope, st INSERT INTO client_details (id, client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection, token_endpoint_auth_method, require_auth_time, device_code_validity_seconds, created_at, active) VALUES - (1, 'client', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Test Client', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), - (2, 'tasks-app', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Tasks App', false, null, 0, 0, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), - (3, 'post-client', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Post client', false, null, 3600,600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (4, 'client-cred', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Client credentials', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), - (5, 'password-grant', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Password grant client', false, null, 3600, 600, true, 'SECRET_BASIC',true, null, CURRENT_TIMESTAMP(), true), - (6, 'scim-client-ro', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'SCIM client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP(), true), - (7, 'scim-client-rw', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'SCIM client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP(), true), - (8, 'token-exchange-actor', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Token Exchange grant client actor', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (9, 'token-exchange-subject', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Token Exchange grant client subject', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (10, 'registration-client', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Registration service test client', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (11, 'token-lookup-client', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Token lookup client', false, null, 3600, 600, true, 'SECRET_BASIC', false, null, CURRENT_TIMESTAMP(), true), - (12, 'device-code-client', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Device code client', false, null, 3600, 600, true, 'SECRET_BASIC', false, 600, CURRENT_TIMESTAMP(), true), + (1, 'client', 'secret', 'Test Client', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), + (2, 'tasks-app', 'secret', 'Tasks App', false, null, 0, 0, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), + (3, 'post-client', 'secret', 'Post client', false, null, 3600,600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (4, 'client-cred', 'secret', 'Client credentials', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), + (5, 'password-grant', 'secret', 'Password grant client', false, null, 3600, 600, true, 'SECRET_BASIC',true, null, CURRENT_TIMESTAMP(), true), + (6, 'scim-client-ro', 'secret', 'SCIM client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP(), true), + (7, 'scim-client-rw', 'secret', 'SCIM client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP(), true), + (8, 'token-exchange-actor', 'secret', 'Token Exchange grant client actor', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (9, 'token-exchange-subject', 'secret', 'Token Exchange grant client subject', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (10, 'registration-client', 'secret', 'Registration service test client', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (11, 'token-lookup-client', 'secret', 'Token lookup client', false, null, 3600, 600, true, 'SECRET_BASIC', false, null, CURRENT_TIMESTAMP(), true), + (12, 'device-code-client', 'secret', 'Device code client', false, null, 3600, 600, true, 'SECRET_BASIC', false, 600, CURRENT_TIMESTAMP(), true), (13, 'implicit-flow-client', null, 'Implicit Flow client', false, null, 3600, 600, false, null, false, 600, CURRENT_TIMESTAMP(), true), (14, 'public-dc-client', null, 'Public Device Code client', false, null, 3600, 600, false, null, false, 600, CURRENT_TIMESTAMP(), true), - (17, 'admin-client-ro', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Admin client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (18, 'admin-client-rw', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'Admin client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (17, 'admin-client-ro', 'secret', 'Admin client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (18, 'admin-client-rw', 'secret', 'Admin client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), (19, 'public-client', null, 'Public client', false, 3600, 3600, 600, true, 'NONE', false, null, CURRENT_TIMESTAMP(), true); INSERT INTO client_details (id, client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection, token_endpoint_auth_method, require_auth_time, token_endpoint_auth_signing_alg, jwks, active) VALUES - (15, 'jwt-auth-client_secret_jwt', '$2a$10$aeZw.BWWE2.qgNbZvcY28uXSlcuC9ykeb6xsfLlPtItPV2PAKoI62', 'JWT Bearer Auth Client (client_secret_jwt)', + (15, 'jwt-auth-client_secret_jwt', 'c8e9eed0-e6e4-4a66-b16e-6f37096356a7', 'JWT Bearer Auth Client (client_secret_jwt)', false, null, 3600, 600, true, 'SECRET_JWT', false, 'HS256', null, true), - (16, 'jwt-auth-private_key_jwt', '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', 'JWT Bearer Auth Client (private_key_jwt)', + (16, 'jwt-auth-private_key_jwt', 'secret', 'JWT Bearer Auth Client (private_key_jwt)', false, null, 3600, 600, true,'PRIVATE_KEY', false, 'RS256', '{"keys":[{"kty":"RSA","e":"AQAB","kid":"rsa1","n":"1y1CP181zqPNPlV1JDM7Xv0QnGswhSTHe8_XPZHxDTJkykpk_1BmgA3ovP62QRE2ORgsv5oSBI_Z_RaOc4Zx2FonjEJF2oBHtBjsAiF-pxGkM5ZPjFNgFTGp1yUUBjFDcEeIGCwPEyYSt93sQIP_0DRbViMUnpyn3xgM_a1dO5brEWR2n1Uqff1yA5NXfLS03qpl2dpH4HFY5-Zs4bvtJykpAOhoHuIQbz-hmxb9MZ3uTAwsx2HiyEJtz-suyTBHO3BM2o8UcCeyfa34ShPB8i86-sf78fOk2KeRIW1Bju3ANmdV3sxL0j29cesxKCZ06u2ZiGR3Srbft8EdLPzf-w"}]}', true); @@ -1560,7 +1560,7 @@ insert into iam_account_client(id, account_id, client_id, creation_time) VALUES -- TOTP multi-factor secrets insert into iam_totp_mfa(active, secret, creation_time, last_update_time, account_id) VALUES -(true, '$2a$10$hs3fGDkX0.tKTqWLrTGZs.duMx/.Iidm4x0ZZcNN88ovO3Gm10HB2', CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP(), 1000); +(true, 'secret', CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP(), 1000); -- Client last used dates insert into client_last_used(client_details_id, last_used) VALUES From 07b234aa7f9237c94b5a2d3f9583ac37de6292b2 Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Wed, 26 Mar 2025 15:34:10 +0100 Subject: [PATCH 04/37] Add clientSecret dialog component - Hide the client secret value from UI - Add "Regenerate client secret" button to allow owner client to request a new secret --- .../clientauth/clientauth.component.html | 1 - .../clientsecret/clientsecret.component.html | 75 ++++++------- .../clientsecret/clientsecret.component.js | 106 +++++++++++------- .../clientsecret/clientsecret.dialog.html | 55 +++++++++ 4 files changed, 151 insertions(+), 86 deletions(-) create mode 100644 iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.dialog.html diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientauth/clientauth.component.html b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientauth/clientauth.component.html index 1f7e75db1a..e221764b5a 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientauth/clientauth.component.html +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientauth/clientauth.component.html @@ -56,7 +56,6 @@ -

diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.html b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.html index fcd1187cc7..c42348d5a5 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.html +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.html @@ -15,52 +15,20 @@ limitations under the License. --> -
-
- - -
- - - - - - - - -
-

- The secret will be generated when the client is saved. -

-
-
- -
-
- -
-
- -

- Not defined. -

+
+
+
+
+
-

- Registration access token provides management access to the - client. + Registration access token provides management access to the client.

Regenerate registration - access token \ No newline at end of file + ng-if="!$ctrl.newClient && !$ctrl.isLimited()">Regenerate registration access token + + + + diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.js index 42ec45c4e8..ea03c3edce 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.js +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.js @@ -16,16 +16,50 @@ (function () { 'use strict'; - function ClientSecretController(ModalService, toaster, ClientsService) { + function ModalClientSecretController($rootScope, $scope, $uibModal, $uibModalInstance, ClientsService, toaster, client) { var self = this; self.showSecret = false; - self.toggleSecretVisibility = toggleSecretVisibility; + // self.toggleSecretVisibility = toggleSecretVisibility; self.clipboardSuccess = clipboardSuccess; self.clipboardError = clipboardError; - self.rotateClientSecret = rotateClientSecret; - self.isLimited = isLimited; - self.rotateClientRat = rotateClientRat; + self.confirmation = false; + self.clientId = client.client_id; + self.clientName = client.client_name; + self.isNewClient = !!self.clientId; + self.parent = parent; + self.showSecret = false; + + self.closeModal = function () { + self.isModalOpen = false; + $uibModalInstance.dismiss('Dismissed'); + }; + + self.closeModal = function () { + self.isModalOpen = false; + $uibModalInstance.close(); + }; + + self.toggleSecretVisibility = function() { + self.showSecret = !self.showSecret; + }; + + self.confirmRequestNewSecret = function () { + self.confirmation = true; + var results = ClientsService.rotateClientSecret(client.client_id).then(res => { + self.newSecret = res.client_secret; + toaster.pop({ + type: 'success', + body: 'Registration access token rotated for client ' + self.clientName + }); + }).catch(error => { + console.error(error); + toaster.pop({ + type: 'error', + body: 'Could not rotate secret for client ' + self.clientName + }); + }); + } function clipboardError(event) { toaster.pop({ @@ -34,10 +68,6 @@ }); } - function isLimited() { - return self.limited === 'true' | self.limited; - } - function clipboardSuccess(event, source) { toaster.pop({ type: 'success', @@ -48,9 +78,30 @@ toggleSecretVisibility(); } } + } - function toggleSecretVisibility() { - self.showSecret = !self.showSecret; + function ClientSecretController($uibModal, toaster, ClientsService) { + var self = this; + + self.isLimited = isLimited; + self.rotateClientRat = rotateClientRat; + self.openModalRequestClientSecret = function () { + self.isModalOpen = true; + + var modalSecret = $uibModal.open({ + templateUrl: '/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.dialog.html', + controller: ModalClientSecretController, + controllerAs: '$ctrl', + resolve: { + client: () => { return self.client } + } + }); + + modalSecret.result.then(self.handleSuccess); + }; + + function isLimited() { + return self.limited === 'true' | self.limited; } function rotateClientRat() { @@ -70,37 +121,6 @@ } } - function rotateClientSecret() { - - var modalOptions = { - closeButtonText: 'Cancel', - actionButtonText: 'Confirm Change', - headerText: 'Regenerate Client Secret', - bodyText: - `Are you sure you want to change the secret of this client: ` + self.client.client_name+ ` ?` - }; - - ModalService.showModal({}, modalOptions) - .then( - function() { - ClientsService.rotateClientSecret(self.client.client_id).then(res => { - self.client = res; - toaster.pop({ - type: 'success', - body: 'Secret rotated for client ' + self.client.client_name - }); - }).catch(res => { - toaster.pop({ - type: 'error', - body: 'Could not rotate secret for client ' + self.client.client_name - }); - }); - } - ).catch(function(error) { - console.info("Cancel Regenerate Client Secret"); - }); - } - self.$onInit = function () { console.debug('ClientSecretController.self', self); }; @@ -120,7 +140,7 @@ newClient: "<", limited: '@' }, - controller: ['ModalService', 'toaster', 'ClientsService', ClientSecretController], + controller: ['$uibModal', 'toaster', 'ClientsService', ClientSecretController], controllerAs: '$ctrl' }; } diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.dialog.html b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.dialog.html new file mode 100644 index 0000000000..9b6b66a66b --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.dialog.html @@ -0,0 +1,55 @@ + + + + From 1bac215114b44d01d7730037fd863d9e0fcd8db7 Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Wed, 26 Mar 2025 17:35:40 +0100 Subject: [PATCH 05/37] Refactor bcrypt call into IamBcryptUtil Updated the generateNewClientSecret function to use this utils method. --- .../DefaultClientManagementService.java | 18 +++++++++++++++--- .../iam/config/AuthorizationServerConfig.java | 4 +--- .../config/security/IamApiSecurityConfig.java | 4 +--- .../IamTokenEndointSecurityConfig.java | 5 ++--- .../it/infn/mw/iam/util/IamBcryptUtil.java | 5 +++++ 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java index 1055aca9c3..eb719fa234 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java @@ -17,6 +17,7 @@ import static it.infn.mw.iam.api.client.util.ClientSuppliers.accountNotFound; import static it.infn.mw.iam.api.client.util.ClientSuppliers.clientNotFound; +import static it.infn.mw.iam.util.IamBcryptUtil.bcrypt; import static java.util.Objects.isNull; import static org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod.NONE; @@ -120,15 +121,20 @@ public Optional retrieveClientByClientId(String clientId) { @Override public RegisteredClientDTO saveNewClient(RegisteredClientDTO client) throws ParseException { + String secret = defaultsService.generateClientSecret(); + ClientDetailsEntity entity = converter.entityFromClientManagementRequest(client); entity.setDynamicallyRegistered(false); entity.setCreatedAt(Date.from(clock.instant())); + entity.setClientSecret(bcrypt().encode(secret)); entity.setActive(true); defaultsService.setupClientDefaults(entity); entity = clientService.saveNewClient(entity); - return converter.registeredClientDtoFromEntity(entity); + RegisteredClientDTO newClientResponse = converter.registeredClientDtoFromEntity(entity); + newClientResponse.setClientSecret(secret); + return newClientResponse; } @Override @@ -181,6 +187,8 @@ public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO cli newClient.setClientSecret(null); } else if (isNull(client.getClientSecret())) { client.setClientSecret(defaultsService.generateClientSecret()); + } else { + newClient.setClientSecret(defaultsService.generateClientSecret()); } newClient = clientService.updateClient(newClient); @@ -210,10 +218,14 @@ public RegisteredClientDTO generateNewClientSecret(String clientId) { ClientDetailsEntity client = clientService.findClientByClientId(clientId) .orElseThrow(ClientSuppliers.clientNotFound(clientId)); - client.setClientSecret(defaultsService.generateClientSecret()); + String pwd = defaultsService.generateClientSecret(); + client.setClientSecret(pwd); + client.setClientSecret(bcrypt().encode(pwd)); client = clientService.updateClient(client); eventPublisher.publishEvent(new ClientSecretUpdatedEvent(this, client)); - return converter.registeredClientDtoFromEntity(client); + RegisteredClientDTO clientWithSecret = converter.registeredClientDtoFromEntity(client); + clientWithSecret.setClientSecret(pwd); + return clientWithSecret; } @Override diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java index 46d2889dfd..6edbfdc5da 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java @@ -70,8 +70,6 @@ @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { - private static final short DEFAULT_ROUND = 12; - @Autowired @Qualifier("iamUserDetailsService") private UserDetailsService iamUserDetailsService; @@ -125,7 +123,7 @@ AuthenticationEventPublisher iamAuthenticationEventPublisher() { public PasswordEncoder passwordEncoder() { String encodingId = "bcrypt"; Map encoders = new HashMap<>(); - encoders.put(encodingId, new BCryptPasswordEncoder(DEFAULT_ROUND)); + encoders.put(encodingId, new BCryptPasswordEncoder()); // encoders.put("noop", NoOpPasswordEncoder.getInstance()); return new DelegatingPasswordEncoder(encodingId, encoders); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java index c5ab07e67d..da6e072fe7 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java @@ -47,8 +47,6 @@ @Configuration public class IamApiSecurityConfig { - private static final short DEFAULT_ROUND = 12; - @Configuration @Order(20) public static class IamProxyCertificateApiConfig extends WebSecurityConfigurerAdapter { @@ -68,7 +66,7 @@ public static class IamProxyCertificateApiConfig extends WebSecurityConfigurerAd @Override protected void configure(final AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) - .passwordEncoder(new BCryptPasswordEncoder(DEFAULT_ROUND)); + .passwordEncoder(new BCryptPasswordEncoder()); } @Override diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamTokenEndointSecurityConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamTokenEndointSecurityConfig.java index 1f271f99dd..6877949c19 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamTokenEndointSecurityConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamTokenEndointSecurityConfig.java @@ -15,6 +15,7 @@ */ package it.infn.mw.iam.config.security; +import static it.infn.mw.iam.util.IamBcryptUtil.bcrypt; import static java.util.Collections.singletonList; import static org.springframework.http.HttpMethod.OPTIONS; @@ -32,7 +33,6 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter; import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler; import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; @@ -50,7 +50,6 @@ public class IamTokenEndointSecurityConfig extends WebSecurityConfigurerAdapter { public static final String TOKEN_ENDPOINT = "/token"; - private static final short DEFAULT_ROUND = 12; @Autowired private OAuth2AuthenticationEntryPoint authenticationEntryPoint; @@ -72,7 +71,7 @@ public class IamTokenEndointSecurityConfig extends WebSecurityConfigurerAdapter protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) - .passwordEncoder(new BCryptPasswordEncoder(DEFAULT_ROUND)); + .passwordEncoder(bcrypt()); } @Bean diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/util/IamBcryptUtil.java b/iam-login-service/src/main/java/it/infn/mw/iam/util/IamBcryptUtil.java index 6396c1c01b..8cde72f926 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/util/IamBcryptUtil.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/util/IamBcryptUtil.java @@ -35,4 +35,9 @@ public static void main(String[] args) { System.out.println(encoder.encode(args[0])); } + public static final BCryptPasswordEncoder bcrypt() { + final short DEFAULT_ROUND = 12; + return new BCryptPasswordEncoder(DEFAULT_ROUND); + } + } From b4602d4d499a70b7670be3737b66c49bf4f67d89 Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Thu, 24 Apr 2025 13:05:33 +0200 Subject: [PATCH 06/37] Hash client's secret on dynamic registration --- .../service/DefaultClientRegistrationService.java | 6 ++++++ .../mw/iam/api/client/service/DefaultClientService.java | 3 +++ 2 files changed, 9 insertions(+) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java index e44a812835..35fcff364b 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java @@ -56,6 +56,7 @@ import it.infn.mw.iam.api.client.service.ClientService; import it.infn.mw.iam.api.common.client.AuthorizationGrantType; import it.infn.mw.iam.api.common.client.RegisteredClientDTO; +import it.infn.mw.iam.api.common.client.TokenEndpointAuthenticationMethod; import it.infn.mw.iam.audit.events.account.client.AccountClientOwnerAssigned; import it.infn.mw.iam.audit.events.client.ClientRegistered; import it.infn.mw.iam.audit.events.client.ClientRegistrationAccessTokenRotatedEvent; @@ -362,7 +363,12 @@ public RegisteredClientDTO registerClient(RegisteredClientDTO request, checkAllowedGrantTypes(request, authentication); cleanupRequestedScopes(client, authentication); + if (isNull(client.getClientSecret())) { + client.setClientSecret(defaultsService.generateClientSecret()); + } + String tmpClientSecret = client.getClientSecret(); client = clientService.saveNewClient(client); + client.setClientSecret(tmpClientSecret); RegisteredClientDTO response = converter.registrationResponseFromClient(client); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java index b28e54efd4..1f65f9861d 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java @@ -15,6 +15,8 @@ */ package it.infn.mw.iam.api.client.service; +import static it.infn.mw.iam.util.IamBcryptUtil.bcrypt; + import java.time.Clock; import java.util.Date; import java.util.Optional; @@ -65,6 +67,7 @@ public DefaultClientService(Clock clock, IamClientRepository clientRepo, public ClientDetailsEntity saveNewClient(ClientDetailsEntity client) { client.setCreatedAt(Date.from(clock.instant())); eventPublisher.publishEvent(new ClientCreatedEvent(this, client)); + client.setClientSecret(bcrypt().encode(client.getClientSecret())); return clientRepo.save(client); } From c9904bd6cc627249d814688b6f6532d6cae21747 Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Thu, 24 Apr 2025 13:08:22 +0200 Subject: [PATCH 07/37] Add secret encoder config on OAuth API --- .../mw/iam/config/security/MitreSecurityConfig.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/MitreSecurityConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/MitreSecurityConfig.java index d84606927a..4061881e8f 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/MitreSecurityConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/MitreSecurityConfig.java @@ -28,7 +28,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter; import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter; import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; @@ -180,12 +180,15 @@ public static class IntrospectEndpointAuthorizationConfig extends WebSecurityCon @Autowired @Qualifier("clientUserDetailsService") private UserDetailsService userDetailsService; + + @Autowired + private PasswordEncoder passwordEncoder; @Override protected void configure(final AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) - .passwordEncoder(NoOpPasswordEncoder.getInstance()); + .passwordEncoder(passwordEncoder); } @Override @@ -219,6 +222,9 @@ public static class RevokeEndpointAuthorizationConfig extends WebSecurityConfigu @Autowired private OAuth2AuthenticationEntryPoint authenticationEntryPoint; + @Autowired + private PasswordEncoder passwordEncoder; + @Autowired @Qualifier("clientUserDetailsService") private UserDetailsService userDetailsService; @@ -227,7 +233,7 @@ public static class RevokeEndpointAuthorizationConfig extends WebSecurityConfigu protected void configure(final AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) - .passwordEncoder(NoOpPasswordEncoder.getInstance()); + .passwordEncoder(passwordEncoder); } private ClientCredentialsTokenEndpointFilter clientCredentialsEndpointFilter() From 76e3d7ecd418d696f21d36860a84487f63305bf4 Mon Sep 17 00:00:00 2001 From: Stefano Enrico Zotti Date: Thu, 24 Apr 2025 16:23:26 +0200 Subject: [PATCH 08/37] Add new angular component --- .../clientsecretview.component.js | 73 +++++++++++++++++++ .../newclientsecretshow.component.html | 65 +++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/newclientsecretshow/clientsecretview.component.js create mode 100644 iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/newclientsecretshow/newclientsecretshow.component.html diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/newclientsecretshow/clientsecretview.component.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/newclientsecretshow/clientsecretview.component.js new file mode 100644 index 0000000000..2a6a6d5f38 --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/newclientsecretshow/clientsecretview.component.js @@ -0,0 +1,73 @@ +/* + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function () { + 'use strict'; + + function ClientSecretViewController($uibModal, toaster, ClientsService, data) { + + var $ctrl = this; + + $ctrl.data = data; + $ctrl.newClient = !!data.client.clientSecret ; + $ctrl.client = data.client + + self.clipboardSuccess = clipboardSuccess; + self.clipboardError = clipboardError; + + $ctrl.ok = function() { + $uibModalInstance.close($ctrl.selected); + }; + + $ctrl.closeModal = function() { + $uibModalInstance.dismiss('cancel'); + }; + + function clipboardError(event) { + toaster.pop({ + type: 'error', + body: 'Could not copy secret to clipboard!' + }); + } + + function clipboardSuccess(event, source) { + toaster.pop({ + type: 'success', + body: 'Secret copied to clipboard!' + }); + event.clearSelection(); + if (source === 'secret') { + toggleSecretVisibility(); + } + } + }; + + angular.module('dashboardApp') + .component('ClientSecretView', ClientSecretView()); + + function ClientSecretView() { + return { + templateUrl: "/resources/iam/apps/dashboard-app/components/clients/client/newclientsecretshow/clientsecretview.component.html", + bindings: { + client: "=", + newClient: "<", + limited: '@' + }, + controller: ['$uibModal', 'toaster', 'ClientsService', 'data', ClientSecretViewController], + controllerAs: '$ctrl' + }; + } + +}()); \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/newclientsecretshow/newclientsecretshow.component.html b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/newclientsecretshow/newclientsecretshow.component.html new file mode 100644 index 0000000000..793467fc50 --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/newclientsecretshow/newclientsecretshow.component.html @@ -0,0 +1,65 @@ + + + + \ No newline at end of file From 7daac4cf63b44c6c34f21d0d003c25dfcdb52037 Mon Sep 17 00:00:00 2001 From: Stefano Enrico Zotti Date: Thu, 24 Apr 2025 16:30:45 +0200 Subject: [PATCH 09/37] Refactor show secret after save or update client --- .../clients/client/client.component.js | 68 ++++++++++++- .../clientsecret/clientsecret.component.html | 4 +- .../clientsecret/clientsecret.component.js | 3 +- .../myclients/myclient/myclient.component.js | 95 ++++++++++++++++++- 4 files changed, 162 insertions(+), 8 deletions(-) diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.js index 9af47294c5..65d2213399 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.js +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.js @@ -16,6 +16,49 @@ (function () { 'use strict'; + function ClientSecretViewController($uibModal, $uibModalInstance, toaster, ClientsService, data) { + var $ctrl = this; + $ctrl.data = data; + $ctrl.isNewClient = data.isNewClient; + $ctrl.newClient = data.client; + $ctrl.secret = $ctrl.newClient.client_secret; + $ctrl.clientId = $ctrl.newClient.client_id; + $ctrl.showSecret = false; + $ctrl.confirmation = true; + + self.clipboardSuccess = clipboardSuccess; + self.clipboardError = clipboardError; + + $ctrl.ok = function() { + $uibModalInstance.close($ctrl.selected); + }; + + $ctrl.closeModal = function() { + $uibModalInstance.dismiss('cancel'); + }; + + $ctrl.toggleSecretVisibility = function() { + $ctrl.showSecret = !$ctrl.showSecret; + }; + + function clipboardError(event) { + toaster.pop({ + type: 'error', + body: 'Could not copy secret to clipboard!' + }); + } + + function clipboardSuccess(event, source) { + toaster.pop({ + type: 'success', + body: 'Secret copied to clipboard!' + }); + event.clearSelection(); + if (source === 'secret') { + toggleSecretVisibility(); + } + } + }; function ClientController(ClientsService, FindService, toaster, $uibModal, $location) { var self = this; @@ -61,7 +104,6 @@ function saveClient() { - function handleSuccess(res) { self.client = res; self.clientVal = angular.copy(self.client); @@ -93,7 +135,29 @@ type: 'success', body: 'Client saved!' }); - $location.path('/clients'); + + var modalSecret = $uibModal.open({ + templateUrl: '/resources/iam/apps/dashboard-app/components/clients/client/newclientsecretshow/newclientsecretshow.component.html', + controller: ClientSecretViewController, + controllerAs: '$ctrl', + resolve: { + data: { + client: res, + title: "New client credential details", + message: "Save this client credential on safe before press Confirm button", + isNewClient: true, + } + } + }); + + modalSecret.result + .then(() => {$location.path('/clients');}) + .catch(() => { + toaster.pop({ + type: 'error', + body: errorMsg + }); + }); }).catch(handleError); } else { diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.html b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.html index c42348d5a5..24d301a2eb 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.html +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

-
+

Registration access token provides management access to the client. diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.js index ea03c3edce..9bc7b891e0 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.js +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/clientsecret/clientsecret.component.js @@ -23,7 +23,7 @@ // self.toggleSecretVisibility = toggleSecretVisibility; self.clipboardSuccess = clipboardSuccess; self.clipboardError = clipboardError; - self.confirmation = false; + self.confirmation = true; self.clientId = client.client_id; self.clientName = client.client_name; self.isNewClient = !!self.clientId; @@ -78,6 +78,7 @@ toggleSecretVisibility(); } } + self.confirmRequestNewSecret(); } function ClientSecretController($uibModal, toaster, ClientsService) { diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/user/myclients/myclient/myclient.component.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/user/myclients/myclient/myclient.component.js index 9d2df3592c..9bd15e7365 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/user/myclients/myclient/myclient.component.js +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/user/myclients/myclient/myclient.component.js @@ -16,6 +16,50 @@ (function () { 'use strict'; + function ClientSecretViewController($uibModal, $uibModalInstance, toaster, ClientsService, data) { + var $ctrl = this; + $ctrl.data = data; + $ctrl.isNewClient = data.isNewClient; + $ctrl.newClient = data.client; + $ctrl.secret = $ctrl.newClient.client_secret; + $ctrl.clientId = $ctrl.newClient.client_id; + $ctrl.showSecret = false; + $ctrl.confirmation = true; + + self.clipboardSuccess = clipboardSuccess; + self.clipboardError = clipboardError; + + $ctrl.ok = function() { + $uibModalInstance.close($ctrl.selected); + }; + + $ctrl.closeModal = function() { + $uibModalInstance.dismiss('cancel'); + }; + + $ctrl.toggleSecretVisibility = function() { + $ctrl.showSecret = !$ctrl.showSecret; + }; + + function clipboardError(event) { + toaster.pop({ + type: 'error', + body: 'Could not copy secret to clipboard!' + }); + } + + function clipboardSuccess(event, source) { + toaster.pop({ + type: 'success', + body: 'Secret copied to clipboard!' + }); + event.clearSelection(); + if (source === 'secret') { + toggleSecretVisibility(); + } + } + }; + function MyClientController($location, ClientRegistrationService, toaster, $uibModal) { var self = this; @@ -54,7 +98,29 @@ type: 'success', body: 'Client saved!' }); - return res; + + var res = () => { + var modalInstance = $uibModal.open({ + templateUrl: 'clientsecret-view.content.html', + controller: 'ModalClientSecretViewController', + resolve: { + data: function() { + return { + title: '', + message: '', + clientDetail: res, + }; + } + } + }); + + modalInstance.result.then(function(selectedItem) { + $ctrl.selected = selectedItem; + }, function() { + console.log('Dialog dismissed at: ' + new Date()); + }); + return res; + } } function handleError(res) { @@ -73,9 +139,32 @@ function registerClient() { return ClientRegistrationService.registerClient(self.clientVal).then(res => { - handleSuccess(res); - $location.path('/home/clients'); + handleSuccess(res); + + var modalSecret = $uibModal.open({ + templateUrl: '/resources/iam/apps/dashboard-app/components/clients/client/newclientsecretshow/newclientsecretshow.component.html', + controller: ClientSecretViewController, + controllerAs: '$ctrl', + resolve: { + data: { + client: res, + title: "New client credential details", + message: "Save this client credential on safe before press Confirm button", + isNewClient: true, + } + } + }); + + modalSecret.result + .then(() => {$location.path('/home/clients');}) + .catch(() => { + toaster.pop({ + type: 'error', + body: errorMsg + }); + }); return res; + }).catch(handleError); } From 32e506360940febf23db48a4d9cf3d9dd5df6d94 Mon Sep 17 00:00:00 2001 From: Stefano Enrico Zotti Date: Mon, 28 Apr 2025 16:24:27 +0200 Subject: [PATCH 10/37] Refactor test with hashed client secret --- .../test/api/client/RegistrationAccessTokenTests.java | 3 ++- .../scope/matchers/ScopeMatcherNoCacheTests.java | 11 ++++++----- .../service/client/ClientManagementServiceTests.java | 3 ++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/RegistrationAccessTokenTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/RegistrationAccessTokenTests.java index 331a821935..e2680a9c5b 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/RegistrationAccessTokenTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/RegistrationAccessTokenTests.java @@ -33,6 +33,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import com.nimbusds.jwt.JWT; import com.nimbusds.jwt.JWTParser; @@ -110,7 +111,7 @@ public void testRatWorkAsExpected() throws ParseException { .body() .as(RegisteredClientDTO.class); - assertThat(getResponse.getClientSecret(), is(registerResponse.getClientSecret())); + assertThat(new BCryptPasswordEncoder(12).matches(registerResponse.getClientSecret(), getResponse.getClientSecret()), is(true)); assertThat(getResponse.getRegistrationAccessToken(), nullValue()); RegisteredClientDTO rotatedRatClient = diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/scope/matchers/ScopeMatcherNoCacheTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/scope/matchers/ScopeMatcherNoCacheTests.java index 85ad1a3a4b..8a3a5506a3 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/scope/matchers/ScopeMatcherNoCacheTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/scope/matchers/ScopeMatcherNoCacheTests.java @@ -46,6 +46,7 @@ public class ScopeMatcherNoCacheTests extends EndpointsTestUtils { private static final String CLIENT_ID = "cache-client"; private static final String CLIENT_SECRET = "secret"; + private static final String HASH_CLIENT_SECRET = "$2a$12$WWEtffWdIellMxblYDNEx..nVahwP9ZMuRYyVdYFb.7DECPRaOp1K"; @Autowired private IamClientRepository clientRepo; @@ -53,11 +54,11 @@ public class ScopeMatcherNoCacheTests extends EndpointsTestUtils { @Autowired private CacheManager cacheManager; - private String getAccessTokenForClient(String scopes) throws Exception { + private String getAccessTokenForClient(String scopes, String secret) throws Exception { return new AccessTokenGetter().grantType("client_credentials") .clientId(CLIENT_ID) - .clientSecret(CLIENT_SECRET) + .clientSecret(secret) .scope(scopes) .getAccessTokenValue(); } @@ -74,17 +75,17 @@ public void updatingClientScopesWithNoCache() throws Exception { ClientDetailsEntity client = new ClientDetailsEntity(); client.setClientId(CLIENT_ID); - client.setClientSecret(CLIENT_SECRET); + client.setClientSecret(HASH_CLIENT_SECRET); client.setScope(Sets.newHashSet("openid", "profile", "email")); clientRepo.save(client); try { - JWT token = JWTParser.parse(getAccessTokenForClient("openid profile email")); + JWT token = JWTParser.parse(getAccessTokenForClient("openid profile email", CLIENT_SECRET)); assertThat("scim:read", not(in(token.getJWTClaimsSet().getClaim("scope").toString().split(" ")))); client.setScope(Sets.newHashSet("openid", "profile", "email", "scim:read")); clientRepo.save(client); - token = JWTParser.parse(getAccessTokenForClient("openid profile email scim:read")); + token = JWTParser.parse(getAccessTokenForClient("openid profile email scim:read", CLIENT_SECRET)); assertThat("scim:read", in(token.getJWTClaimsSet().getClaim("scope").toString().split(" "))); } finally { clientRepo.delete(client); diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/service/client/ClientManagementServiceTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/service/client/ClientManagementServiceTests.java index 3f623679a9..26d3e43d23 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/service/client/ClientManagementServiceTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/service/client/ClientManagementServiceTests.java @@ -52,6 +52,7 @@ import org.springframework.data.domain.Sort.Direction; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.testcontainers.shaded.com.google.common.collect.Sets; import it.infn.mw.iam.IamLoginService; @@ -131,7 +132,7 @@ void testClientRetrieve() { RegisteredClientDTO client = managementService.retrieveClientByClientId("client").orElseThrow(); assertThat(client.getClientId(), is("client")); - assertThat(client.getClientSecret(), is("secret")); + assertThat(new BCryptPasswordEncoder(12).matches("secret", client.getClientSecret()), is(true)); assertThat(client.getGrantTypes(), hasItems(CODE, REDELEGATE, IMPLICIT, REFRESH_TOKEN)); assertThat(client.getScope(), hasItems("openid", "offline_access", "profile", "email", "address", "phone", "read-tasks", "write-tasks", "read:/", "write:/")); From c53ea686abe91fa1772976789ca87750b71c8353 Mon Sep 17 00:00:00 2001 From: Stefano Enrico Zotti Date: Mon, 28 Apr 2025 16:24:42 +0200 Subject: [PATCH 11/37] Rename migration hashed clietn secret --- ...ecret.java => V109__HashClientSecret.java} | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) rename iam-persistence/src/main/java/db/migration/mysql/{V106__HashClientSecret.java => V109__HashClientSecret.java} (73%) diff --git a/iam-persistence/src/main/java/db/migration/mysql/V106__HashClientSecret.java b/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java similarity index 73% rename from iam-persistence/src/main/java/db/migration/mysql/V106__HashClientSecret.java rename to iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java index fe1fc7acba..e5d9a372b0 100644 --- a/iam-persistence/src/main/java/db/migration/mysql/V106__HashClientSecret.java +++ b/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java @@ -15,6 +15,8 @@ */ package db.migration.mysql; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.rowset.SqlRowSet; @@ -24,7 +26,9 @@ import it.infn.mw.iam.persistence.migrations.BaseFlywayJavaMigrationAdapter; -public class V106__HashClientSecret extends BaseFlywayJavaMigrationAdapter { +public class V109__HashClientSecret extends BaseFlywayJavaMigrationAdapter { + + public static final Logger LOG = LoggerFactory.getLogger(V109__HashClientSecret.class); @Override public void migrate(JdbcTemplate jdbcTemplate) throws DataAccessException { @@ -33,6 +37,8 @@ public void migrate(JdbcTemplate jdbcTemplate) throws DataAccessException { PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(DEFAULT_ROUND); + LOG.debug("### START MIGRATION V109__HashClientSecret ###"); + SqlRowSet clientList = jdbcTemplate.queryForRowSet("SELECT id, client_secret FROM client_details"); while (clientList.next()) { @@ -42,12 +48,13 @@ public void migrate(JdbcTemplate jdbcTemplate) throws DataAccessException { } Long id = clientList.getLong("id"); - String PassEncrypted = passwordEncoder.encode(clientSecret); - - if (passwordEncoder.matches(clientSecret, PassEncrypted)) { - jdbcTemplate.update("UPDATE client_details SET client_secret=? WHERE id=?", PassEncrypted, id); - } + String secretHashed = passwordEncoder.encode(clientSecret); + assert !passwordEncoder.matches(clientSecret, secretHashed); + + jdbcTemplate.update("UPDATE client_details SET client_secret=? WHERE id=?", secretHashed, id); } + + LOG.debug("### END MIGRATION V109__HashClientSecret ###"); } } \ No newline at end of file From 5d41d25b3c3cc46a600d97a3f510a714afb16acf Mon Sep 17 00:00:00 2001 From: Stefano Enrico Zotti Date: Mon, 28 Apr 2025 16:33:46 +0200 Subject: [PATCH 12/37] Add new isClientOwner preAuthorize method --- .../management/ClientManagementAPIController.java | 2 +- .../IamMethodSecurityExpressionHandler.java | 8 ++++++-- .../expression/IamSecurityExpressionMethods.java | 14 +++++++++++++- .../IamWebSecurityExpressionHandler.java | 8 ++++++-- .../iam/test/util/IamSecurityExpressionsTests.java | 13 +++++++++---- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java index 60231fb9a0..ee1a9464a1 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java @@ -159,7 +159,7 @@ public void disableClient(@PathVariable String clientId) { @PostMapping("/{clientId}/secret") @ResponseStatus(CREATED) - @PreAuthorize("#iam.hasScope('iam:admin.write') or #iam.hasDashboardRole('ROLE_ADMIN')") + @PreAuthorize("#iam.hasScope('iam:admin.write') or #iam.hasDashboardRole('ROLE_ADMIN') or #iam.isClientOwner('clientId')") public RegisteredClientDTO rotateClientSecret(@PathVariable String clientId) { return managementService.generateNewClientSecret(clientId); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamMethodSecurityExpressionHandler.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamMethodSecurityExpressionHandler.java index 87af6e5429..7d7d0ff9ee 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamMethodSecurityExpressionHandler.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamMethodSecurityExpressionHandler.java @@ -22,6 +22,7 @@ import org.springframework.stereotype.Component; import it.infn.mw.iam.api.account.AccountUtils; +import it.infn.mw.iam.api.client.service.DefaultClientService; import it.infn.mw.iam.api.requests.GroupRequestUtils; import it.infn.mw.iam.core.userinfo.OAuth2AuthenticationScopeResolver; @@ -30,12 +31,15 @@ public class IamMethodSecurityExpressionHandler extends OAuth2MethodSecurityExpressionHandler { private final AccountUtils accountUtils; + private final DefaultClientService clientService; private final GroupRequestUtils groupRequestUtils; private final OAuth2AuthenticationScopeResolver scopeResolver; public IamMethodSecurityExpressionHandler(AccountUtils accountUtils, - GroupRequestUtils groupRequestUtils, OAuth2AuthenticationScopeResolver scopeResolver) { + DefaultClientService clientService, GroupRequestUtils groupRequestUtils, + OAuth2AuthenticationScopeResolver scopeResolver) { this.accountUtils = accountUtils; + this.clientService = clientService; this.groupRequestUtils = groupRequestUtils; this.scopeResolver = scopeResolver; } @@ -46,7 +50,7 @@ public StandardEvaluationContext createEvaluationContextInternal(Authentication StandardEvaluationContext ec = super.createEvaluationContextInternal(authentication, mi); ec.setVariable("iam", new IamSecurityExpressionMethods(authentication, accountUtils, - groupRequestUtils, scopeResolver)); + clientService, groupRequestUtils, scopeResolver)); return ec; } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java index c859d17715..8778713a97 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java @@ -20,11 +20,13 @@ import java.util.Collection; import java.util.Optional; +import org.mitre.oauth2.model.ClientDetailsEntity; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.provider.OAuth2Authentication; import it.infn.mw.iam.api.account.AccountUtils; +import it.infn.mw.iam.api.client.service.DefaultClientService; import it.infn.mw.iam.api.requests.GroupRequestUtils; import it.infn.mw.iam.authn.AbstractExternalAuthenticationToken; import it.infn.mw.iam.core.IamGroupRequestStatus; @@ -40,13 +42,16 @@ public class IamSecurityExpressionMethods { private final Authentication authentication; private final AccountUtils accountUtils; + private final DefaultClientService clientService; private final GroupRequestUtils groupRequestUtils; private final OAuth2AuthenticationScopeResolver scopeResolver; public IamSecurityExpressionMethods(Authentication authentication, AccountUtils accountUtils, - GroupRequestUtils groupRequestUtils, OAuth2AuthenticationScopeResolver scopeResolver) { + DefaultClientService clientService, GroupRequestUtils groupRequestUtils, + OAuth2AuthenticationScopeResolver scopeResolver) { this.authentication = authentication; this.accountUtils = accountUtils; + this.clientService = clientService; this.groupRequestUtils = groupRequestUtils; this.scopeResolver = scopeResolver; } @@ -153,4 +158,11 @@ public boolean hasDashboardRole(Role role) { public boolean hasAdminOrGMDashboardRoleOfGroup(String gid) { return (hasDashboardRole(Role.ROLE_ADMIN) || isGroupManager(gid)); } + + public boolean isClientOwner(String clientId) { + Optional account = accountUtils.getAuthenticatedUserAccount(); + Optional client = + clientService.findClientByClientIdAndAccount(clientId, account.orElse(null)); + return client.isPresent(); + } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamWebSecurityExpressionHandler.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamWebSecurityExpressionHandler.java index bfc63a708c..c0ca02de12 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamWebSecurityExpressionHandler.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamWebSecurityExpressionHandler.java @@ -22,6 +22,7 @@ import org.springframework.stereotype.Component; import it.infn.mw.iam.api.account.AccountUtils; +import it.infn.mw.iam.api.client.service.DefaultClientService; import it.infn.mw.iam.api.requests.GroupRequestUtils; import it.infn.mw.iam.core.userinfo.OAuth2AuthenticationScopeResolver; @@ -30,12 +31,15 @@ public class IamWebSecurityExpressionHandler extends OAuth2WebSecurityExpressionHandler { private final AccountUtils accountUtils; + private final DefaultClientService clientService; private final GroupRequestUtils groupRequestUtils; private final OAuth2AuthenticationScopeResolver scopeResolver; public IamWebSecurityExpressionHandler(AccountUtils accountUtils, - GroupRequestUtils groupRequestUtils, OAuth2AuthenticationScopeResolver scopeResolver) { + DefaultClientService clientService, GroupRequestUtils groupRequestUtils, + OAuth2AuthenticationScopeResolver scopeResolver) { this.accountUtils = accountUtils; + this.clientService = clientService; this.groupRequestUtils = groupRequestUtils; this.scopeResolver = scopeResolver; } @@ -47,7 +51,7 @@ public StandardEvaluationContext createEvaluationContextInternal(Authentication StandardEvaluationContext ec = super.createEvaluationContextInternal(authentication, invocation); ec.setVariable("iam", new IamSecurityExpressionMethods(authentication, accountUtils, - groupRequestUtils, scopeResolver)); + clientService, groupRequestUtils, scopeResolver)); return ec; } diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java index d53159c4e2..13a09a44b6 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java @@ -31,6 +31,7 @@ import it.infn.mw.iam.IamLoginService; import it.infn.mw.iam.api.account.AccountUtils; +import it.infn.mw.iam.api.client.service.DefaultClientService; import it.infn.mw.iam.api.requests.GroupRequestUtils; import it.infn.mw.iam.api.requests.model.GroupRequestDto; import it.infn.mw.iam.core.expression.IamSecurityExpressionMethods; @@ -45,6 +46,9 @@ public class IamSecurityExpressionsTests extends GroupRequestsTestUtils { @Autowired private AccountUtils accountUtils; + @Autowired + private DefaultClientService clientService; + @Autowired private GroupRequestUtils groupRequestUtils; @@ -58,14 +62,15 @@ public class IamSecurityExpressionsTests extends GroupRequestsTestUtils { public void destroy() { repo.deleteAll(); } - + private IamSecurityExpressionMethods getMethods() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return new IamSecurityExpressionMethods(authentication, accountUtils, groupRequestUtils, scopeResolver); + return new IamSecurityExpressionMethods(authentication, accountUtils, clientService, + groupRequestUtils, scopeResolver); } @Test - @WithMockUser(roles = { "ADMIN", "USER" }, username = TEST_ADMIN) + @WithMockUser(roles = {"ADMIN", "USER"}, username = TEST_ADMIN) public void testIsAdmin() { assertTrue(getMethods().isAdmin()); assertTrue(getMethods().isUser(TEST_ADMIN_UUID)); @@ -77,7 +82,7 @@ public void testIsAdmin() { } @Test - @WithMockUser(roles = { "USER" }, username = TEST_USERNAME) + @WithMockUser(roles = {"USER"}, username = TEST_USERNAME) public void testIsNotAdmin() { assertFalse(getMethods().isAdmin()); assertTrue(getMethods().isUser(TEST_USERUUID)); From 71d08312db9caedd2deb8bb01938338f4ae2706a Mon Sep 17 00:00:00 2001 From: Federica Agostini Date: Fri, 18 Apr 2025 13:08:05 +0200 Subject: [PATCH 13/37] Add support for resource parameter (#916) - Add the requested resources to the consent page - Allow iam-test-client to perform 'resource' request - Add all granted resources if not requested at token endpoint - All granted resources can be requested also with aud or audience parameters at token endpoint --- .../infn/mw/iam/core/oauth/IamDeviceEndpointController.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/IamDeviceEndpointController.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/IamDeviceEndpointController.java index 4a7e0c7b22..30f7f8b4cf 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/IamDeviceEndpointController.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/IamDeviceEndpointController.java @@ -315,8 +315,9 @@ private void setModelForConsentPage(ModelMap model, Authentication authn, Device model.put("resources", splitBySpace(dc.getRequestParameters().get(RESOURCE))); } - // just for tests validation - model.put("scope", OAuth2Utils.formatParameterList(dc.getScope())); + // just for tests validation + model.put("scope",OAuth2Utils.formatParameterList(dc.getScope())); + } private void approveDevice(DeviceCode dc, OAuth2Authentication o2Auth) { From 014b432274b8e0857c9abd5177f06d59851dc5e2 Mon Sep 17 00:00:00 2001 From: enricovianello Date: Wed, 21 May 2025 11:53:25 +0200 Subject: [PATCH 14/37] Cosmetic fix --- .../infn/mw/iam/core/oauth/IamDeviceEndpointController.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/IamDeviceEndpointController.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/IamDeviceEndpointController.java index 30f7f8b4cf..4a7e0c7b22 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/IamDeviceEndpointController.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/IamDeviceEndpointController.java @@ -315,9 +315,8 @@ private void setModelForConsentPage(ModelMap model, Authentication authn, Device model.put("resources", splitBySpace(dc.getRequestParameters().get(RESOURCE))); } - // just for tests validation - model.put("scope",OAuth2Utils.formatParameterList(dc.getScope())); - + // just for tests validation + model.put("scope", OAuth2Utils.formatParameterList(dc.getScope())); } private void approveDevice(DeviceCode dc, OAuth2Authentication o2Auth) { From 2039dfcb07de67c4c8943834dd420b65b6bb1ccf Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Fri, 6 Jun 2025 16:09:52 +0200 Subject: [PATCH 15/37] Rollback isClientOwner --- .../management/ClientManagementAPIController.java | 2 +- .../IamMethodSecurityExpressionHandler.java | 7 ++----- .../expression/IamSecurityExpressionMethods.java | 14 +------------- .../IamWebSecurityExpressionHandler.java | 7 ++----- .../iam/test/util/IamSecurityExpressionsTests.java | 6 +----- 5 files changed, 7 insertions(+), 29 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java index ee1a9464a1..60231fb9a0 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java @@ -159,7 +159,7 @@ public void disableClient(@PathVariable String clientId) { @PostMapping("/{clientId}/secret") @ResponseStatus(CREATED) - @PreAuthorize("#iam.hasScope('iam:admin.write') or #iam.hasDashboardRole('ROLE_ADMIN') or #iam.isClientOwner('clientId')") + @PreAuthorize("#iam.hasScope('iam:admin.write') or #iam.hasDashboardRole('ROLE_ADMIN')") public RegisteredClientDTO rotateClientSecret(@PathVariable String clientId) { return managementService.generateNewClientSecret(clientId); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamMethodSecurityExpressionHandler.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamMethodSecurityExpressionHandler.java index 7d7d0ff9ee..67de843ad6 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamMethodSecurityExpressionHandler.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamMethodSecurityExpressionHandler.java @@ -31,15 +31,12 @@ public class IamMethodSecurityExpressionHandler extends OAuth2MethodSecurityExpressionHandler { private final AccountUtils accountUtils; - private final DefaultClientService clientService; private final GroupRequestUtils groupRequestUtils; private final OAuth2AuthenticationScopeResolver scopeResolver; public IamMethodSecurityExpressionHandler(AccountUtils accountUtils, - DefaultClientService clientService, GroupRequestUtils groupRequestUtils, - OAuth2AuthenticationScopeResolver scopeResolver) { + GroupRequestUtils groupRequestUtils, OAuth2AuthenticationScopeResolver scopeResolver) { this.accountUtils = accountUtils; - this.clientService = clientService; this.groupRequestUtils = groupRequestUtils; this.scopeResolver = scopeResolver; } @@ -50,7 +47,7 @@ public StandardEvaluationContext createEvaluationContextInternal(Authentication StandardEvaluationContext ec = super.createEvaluationContextInternal(authentication, mi); ec.setVariable("iam", new IamSecurityExpressionMethods(authentication, accountUtils, - clientService, groupRequestUtils, scopeResolver)); + groupRequestUtils, scopeResolver)); return ec; } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java index 8778713a97..c859d17715 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java @@ -20,13 +20,11 @@ import java.util.Collection; import java.util.Optional; -import org.mitre.oauth2.model.ClientDetailsEntity; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.provider.OAuth2Authentication; import it.infn.mw.iam.api.account.AccountUtils; -import it.infn.mw.iam.api.client.service.DefaultClientService; import it.infn.mw.iam.api.requests.GroupRequestUtils; import it.infn.mw.iam.authn.AbstractExternalAuthenticationToken; import it.infn.mw.iam.core.IamGroupRequestStatus; @@ -42,16 +40,13 @@ public class IamSecurityExpressionMethods { private final Authentication authentication; private final AccountUtils accountUtils; - private final DefaultClientService clientService; private final GroupRequestUtils groupRequestUtils; private final OAuth2AuthenticationScopeResolver scopeResolver; public IamSecurityExpressionMethods(Authentication authentication, AccountUtils accountUtils, - DefaultClientService clientService, GroupRequestUtils groupRequestUtils, - OAuth2AuthenticationScopeResolver scopeResolver) { + GroupRequestUtils groupRequestUtils, OAuth2AuthenticationScopeResolver scopeResolver) { this.authentication = authentication; this.accountUtils = accountUtils; - this.clientService = clientService; this.groupRequestUtils = groupRequestUtils; this.scopeResolver = scopeResolver; } @@ -158,11 +153,4 @@ public boolean hasDashboardRole(Role role) { public boolean hasAdminOrGMDashboardRoleOfGroup(String gid) { return (hasDashboardRole(Role.ROLE_ADMIN) || isGroupManager(gid)); } - - public boolean isClientOwner(String clientId) { - Optional account = accountUtils.getAuthenticatedUserAccount(); - Optional client = - clientService.findClientByClientIdAndAccount(clientId, account.orElse(null)); - return client.isPresent(); - } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamWebSecurityExpressionHandler.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamWebSecurityExpressionHandler.java index c0ca02de12..754aa3edc4 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamWebSecurityExpressionHandler.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamWebSecurityExpressionHandler.java @@ -31,15 +31,12 @@ public class IamWebSecurityExpressionHandler extends OAuth2WebSecurityExpressionHandler { private final AccountUtils accountUtils; - private final DefaultClientService clientService; private final GroupRequestUtils groupRequestUtils; private final OAuth2AuthenticationScopeResolver scopeResolver; public IamWebSecurityExpressionHandler(AccountUtils accountUtils, - DefaultClientService clientService, GroupRequestUtils groupRequestUtils, - OAuth2AuthenticationScopeResolver scopeResolver) { + GroupRequestUtils groupRequestUtils, OAuth2AuthenticationScopeResolver scopeResolver) { this.accountUtils = accountUtils; - this.clientService = clientService; this.groupRequestUtils = groupRequestUtils; this.scopeResolver = scopeResolver; } @@ -51,7 +48,7 @@ public StandardEvaluationContext createEvaluationContextInternal(Authentication StandardEvaluationContext ec = super.createEvaluationContextInternal(authentication, invocation); ec.setVariable("iam", new IamSecurityExpressionMethods(authentication, accountUtils, - clientService, groupRequestUtils, scopeResolver)); + groupRequestUtils, scopeResolver)); return ec; } diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java index 13a09a44b6..c4b8c68b61 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java @@ -31,7 +31,6 @@ import it.infn.mw.iam.IamLoginService; import it.infn.mw.iam.api.account.AccountUtils; -import it.infn.mw.iam.api.client.service.DefaultClientService; import it.infn.mw.iam.api.requests.GroupRequestUtils; import it.infn.mw.iam.api.requests.model.GroupRequestDto; import it.infn.mw.iam.core.expression.IamSecurityExpressionMethods; @@ -46,9 +45,6 @@ public class IamSecurityExpressionsTests extends GroupRequestsTestUtils { @Autowired private AccountUtils accountUtils; - @Autowired - private DefaultClientService clientService; - @Autowired private GroupRequestUtils groupRequestUtils; @@ -65,7 +61,7 @@ public void destroy() { private IamSecurityExpressionMethods getMethods() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return new IamSecurityExpressionMethods(authentication, accountUtils, clientService, + return new IamSecurityExpressionMethods(authentication, accountUtils, groupRequestUtils, scopeResolver); } From 7dbf5090729a829a8de7ea459a448658859cb9ca Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Fri, 6 Jun 2025 16:18:11 +0200 Subject: [PATCH 16/37] Fix saveNewClient --- .../service/DefaultClientRegistrationService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java index 35fcff364b..bad827385b 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java @@ -366,11 +366,9 @@ public RegisteredClientDTO registerClient(RegisteredClientDTO request, if (isNull(client.getClientSecret())) { client.setClientSecret(defaultsService.generateClientSecret()); } - String tmpClientSecret = client.getClientSecret(); - client = clientService.saveNewClient(client); - client.setClientSecret(tmpClientSecret); RegisteredClientDTO response = converter.registrationResponseFromClient(client); + client = clientService.saveNewClient(client); if (isAnonymous(authentication)) { From 5713088601d18be335e73fe016b77a1819d4d6b4 Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Fri, 6 Jun 2025 16:20:48 +0200 Subject: [PATCH 17/37] Remove useless code --- .../management/service/DefaultClientManagementService.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java index eb719fa234..0b201ed298 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java @@ -187,8 +187,6 @@ public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO cli newClient.setClientSecret(null); } else if (isNull(client.getClientSecret())) { client.setClientSecret(defaultsService.generateClientSecret()); - } else { - newClient.setClientSecret(defaultsService.generateClientSecret()); } newClient = clientService.updateClient(newClient); @@ -219,7 +217,6 @@ public RegisteredClientDTO generateNewClientSecret(String clientId) { .orElseThrow(ClientSuppliers.clientNotFound(clientId)); String pwd = defaultsService.generateClientSecret(); - client.setClientSecret(pwd); client.setClientSecret(bcrypt().encode(pwd)); client = clientService.updateClient(client); eventPublisher.publishEvent(new ClientSecretUpdatedEvent(this, client)); From 94ecf959067f6ddeac4b7395af661213b9e8f126 Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Fri, 6 Jun 2025 19:37:06 +0200 Subject: [PATCH 18/37] Fix tests --- .../DefaultClientRegistrationService.java | 7 +----- .../client/service/DefaultClientService.java | 23 +++++++++++-------- ...tAuthenticationIntegrationTestSupport.java | 9 ++++++-- .../oauth/devicecode/DeviceCodeTests.java | 2 +- .../mysql/V109__HashClientSecret.java | 7 ++++-- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java index bad827385b..bfaf5169d0 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java @@ -47,8 +47,8 @@ import org.springframework.validation.annotation.Validated; import it.infn.mw.iam.api.account.AccountUtils; -import it.infn.mw.iam.api.client.error.InvalidClientRegistrationRequest; import it.infn.mw.iam.api.client.error.ClientSuspended; +import it.infn.mw.iam.api.client.error.InvalidClientRegistrationRequest; import it.infn.mw.iam.api.client.registration.validation.OnDynamicClientRegistration; import it.infn.mw.iam.api.client.registration.validation.OnDynamicClientUpdate; import it.infn.mw.iam.api.client.service.ClientConverter; @@ -56,7 +56,6 @@ import it.infn.mw.iam.api.client.service.ClientService; import it.infn.mw.iam.api.common.client.AuthorizationGrantType; import it.infn.mw.iam.api.common.client.RegisteredClientDTO; -import it.infn.mw.iam.api.common.client.TokenEndpointAuthenticationMethod; import it.infn.mw.iam.audit.events.account.client.AccountClientOwnerAssigned; import it.infn.mw.iam.audit.events.client.ClientRegistered; import it.infn.mw.iam.audit.events.client.ClientRegistrationAccessTokenRotatedEvent; @@ -363,10 +362,6 @@ public RegisteredClientDTO registerClient(RegisteredClientDTO request, checkAllowedGrantTypes(request, authentication); cleanupRequestedScopes(client, authentication); - if (isNull(client.getClientSecret())) { - client.setClientSecret(defaultsService.generateClientSecret()); - } - RegisteredClientDTO response = converter.registrationResponseFromClient(client); client = clientService.saveNewClient(client); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java index 1f65f9861d..efba7509ef 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java @@ -16,6 +16,7 @@ package it.infn.mw.iam.api.client.service; import static it.infn.mw.iam.util.IamBcryptUtil.bcrypt; +import static java.util.Objects.isNull; import java.time.Clock; import java.util.Date; @@ -55,7 +56,8 @@ public class DefaultClientService implements ClientService { private OAuth2TokenEntityService tokenService; public DefaultClientService(Clock clock, IamClientRepository clientRepo, - IamAccountClientRepository accountClientRepo, ApplicationEventPublisher eventPublisher, OAuth2TokenEntityService tokenService) { + IamAccountClientRepository accountClientRepo, ApplicationEventPublisher eventPublisher, + OAuth2TokenEntityService tokenService) { this.clock = clock; this.clientRepo = clientRepo; this.accountClientRepo = accountClientRepo; @@ -67,7 +69,9 @@ public DefaultClientService(Clock clock, IamClientRepository clientRepo, public ClientDetailsEntity saveNewClient(ClientDetailsEntity client) { client.setCreatedAt(Date.from(clock.instant())); eventPublisher.publishEvent(new ClientCreatedEvent(this, client)); - client.setClientSecret(bcrypt().encode(client.getClientSecret())); + if (!isNull(client.getClientSecret())) { + client.setClientSecret(bcrypt().encode(client.getClientSecret())); + } return clientRepo.save(client); } @@ -86,7 +90,7 @@ private Supplier newAccountClient(IamAccount owner, @Override public ClientDetailsEntity linkClientToAccount(ClientDetailsEntity client, IamAccount owner) { IamAccountClient ac = accountClientRepo.findByAccountAndClient(owner, client) - .orElseGet(newAccountClient(owner, client)); + .orElseGet(newAccountClient(owner, client)); return ac.getClient(); } @@ -106,7 +110,8 @@ public ClientDetailsEntity updateClient(ClientDetailsEntity client) { } @Override - public ClientDetailsEntity updateClientStatus(ClientDetailsEntity client, boolean status, String userId) { + public ClientDetailsEntity updateClientStatus(ClientDetailsEntity client, boolean status, + String userId) { client.setActive(status); client.setStatusChangedBy(userId); client.setStatusChangedOn(Date.from(clock.instant())); @@ -127,7 +132,7 @@ public Optional findClientByClientIdAndAccount(String clien if (maybeClient.isPresent()) { return accountClientRepo.findByAccountAndClientId(account, maybeClient.get().getId()) - .map(IamAccountClient::getClient); + .map(IamAccountClient::getClient); } return Optional.empty(); @@ -149,12 +154,12 @@ private boolean isValidAccessToken(OAuth2AccessTokenEntity a) { private void deleteTokensByClient(ClientDetailsEntity client) { // delete all valid access tokens (exclude registration and resource tokens) tokenService.getAccessTokensForClient(client) - .stream() - .filter(this::isValidAccessToken) - .forEach(at -> tokenService.revokeAccessToken(at)); + .stream() + .filter(this::isValidAccessToken) + .forEach(at -> tokenService.revokeAccessToken(at)); // delete all valid refresh tokens tokenService.getRefreshTokensForClient(client) - .forEach(rt -> tokenService.revokeRefreshToken(rt)); + .forEach(rt -> tokenService.revokeRefreshToken(rt)); } @Override diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/assertion/JWTBearerClientAuthenticationIntegrationTestSupport.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/assertion/JWTBearerClientAuthenticationIntegrationTestSupport.java index de26d64797..763de342c6 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/assertion/JWTBearerClientAuthenticationIntegrationTestSupport.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/assertion/JWTBearerClientAuthenticationIntegrationTestSupport.java @@ -24,6 +24,7 @@ import java.util.UUID; import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.oauth2.model.ClientDetailsEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ResourceLoader; @@ -36,13 +37,13 @@ import com.nimbusds.jwt.SignedJWT; import it.infn.mw.iam.core.jwk.IamJWTSigningService; +import it.infn.mw.iam.persistence.repository.client.IamClientRepository; import it.infn.mw.iam.test.oauth.EndpointsTestUtils; import it.infn.mw.iam.util.JWKKeystoreLoader; public class JWTBearerClientAuthenticationIntegrationTestSupport extends EndpointsTestUtils { public static final String CLIENT_ID_SECRET_JWT = "jwt-auth-client_secret_jwt"; - public static final String CLIENT_ID_SECRET_JWT_SECRET = "c8e9eed0-e6e4-4a66-b16e-6f37096356a7"; public static final String TOKEN_ENDPOINT_AUDIENCE = "http://localhost:8080/token"; public static final String TOKEN_ENDPOINT = "/token"; public static final String JWT_BEARER_ASSERTION_TYPE = @@ -55,10 +56,14 @@ public class JWTBearerClientAuthenticationIntegrationTestSupport extends Endpoin @Autowired ResourceLoader loader; + @Autowired + private IamClientRepository clientRepo; + public SignedJWT createSymmetricClientAuthToken(String clientId, Instant expirationTime) throws JOSEException { - JWSSigner signer = new MACSigner(CLIENT_ID_SECRET_JWT_SECRET); + ClientDetailsEntity client = clientRepo.findByClientId(CLIENT_ID_SECRET_JWT).orElseThrow(); + JWSSigner signer = new MACSigner(client.getClientSecret()); JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().subject(clientId) .issuer(clientId) .expirationTime(Date.from(expirationTime)) diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/devicecode/DeviceCodeTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/devicecode/DeviceCodeTests.java index 558faf6892..1cd2520284 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/devicecode/DeviceCodeTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/devicecode/DeviceCodeTests.java @@ -596,7 +596,7 @@ public void deviceCodeWorksForDynamicallyRegisteredClient() assertNotNull(newClient); - String tokenResponse = getTokenResponse(newClient.getClientId(), newClient.getClientSecret(), + String tokenResponse = getTokenResponse(newClient.getClientId(), registrationResponse.getClientSecret(), TEST_USERNAME, TEST_PASSWORD, "openid profile offline_access"); JsonNode tokenResponseJson = mapper.readTree(tokenResponse); diff --git a/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java b/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java index e5d9a372b0..d83f9befbf 100644 --- a/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java +++ b/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java @@ -49,8 +49,11 @@ public void migrate(JdbcTemplate jdbcTemplate) throws DataAccessException { Long id = clientList.getLong("id"); String secretHashed = passwordEncoder.encode(clientSecret); - assert !passwordEncoder.matches(clientSecret, secretHashed); - + if(passwordEncoder.matches(clientSecret, secretHashed)) { + LOG.info("Client secret already hashed"); + return; + } + jdbcTemplate.update("UPDATE client_details SET client_secret=? WHERE id=?", secretHashed, id); } From 5a5ffbb5898d841128282dd26d951e313184e49e Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Fri, 6 Jun 2025 22:14:53 +0200 Subject: [PATCH 19/37] Fix rebase and format --- .../java/db/migration/mysql/V109__HashClientSecret.java | 7 ++++--- .../resources/db/migration/test/V100000___test_data.sql | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java b/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java index d83f9befbf..6d8ead6a35 100644 --- a/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java +++ b/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java @@ -39,7 +39,8 @@ public void migrate(JdbcTemplate jdbcTemplate) throws DataAccessException { LOG.debug("### START MIGRATION V109__HashClientSecret ###"); - SqlRowSet clientList = jdbcTemplate.queryForRowSet("SELECT id, client_secret FROM client_details"); + SqlRowSet clientList = + jdbcTemplate.queryForRowSet("SELECT id, client_secret FROM client_details"); while (clientList.next()) { String clientSecret = clientList.getString("client_secret"); @@ -49,7 +50,7 @@ public void migrate(JdbcTemplate jdbcTemplate) throws DataAccessException { Long id = clientList.getLong("id"); String secretHashed = passwordEncoder.encode(clientSecret); - if(passwordEncoder.matches(clientSecret, secretHashed)) { + if (passwordEncoder.matches(clientSecret, secretHashed)) { LOG.info("Client secret already hashed"); return; } @@ -60,4 +61,4 @@ public void migrate(JdbcTemplate jdbcTemplate) throws DataAccessException { LOG.debug("### END MIGRATION V109__HashClientSecret ###"); } -} \ No newline at end of file +} diff --git a/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql b/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql index e7821e0261..b42f42ac8a 100644 --- a/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql +++ b/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql @@ -26,7 +26,8 @@ INSERT INTO client_details (id, client_id, client_secret, client_name, dynamical (14, 'public-dc-client', null, 'Public Device Code client', false, null, 3600, 600, false, null, false, 600, CURRENT_TIMESTAMP(), true), (17, 'admin-client-ro', 'secret', 'Admin client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), (18, 'admin-client-rw', 'secret', 'Admin client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (19, 'public-client', null, 'Public client', false, 3600, 3600, 600, true, 'NONE', false, null, CURRENT_TIMESTAMP(), true); + (19, 'public-client', null, 'Public client', false, 3600, 3600, 600, true, 'NONE', false, null, CURRENT_TIMESTAMP(), true), + (20, 'refresh-client', 'secret', 'Refresh Flow client', false, 36000, 3600, 600, true, 'SECRET_BASIC', true, 30, CURRENT_TIMESTAMP(), true); INSERT INTO client_details (id, client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection, From 0936dbcf6996b8c63d5fca17c66b8dae2ab8e935 Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Mon, 9 Jun 2025 22:20:18 +0200 Subject: [PATCH 20/37] Restore AuthorizationServerConfig from v1.12.0 --- .../iam/config/AuthorizationServerConfig.java | 20 ++++++------------- .../config/security/IamApiSecurityConfig.java | 4 ++-- ...tUtil.java => IamClientSecretEncoder.java} | 0 3 files changed, 8 insertions(+), 16 deletions(-) rename iam-login-service/src/main/java/it/infn/mw/iam/util/{IamBcryptUtil.java => IamClientSecretEncoder.java} (100%) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java index 6edbfdc5da..42a412c445 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java @@ -17,8 +17,6 @@ import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import org.mitre.oauth2.service.ClientDetailsEntityService; import org.mitre.oauth2.service.DeviceCodeService; @@ -35,8 +33,6 @@ import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; @@ -119,20 +115,10 @@ AuthenticationEventPublisher iamAuthenticationEventPublisher() { return new IamAuthenticationEventPublisher(); } - @Bean(name = "passwordEncoder") - public PasswordEncoder passwordEncoder() { - String encodingId = "bcrypt"; - Map encoders = new HashMap<>(); - encoders.put(encodingId, new BCryptPasswordEncoder()); - // encoders.put("noop", NoOpPasswordEncoder.getInstance()); - return new DelegatingPasswordEncoder(encodingId, encoders); - } - @Bean(name = "authenticationManager") AuthenticationManager authenticationManager() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); - provider.setPasswordEncoder(passwordEncoder()); provider.setUserDetailsService(iamUserDetailsService); provider.setPasswordEncoder(passwordEncoder); @@ -141,6 +127,7 @@ AuthenticationManager authenticationManager() { pm.setAuthenticationEventPublisher(iamAuthenticationEventPublisher()); return pm; + } @Bean @@ -200,11 +187,16 @@ public void configure(final AuthorizationServerEndpointsConfigurer endpoints) th public void configure(final ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetailsService); + } @Override public void configure(final AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients(); + + } + + } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java index da6e072fe7..b5289d7be1 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java @@ -31,7 +31,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter; import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler; @@ -66,7 +66,7 @@ public static class IamProxyCertificateApiConfig extends WebSecurityConfigurerAd @Override protected void configure(final AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) - .passwordEncoder(new BCryptPasswordEncoder()); + .passwordEncoder(NoOpPasswordEncoder.getInstance()); } @Override diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/util/IamBcryptUtil.java b/iam-login-service/src/main/java/it/infn/mw/iam/util/IamClientSecretEncoder.java similarity index 100% rename from iam-login-service/src/main/java/it/infn/mw/iam/util/IamBcryptUtil.java rename to iam-login-service/src/main/java/it/infn/mw/iam/util/IamClientSecretEncoder.java From 76a11fc220376cb1103b0a920f2d63098c184fdd Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Mon, 9 Jun 2025 22:25:10 +0200 Subject: [PATCH 21/37] Refactor password encoder and other fixes --- .../DefaultClientManagementService.java | 6 ++-- .../client/service/DefaultClientService.java | 4 +-- .../config/security/IamApiSecurityConfig.java | 4 +-- .../IamTokenEndointSecurityConfig.java | 4 +-- .../config/security/MitreSecurityConfig.java | 12 ++----- .../mw/iam/util/IamClientSecretEncoder.java | 34 ++++++++++++------- .../client/ClientManagementServiceTests.java | 4 +-- .../util/IamSecurityExpressionsTests.java | 8 +++-- .../mysql/V109__HashClientSecret.java | 9 +++-- 9 files changed, 46 insertions(+), 39 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java index 0b201ed298..7f73bda324 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java @@ -17,7 +17,6 @@ import static it.infn.mw.iam.api.client.util.ClientSuppliers.accountNotFound; import static it.infn.mw.iam.api.client.util.ClientSuppliers.clientNotFound; -import static it.infn.mw.iam.util.IamBcryptUtil.bcrypt; import static java.util.Objects.isNull; import static org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod.NONE; @@ -62,6 +61,7 @@ import it.infn.mw.iam.persistence.model.IamAccount; import it.infn.mw.iam.persistence.model.IamAccountClient; import it.infn.mw.iam.persistence.repository.IamAccountRepository; +import it.infn.mw.iam.util.IamClientSecretEncoder; @Service @Validated @@ -126,7 +126,7 @@ public RegisteredClientDTO saveNewClient(RegisteredClientDTO client) throws Pars ClientDetailsEntity entity = converter.entityFromClientManagementRequest(client); entity.setDynamicallyRegistered(false); entity.setCreatedAt(Date.from(clock.instant())); - entity.setClientSecret(bcrypt().encode(secret)); + entity.setClientSecret(new IamClientSecretEncoder().encode(secret)); entity.setActive(true); defaultsService.setupClientDefaults(entity); @@ -217,7 +217,7 @@ public RegisteredClientDTO generateNewClientSecret(String clientId) { .orElseThrow(ClientSuppliers.clientNotFound(clientId)); String pwd = defaultsService.generateClientSecret(); - client.setClientSecret(bcrypt().encode(pwd)); + client.setClientSecret(new IamClientSecretEncoder().encode(pwd)); client = clientService.updateClient(client); eventPublisher.publishEvent(new ClientSecretUpdatedEvent(this, client)); RegisteredClientDTO clientWithSecret = converter.registeredClientDtoFromEntity(client); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java index efba7509ef..bc96b87e86 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java @@ -15,7 +15,6 @@ */ package it.infn.mw.iam.api.client.service; -import static it.infn.mw.iam.util.IamBcryptUtil.bcrypt; import static java.util.Objects.isNull; import java.time.Clock; @@ -40,6 +39,7 @@ import it.infn.mw.iam.persistence.repository.client.ClientSpecs; import it.infn.mw.iam.persistence.repository.client.IamAccountClientRepository; import it.infn.mw.iam.persistence.repository.client.IamClientRepository; +import it.infn.mw.iam.util.IamClientSecretEncoder; @Service @Transactional @@ -70,7 +70,7 @@ public ClientDetailsEntity saveNewClient(ClientDetailsEntity client) { client.setCreatedAt(Date.from(clock.instant())); eventPublisher.publishEvent(new ClientCreatedEvent(this, client)); if (!isNull(client.getClientSecret())) { - client.setClientSecret(bcrypt().encode(client.getClientSecret())); + client.setClientSecret(new IamClientSecretEncoder().encode(client.getClientSecret())); } return clientRepo.save(client); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java index b5289d7be1..a34314a74f 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamApiSecurityConfig.java @@ -31,7 +31,6 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter; import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler; @@ -42,6 +41,7 @@ import it.infn.mw.iam.config.IamProperties; import it.infn.mw.iam.config.security.IamWebSecurityConfig.UserLoginConfig; import it.infn.mw.iam.core.oauth.FormClientCredentialsAuthenticationFilter; +import it.infn.mw.iam.util.IamClientSecretEncoder; @SuppressWarnings("deprecation") @Configuration @@ -66,7 +66,7 @@ public static class IamProxyCertificateApiConfig extends WebSecurityConfigurerAd @Override protected void configure(final AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) - .passwordEncoder(NoOpPasswordEncoder.getInstance()); + .passwordEncoder(new IamClientSecretEncoder()); } @Override diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamTokenEndointSecurityConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamTokenEndointSecurityConfig.java index 6877949c19..f2b9e18cea 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamTokenEndointSecurityConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/IamTokenEndointSecurityConfig.java @@ -15,7 +15,6 @@ */ package it.infn.mw.iam.config.security; -import static it.infn.mw.iam.util.IamBcryptUtil.bcrypt; import static java.util.Collections.singletonList; import static org.springframework.http.HttpMethod.OPTIONS; @@ -43,6 +42,7 @@ import it.infn.mw.iam.config.IamProperties; import it.infn.mw.iam.core.client.ClientUserDetailsService; import it.infn.mw.iam.core.oauth.assertion.IAMJWTBearerAuthenticationProvider; +import it.infn.mw.iam.util.IamClientSecretEncoder; @SuppressWarnings("deprecation") @Configuration @@ -71,7 +71,7 @@ public class IamTokenEndointSecurityConfig extends WebSecurityConfigurerAdapter protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) - .passwordEncoder(bcrypt()); + .passwordEncoder(new IamClientSecretEncoder()); } @Bean diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/MitreSecurityConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/MitreSecurityConfig.java index 4061881e8f..81bdc4c2c1 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/security/MitreSecurityConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/security/MitreSecurityConfig.java @@ -28,7 +28,6 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter; import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter; import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; @@ -37,6 +36,7 @@ import org.springframework.security.web.context.SecurityContextPersistenceFilter; import it.infn.mw.iam.api.client.registration.ClientRegistrationApiController; +import it.infn.mw.iam.util.IamClientSecretEncoder; @SuppressWarnings("deprecation") @Configuration @@ -180,15 +180,12 @@ public static class IntrospectEndpointAuthorizationConfig extends WebSecurityCon @Autowired @Qualifier("clientUserDetailsService") private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; @Override protected void configure(final AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) - .passwordEncoder(passwordEncoder); + .passwordEncoder(new IamClientSecretEncoder()); } @Override @@ -222,9 +219,6 @@ public static class RevokeEndpointAuthorizationConfig extends WebSecurityConfigu @Autowired private OAuth2AuthenticationEntryPoint authenticationEntryPoint; - @Autowired - private PasswordEncoder passwordEncoder; - @Autowired @Qualifier("clientUserDetailsService") private UserDetailsService userDetailsService; @@ -233,7 +227,7 @@ public static class RevokeEndpointAuthorizationConfig extends WebSecurityConfigu protected void configure(final AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) - .passwordEncoder(passwordEncoder); + .passwordEncoder(new IamClientSecretEncoder()); } private ClientCredentialsTokenEndpointFilter clientCredentialsEndpointFilter() diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/util/IamClientSecretEncoder.java b/iam-login-service/src/main/java/it/infn/mw/iam/util/IamClientSecretEncoder.java index 8cde72f926..5dd2566cb9 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/util/IamClientSecretEncoder.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/util/IamClientSecretEncoder.java @@ -16,28 +16,38 @@ package it.infn.mw.iam.util; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; /** * A simple util to quickly get a password bcrypt-encoded * */ -public class IamBcryptUtil { +public class IamClientSecretEncoder implements PasswordEncoder { - public static void main(String[] args) { + final short DEFAULT_ROUND = 12; - if (args.length == 0) { - System.err.println("Please provide the password to encode as an argument"); - System.exit(1); - } - - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + BCryptPasswordEncoder bcryptEncoder = new BCryptPasswordEncoder(DEFAULT_ROUND); - System.out.println(encoder.encode(args[0])); + @Override + public String encode(CharSequence rawPassword) { + if (rawPassword.isEmpty()) { + return rawPassword.toString(); + } + return bcryptEncoder.encode(rawPassword); } - public static final BCryptPasswordEncoder bcrypt() { - final short DEFAULT_ROUND = 12; - return new BCryptPasswordEncoder(DEFAULT_ROUND); + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + if (rawPassword == null) { + throw new IllegalArgumentException("rawPassword cannot be null"); + } + if (encodedPassword == null) { + throw new IllegalArgumentException("encodedPassword cannot be null"); + } + if (rawPassword.isEmpty() && encodedPassword.isEmpty()) { + return true; + } + return bcryptEncoder.matches(rawPassword, encodedPassword); } } diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/service/client/ClientManagementServiceTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/service/client/ClientManagementServiceTests.java index 26d3e43d23..60542d7d7b 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/service/client/ClientManagementServiceTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/service/client/ClientManagementServiceTests.java @@ -52,7 +52,6 @@ import org.springframework.data.domain.Sort.Direction; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.testcontainers.shaded.com.google.common.collect.Sets; import it.infn.mw.iam.IamLoginService; @@ -68,6 +67,7 @@ import it.infn.mw.iam.persistence.model.IamAccount; import it.infn.mw.iam.persistence.repository.IamAccountRepository; import it.infn.mw.iam.test.util.annotation.IamNoMvcTest; +import it.infn.mw.iam.util.IamClientSecretEncoder; @IamNoMvcTest @SpringBootTest(classes = {IamLoginService.class, ClientTestConfig.class}, @@ -132,7 +132,7 @@ void testClientRetrieve() { RegisteredClientDTO client = managementService.retrieveClientByClientId("client").orElseThrow(); assertThat(client.getClientId(), is("client")); - assertThat(new BCryptPasswordEncoder(12).matches("secret", client.getClientSecret()), is(true)); + assertThat(new IamClientSecretEncoder().matches("secret", client.getClientSecret()), is(true)); assertThat(client.getGrantTypes(), hasItems(CODE, REDELEGATE, IMPLICIT, REFRESH_TOKEN)); assertThat(client.getScope(), hasItems("openid", "offline_access", "profile", "email", "address", "phone", "read-tasks", "write-tasks", "read:/", "write:/")); diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java index c4b8c68b61..ff2793582b 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java @@ -31,6 +31,7 @@ import it.infn.mw.iam.IamLoginService; import it.infn.mw.iam.api.account.AccountUtils; +import it.infn.mw.iam.api.client.service.DefaultClientService; import it.infn.mw.iam.api.requests.GroupRequestUtils; import it.infn.mw.iam.api.requests.model.GroupRequestDto; import it.infn.mw.iam.core.expression.IamSecurityExpressionMethods; @@ -54,6 +55,9 @@ public class IamSecurityExpressionsTests extends GroupRequestsTestUtils { @Autowired private IamGroupRequestRepository repo; + @Autowired + private DefaultClientService clientService; + @After public void destroy() { repo.deleteAll(); @@ -61,8 +65,8 @@ public void destroy() { private IamSecurityExpressionMethods getMethods() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return new IamSecurityExpressionMethods(authentication, accountUtils, - groupRequestUtils, scopeResolver); + return new IamSecurityExpressionMethods(authentication, accountUtils, groupRequestUtils, + scopeResolver, clientService); } @Test diff --git a/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java b/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java index 6d8ead6a35..78235e5653 100644 --- a/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java +++ b/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java @@ -23,7 +23,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; - import it.infn.mw.iam.persistence.migrations.BaseFlywayJavaMigrationAdapter; public class V109__HashClientSecret extends BaseFlywayJavaMigrationAdapter { @@ -49,13 +48,13 @@ public void migrate(JdbcTemplate jdbcTemplate) throws DataAccessException { } Long id = clientList.getLong("id"); - String secretHashed = passwordEncoder.encode(clientSecret); - if (passwordEncoder.matches(clientSecret, secretHashed)) { + String secretHash = passwordEncoder.encode(clientSecret); + if (passwordEncoder.matches(clientSecret, secretHash)) { LOG.info("Client secret already hashed"); - return; + continue; } - jdbcTemplate.update("UPDATE client_details SET client_secret=? WHERE id=?", secretHashed, id); + jdbcTemplate.update("UPDATE client_details SET client_secret=? WHERE id=?", secretHash, id); } LOG.debug("### END MIGRATION V109__HashClientSecret ###"); From a62e15937ba0af8353736a2ce7ef4755cefdf35a Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Mon, 9 Jun 2025 22:37:05 +0200 Subject: [PATCH 22/37] Fix test --- .../infn/mw/iam/test/util/IamSecurityExpressionsTests.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java index ff2793582b..3f4fa1fa81 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java @@ -31,7 +31,6 @@ import it.infn.mw.iam.IamLoginService; import it.infn.mw.iam.api.account.AccountUtils; -import it.infn.mw.iam.api.client.service.DefaultClientService; import it.infn.mw.iam.api.requests.GroupRequestUtils; import it.infn.mw.iam.api.requests.model.GroupRequestDto; import it.infn.mw.iam.core.expression.IamSecurityExpressionMethods; @@ -55,8 +54,6 @@ public class IamSecurityExpressionsTests extends GroupRequestsTestUtils { @Autowired private IamGroupRequestRepository repo; - @Autowired - private DefaultClientService clientService; @After public void destroy() { @@ -66,7 +63,7 @@ public void destroy() { private IamSecurityExpressionMethods getMethods() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return new IamSecurityExpressionMethods(authentication, accountUtils, groupRequestUtils, - scopeResolver, clientService); + scopeResolver); } @Test From b597d06e7e24656757acffc45cb2b98bf6e9e7cc Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Wed, 11 Jun 2025 18:49:33 +0200 Subject: [PATCH 23/37] Allow client owner to regenerate secret --- .../ClientManagementAPIController.java | 2 +- .../IamMethodSecurityExpressionHandler.java | 12 +++++++--- .../IamSecurityExpressionMethods.java | 22 ++++++++++++++++++- .../IamWebSecurityExpressionHandler.java | 12 +++++++--- .../mw/iam/util/IamClientSecretEncoder.java | 2 +- .../matchers/ScopeMatcherNoCacheTests.java | 4 ++-- .../util/IamSecurityExpressionsTests.java | 9 +++++++- .../mysql/V109__HashClientSecret.java | 2 +- 8 files changed, 52 insertions(+), 13 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java index 60231fb9a0..dc150475a0 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java @@ -159,7 +159,7 @@ public void disableClient(@PathVariable String clientId) { @PostMapping("/{clientId}/secret") @ResponseStatus(CREATED) - @PreAuthorize("#iam.hasScope('iam:admin.write') or #iam.hasDashboardRole('ROLE_ADMIN')") + @PreAuthorize("#iam.hasScope('iam:admin.write') or #iam.hasDashboardRole('ROLE_ADMIN') or #iam.isClientOwner(#clientId)") public RegisteredClientDTO rotateClientSecret(@PathVariable String clientId) { return managementService.generateNewClientSecret(clientId); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamMethodSecurityExpressionHandler.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamMethodSecurityExpressionHandler.java index 67de843ad6..b3817b2c06 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamMethodSecurityExpressionHandler.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamMethodSecurityExpressionHandler.java @@ -22,9 +22,10 @@ import org.springframework.stereotype.Component; import it.infn.mw.iam.api.account.AccountUtils; -import it.infn.mw.iam.api.client.service.DefaultClientService; import it.infn.mw.iam.api.requests.GroupRequestUtils; import it.infn.mw.iam.core.userinfo.OAuth2AuthenticationScopeResolver; +import it.infn.mw.iam.persistence.repository.client.IamAccountClientRepository; +import it.infn.mw.iam.persistence.repository.client.IamClientRepository; @SuppressWarnings("deprecation") @Component @@ -33,12 +34,17 @@ public class IamMethodSecurityExpressionHandler extends OAuth2MethodSecurityExpr private final AccountUtils accountUtils; private final GroupRequestUtils groupRequestUtils; private final OAuth2AuthenticationScopeResolver scopeResolver; + private final IamAccountClientRepository accountClientRepo; + private final IamClientRepository clientRepo; public IamMethodSecurityExpressionHandler(AccountUtils accountUtils, - GroupRequestUtils groupRequestUtils, OAuth2AuthenticationScopeResolver scopeResolver) { + GroupRequestUtils groupRequestUtils, OAuth2AuthenticationScopeResolver scopeResolver, + IamAccountClientRepository accountClientRepo, IamClientRepository clientRepo) { this.accountUtils = accountUtils; this.groupRequestUtils = groupRequestUtils; this.scopeResolver = scopeResolver; + this.accountClientRepo = accountClientRepo; + this.clientRepo = clientRepo; } @Override @@ -47,7 +53,7 @@ public StandardEvaluationContext createEvaluationContextInternal(Authentication StandardEvaluationContext ec = super.createEvaluationContextInternal(authentication, mi); ec.setVariable("iam", new IamSecurityExpressionMethods(authentication, accountUtils, - groupRequestUtils, scopeResolver)); + groupRequestUtils, scopeResolver, accountClientRepo, clientRepo)); return ec; } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java index c859d17715..10c0302942 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.Optional; +import org.mitre.oauth2.model.ClientDetailsEntity; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.provider.OAuth2Authentication; @@ -31,6 +32,8 @@ import it.infn.mw.iam.core.userinfo.OAuth2AuthenticationScopeResolver; import it.infn.mw.iam.persistence.model.IamAccount; import it.infn.mw.iam.persistence.model.IamGroupRequest; +import it.infn.mw.iam.persistence.repository.client.IamAccountClientRepository; +import it.infn.mw.iam.persistence.repository.client.IamClientRepository; @SuppressWarnings("deprecation") public class IamSecurityExpressionMethods { @@ -42,13 +45,18 @@ public class IamSecurityExpressionMethods { private final AccountUtils accountUtils; private final GroupRequestUtils groupRequestUtils; private final OAuth2AuthenticationScopeResolver scopeResolver; + private final IamAccountClientRepository accountClientRepo; + private final IamClientRepository clientRepo; public IamSecurityExpressionMethods(Authentication authentication, AccountUtils accountUtils, - GroupRequestUtils groupRequestUtils, OAuth2AuthenticationScopeResolver scopeResolver) { + GroupRequestUtils groupRequestUtils, OAuth2AuthenticationScopeResolver scopeResolver, + IamAccountClientRepository accountClientRepo, IamClientRepository clientRepo) { this.authentication = authentication; this.accountUtils = accountUtils; this.groupRequestUtils = groupRequestUtils; this.scopeResolver = scopeResolver; + this.accountClientRepo = accountClientRepo; + this.clientRepo = clientRepo; } public boolean isExternallyAuthenticatedWithIssuer(String issuer) { @@ -153,4 +161,16 @@ public boolean hasDashboardRole(Role role) { public boolean hasAdminOrGMDashboardRoleOfGroup(String gid) { return (hasDashboardRole(Role.ROLE_ADMIN) || isGroupManager(gid)); } + + public boolean isClientOwner(String clientId) { + Optional owner = accountUtils.getAuthenticatedUserAccount(); + if (!owner.isPresent()) { + return false; + } + Optional client = clientRepo.findByClientId(clientId); + if (!client.isPresent()) { + return false; + } + return accountClientRepo.findByAccountAndClient(owner.get(), client.get()).isPresent(); + } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamWebSecurityExpressionHandler.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamWebSecurityExpressionHandler.java index 754aa3edc4..fd42c29e04 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamWebSecurityExpressionHandler.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamWebSecurityExpressionHandler.java @@ -22,9 +22,10 @@ import org.springframework.stereotype.Component; import it.infn.mw.iam.api.account.AccountUtils; -import it.infn.mw.iam.api.client.service.DefaultClientService; import it.infn.mw.iam.api.requests.GroupRequestUtils; import it.infn.mw.iam.core.userinfo.OAuth2AuthenticationScopeResolver; +import it.infn.mw.iam.persistence.repository.client.IamAccountClientRepository; +import it.infn.mw.iam.persistence.repository.client.IamClientRepository; @SuppressWarnings("deprecation") @Component @@ -33,12 +34,17 @@ public class IamWebSecurityExpressionHandler extends OAuth2WebSecurityExpression private final AccountUtils accountUtils; private final GroupRequestUtils groupRequestUtils; private final OAuth2AuthenticationScopeResolver scopeResolver; + private final IamAccountClientRepository accountClientRepo; + private final IamClientRepository clientRepo; public IamWebSecurityExpressionHandler(AccountUtils accountUtils, - GroupRequestUtils groupRequestUtils, OAuth2AuthenticationScopeResolver scopeResolver) { + GroupRequestUtils groupRequestUtils, OAuth2AuthenticationScopeResolver scopeResolver, + IamAccountClientRepository accountClientRepo, IamClientRepository clientRepo) { this.accountUtils = accountUtils; this.groupRequestUtils = groupRequestUtils; this.scopeResolver = scopeResolver; + this.accountClientRepo = accountClientRepo; + this.clientRepo = clientRepo; } @Override @@ -48,7 +54,7 @@ public StandardEvaluationContext createEvaluationContextInternal(Authentication StandardEvaluationContext ec = super.createEvaluationContextInternal(authentication, invocation); ec.setVariable("iam", new IamSecurityExpressionMethods(authentication, accountUtils, - groupRequestUtils, scopeResolver)); + groupRequestUtils, scopeResolver, accountClientRepo, clientRepo)); return ec; } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/util/IamClientSecretEncoder.java b/iam-login-service/src/main/java/it/infn/mw/iam/util/IamClientSecretEncoder.java index 5dd2566cb9..64cb166073 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/util/IamClientSecretEncoder.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/util/IamClientSecretEncoder.java @@ -24,7 +24,7 @@ */ public class IamClientSecretEncoder implements PasswordEncoder { - final short DEFAULT_ROUND = 12; + final int DEFAULT_ROUND = 12; BCryptPasswordEncoder bcryptEncoder = new BCryptPasswordEncoder(DEFAULT_ROUND); diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/scope/matchers/ScopeMatcherNoCacheTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/scope/matchers/ScopeMatcherNoCacheTests.java index 8a3a5506a3..453c27807b 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/scope/matchers/ScopeMatcherNoCacheTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/scope/matchers/ScopeMatcherNoCacheTests.java @@ -46,7 +46,7 @@ public class ScopeMatcherNoCacheTests extends EndpointsTestUtils { private static final String CLIENT_ID = "cache-client"; private static final String CLIENT_SECRET = "secret"; - private static final String HASH_CLIENT_SECRET = "$2a$12$WWEtffWdIellMxblYDNEx..nVahwP9ZMuRYyVdYFb.7DECPRaOp1K"; + private static final String CLIENT_SECRET_HASH = "$2a$12$WWEtffWdIellMxblYDNEx..nVahwP9ZMuRYyVdYFb.7DECPRaOp1K"; @Autowired private IamClientRepository clientRepo; @@ -75,7 +75,7 @@ public void updatingClientScopesWithNoCache() throws Exception { ClientDetailsEntity client = new ClientDetailsEntity(); client.setClientId(CLIENT_ID); - client.setClientSecret(HASH_CLIENT_SECRET); + client.setClientSecret(CLIENT_SECRET_HASH); client.setScope(Sets.newHashSet("openid", "profile", "email")); clientRepo.save(client); diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java index 3f4fa1fa81..8bfc487cae 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java @@ -36,6 +36,8 @@ import it.infn.mw.iam.core.expression.IamSecurityExpressionMethods; import it.infn.mw.iam.core.userinfo.OAuth2AuthenticationScopeResolver; import it.infn.mw.iam.persistence.repository.IamGroupRequestRepository; +import it.infn.mw.iam.persistence.repository.client.IamAccountClientRepository; +import it.infn.mw.iam.persistence.repository.client.IamClientRepository; import it.infn.mw.iam.test.api.requests.GroupRequestsTestUtils; @RunWith(SpringRunner.class) @@ -54,6 +56,11 @@ public class IamSecurityExpressionsTests extends GroupRequestsTestUtils { @Autowired private IamGroupRequestRepository repo; + @Autowired + private IamAccountClientRepository accountClientRepo; + + @Autowired + private IamClientRepository clientRepo; @After public void destroy() { @@ -63,7 +70,7 @@ public void destroy() { private IamSecurityExpressionMethods getMethods() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return new IamSecurityExpressionMethods(authentication, accountUtils, groupRequestUtils, - scopeResolver); + scopeResolver, accountClientRepo, clientRepo); } @Test diff --git a/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java b/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java index 78235e5653..0bc585211c 100644 --- a/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java +++ b/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java @@ -32,7 +32,7 @@ public class V109__HashClientSecret extends BaseFlywayJavaMigrationAdapter { @Override public void migrate(JdbcTemplate jdbcTemplate) throws DataAccessException { - final short DEFAULT_ROUND = 12; + final int DEFAULT_ROUND = 12; PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(DEFAULT_ROUND); From b38111deeb465004cd3907d46c231024cedcd756 Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Thu, 12 Jun 2025 12:56:39 +0200 Subject: [PATCH 24/37] Add tests --- .../IamSecurityExpressionMethods.java | 5 +- .../ClientManagementAPIIntegrationTests.java | 111 +++++++++++++++--- 2 files changed, 96 insertions(+), 20 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java index 10c0302942..a34e576e11 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java @@ -164,11 +164,8 @@ public boolean hasAdminOrGMDashboardRoleOfGroup(String gid) { public boolean isClientOwner(String clientId) { Optional owner = accountUtils.getAuthenticatedUserAccount(); - if (!owner.isPresent()) { - return false; - } Optional client = clientRepo.findByClientId(clientId); - if (!client.isPresent()) { + if (!owner.isPresent() || !client.isPresent()) { return false; } return accountClientRepo.findByAccountAndClient(owner.get(), client.get()).isPresent(); diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java index 0f791d6e0e..bc1f5ea9a1 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java @@ -21,6 +21,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -34,6 +35,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mitre.oauth2.model.ClientDetailsEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.test.context.support.WithAnonymousUser; @@ -45,6 +47,8 @@ import it.infn.mw.iam.IamLoginService; import it.infn.mw.iam.api.client.management.ClientManagementAPIController; +import it.infn.mw.iam.api.client.registration.ClientRegistrationApiController; +import it.infn.mw.iam.api.client.util.ClientSuppliers; import it.infn.mw.iam.api.common.client.RegisteredClientDTO; import it.infn.mw.iam.persistence.repository.client.IamClientRepository; import it.infn.mw.iam.test.api.TestSupport; @@ -53,7 +57,6 @@ import it.infn.mw.iam.test.util.WithMockOAuthUser; import it.infn.mw.iam.test.util.annotation.IamMockMvcIntegrationTest; import it.infn.mw.iam.test.util.oauth.MockOAuth2Filter; -import org.mitre.oauth2.model.ClientDetailsEntity; @IamMockMvcIntegrationTest @SpringBootTest(classes = {IamLoginService.class, CoreControllerTestSupport.class}) @@ -211,10 +214,13 @@ void setTokenLifetimesWorks() throws Exception { assertEquals(600, client.getDeviceCodeValiditySeconds()); Optional clientDB = clientRepo.findByClientId(client.getClientId()); - assertEquals(client.getAccessTokenValiditySeconds(), clientDB.get().getAccessTokenValiditySeconds()); - assertEquals(client.getRefreshTokenValiditySeconds(), clientDB.get().getRefreshTokenValiditySeconds()); + assertEquals(client.getAccessTokenValiditySeconds(), + clientDB.get().getAccessTokenValiditySeconds()); + assertEquals(client.getRefreshTokenValiditySeconds(), + clientDB.get().getRefreshTokenValiditySeconds()); assertEquals(client.getIdTokenValiditySeconds(), clientDB.get().getIdTokenValiditySeconds()); - assertEquals(client.getDeviceCodeValiditySeconds(), clientDB.get().getDeviceCodeValiditySeconds()); + assertEquals(client.getDeviceCodeValiditySeconds(), + clientDB.get().getDeviceCodeValiditySeconds()); clientJson = ClientJsonStringBuilder.builder() .scopes("openid") @@ -235,10 +241,13 @@ void setTokenLifetimesWorks() throws Exception { assertEquals(0, client.getRefreshTokenValiditySeconds()); clientDB = clientRepo.findByClientId(client.getClientId()); - assertEquals(client.getAccessTokenValiditySeconds(), clientDB.get().getAccessTokenValiditySeconds()); - assertEquals(client.getRefreshTokenValiditySeconds(), clientDB.get().getRefreshTokenValiditySeconds()); + assertEquals(client.getAccessTokenValiditySeconds(), + clientDB.get().getAccessTokenValiditySeconds()); + assertEquals(client.getRefreshTokenValiditySeconds(), + clientDB.get().getRefreshTokenValiditySeconds()); assertEquals(client.getIdTokenValiditySeconds(), clientDB.get().getIdTokenValiditySeconds()); - assertEquals(client.getDeviceCodeValiditySeconds(), clientDB.get().getDeviceCodeValiditySeconds()); + assertEquals(client.getDeviceCodeValiditySeconds(), + clientDB.get().getDeviceCodeValiditySeconds()); clientJson = ClientJsonStringBuilder.builder() .scopes("openid") @@ -259,10 +268,13 @@ void setTokenLifetimesWorks() throws Exception { assertEquals(10, client.getRefreshTokenValiditySeconds()); clientDB = clientRepo.findByClientId(client.getClientId()); - assertEquals(client.getAccessTokenValiditySeconds(), clientDB.get().getAccessTokenValiditySeconds()); - assertEquals(client.getRefreshTokenValiditySeconds(), clientDB.get().getRefreshTokenValiditySeconds()); + assertEquals(client.getAccessTokenValiditySeconds(), + clientDB.get().getAccessTokenValiditySeconds()); + assertEquals(client.getRefreshTokenValiditySeconds(), + clientDB.get().getRefreshTokenValiditySeconds()); assertEquals(client.getIdTokenValiditySeconds(), clientDB.get().getIdTokenValiditySeconds()); - assertEquals(client.getDeviceCodeValiditySeconds(), clientDB.get().getDeviceCodeValiditySeconds()); + assertEquals(client.getDeviceCodeValiditySeconds(), + clientDB.get().getDeviceCodeValiditySeconds()); } @@ -298,12 +310,12 @@ void negativeRefreshTokenLifetimesSetToInfinite() throws Exception { @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) void setClientEnableWorks() throws Exception { - mvc.perform(get(ClientManagementAPIController.ENDPOINT + "/client")) + mvc.perform(get(ClientManagementAPIController.ENDPOINT + "/client")) .andExpect(OK) .andExpect(jsonPath("$.active").value(true)); - mvc.perform(patch(ClientManagementAPIController.ENDPOINT + "/{clientId}/enable", "client") - ).andExpect(OK); + mvc.perform(patch(ClientManagementAPIController.ENDPOINT + "/{clientId}/enable", "client")) + .andExpect(OK); mvc.perform(get(ClientManagementAPIController.ENDPOINT + "/client")) .andExpect(OK) @@ -314,15 +326,82 @@ void setClientEnableWorks() throws Exception { @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) void setClientDisableWorks() throws Exception { - mvc.perform(get(ClientManagementAPIController.ENDPOINT + "/client")) + mvc.perform(get(ClientManagementAPIController.ENDPOINT + "/client")) .andExpect(OK) .andExpect(jsonPath("$.active").value(true)); - mvc.perform(patch(ClientManagementAPIController.ENDPOINT + "/{clientId}/disable", "client") - ).andExpect(OK); + mvc.perform(patch(ClientManagementAPIController.ENDPOINT + "/{clientId}/disable", "client")) + .andExpect(OK); mvc.perform(get(ClientManagementAPIController.ENDPOINT + "/client")) .andExpect(OK) .andExpect(jsonPath("$.active").value(false)); } + + @Test + @WithMockUser(username = "test", roles = {"USER"}) + void secretRotationWorksForClientOwner() throws Exception { + + String clientJson = ClientJsonStringBuilder.builder().scopes("openid").build(); + + String responseJson = mvc + .perform(post(ClientRegistrationApiController.ENDPOINT).contentType(APPLICATION_JSON) + .content(clientJson)) + .andExpect(CREATED) + .andReturn() + .getResponse() + .getContentAsString(); + + RegisteredClientDTO client = mapper.readValue(responseJson, RegisteredClientDTO.class); + String clientSecret = client.getClientSecret(); + + final String url = + String.format("%s/%s/secret", ClientManagementAPIController.ENDPOINT, client.getClientId()); + + responseJson = mvc.perform(post(url)).andReturn().getResponse().getContentAsString(); + client = mapper.readValue(responseJson, RegisteredClientDTO.class); + String clientSecretRenewed = client.getClientSecret(); + assertFalse(clientSecret.equals(clientSecretRenewed)); + } + + @Test + @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) + void secretRotationWorksForAdminUser() throws Exception { + + String clientJson = ClientJsonStringBuilder.builder().scopes("openid").build(); + + String responseJson = mvc + .perform(post(ClientManagementAPIController.ENDPOINT).contentType(APPLICATION_JSON) + .content(clientJson)) + .andExpect(CREATED) + .andReturn() + .getResponse() + .getContentAsString(); + + RegisteredClientDTO client = mapper.readValue(responseJson, RegisteredClientDTO.class); + String clientSecret = client.getClientSecret(); + + final String url = + String.format("%s/%s/secret", ClientManagementAPIController.ENDPOINT, client.getClientId()); + + responseJson = mvc.perform(post(url)).andReturn().getResponse().getContentAsString(); + client = mapper.readValue(responseJson, RegisteredClientDTO.class); + String clientSecretRenewed = client.getClientSecret(); + assertFalse(clientSecret.equals(clientSecretRenewed)); + } + + @Test + @WithMockUser(username = "test", roles = {"USER"}) + void secretRotationFailsForOtherUsers() throws Exception { + + ClientDetailsEntity client = + clientRepo.findByClientId("client").orElseThrow(ClientSuppliers.clientNotFound("client")); + + final String url = + String.format("%s/%s/secret", ClientManagementAPIController.ENDPOINT, client.getClientId()); + + mvc.perform(post(url)) + .andExpect(FORBIDDEN) + .andExpect(jsonPath("$.error", containsString("access_denied"))); + } } From 95e15710b982e7fc7d723744d9ed374a4b7afb5c Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Thu, 12 Jun 2025 14:40:51 +0200 Subject: [PATCH 25/37] Fix sonar issues and increase coverage --- .../mw/iam/util/IamClientSecretEncoder.java | 2 +- .../ClientManagementAPIIntegrationTests.java | 21 ++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/util/IamClientSecretEncoder.java b/iam-login-service/src/main/java/it/infn/mw/iam/util/IamClientSecretEncoder.java index 64cb166073..95e52ed10b 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/util/IamClientSecretEncoder.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/util/IamClientSecretEncoder.java @@ -24,7 +24,7 @@ */ public class IamClientSecretEncoder implements PasswordEncoder { - final int DEFAULT_ROUND = 12; + static final int DEFAULT_ROUND = 12; BCryptPasswordEncoder bcryptEncoder = new BCryptPasswordEncoder(DEFAULT_ROUND); diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java index bc1f5ea9a1..1ff257154e 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java @@ -21,7 +21,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -361,7 +361,7 @@ void secretRotationWorksForClientOwner() throws Exception { responseJson = mvc.perform(post(url)).andReturn().getResponse().getContentAsString(); client = mapper.readValue(responseJson, RegisteredClientDTO.class); String clientSecretRenewed = client.getClientSecret(); - assertFalse(clientSecret.equals(clientSecretRenewed)); + assertNotEquals(clientSecret, clientSecretRenewed); } @Test @@ -387,7 +387,7 @@ void secretRotationWorksForAdminUser() throws Exception { responseJson = mvc.perform(post(url)).andReturn().getResponse().getContentAsString(); client = mapper.readValue(responseJson, RegisteredClientDTO.class); String clientSecretRenewed = client.getClientSecret(); - assertFalse(clientSecret.equals(clientSecretRenewed)); + assertNotEquals(clientSecret, clientSecretRenewed); } @Test @@ -404,4 +404,19 @@ void secretRotationFailsForOtherUsers() throws Exception { .andExpect(FORBIDDEN) .andExpect(jsonPath("$.error", containsString("access_denied"))); } + + @Test + @WithMockUser(username = "non-existent-user", roles = {"USER"}) + void secretRotationFailsWhenUserNotFound() throws Exception { + + ClientDetailsEntity client = + clientRepo.findByClientId("client").orElseThrow(ClientSuppliers.clientNotFound("client")); + + final String url = + String.format("%s/%s/secret", ClientManagementAPIController.ENDPOINT, client.getClientId()); + + mvc.perform(post(url)) + .andExpect(FORBIDDEN) + .andExpect(jsonPath("$.error", containsString("access_denied"))); + } } From 6ef29bb496c99d1e370797ad3616c244a358e75e Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Thu, 12 Jun 2025 15:09:08 +0200 Subject: [PATCH 26/37] Fix sonar issue about test duplication --- .../ClientManagementAPIIntegrationTests.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java index 1ff257154e..e335ef2a41 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java @@ -390,12 +390,9 @@ void secretRotationWorksForAdminUser() throws Exception { assertNotEquals(clientSecret, clientSecretRenewed); } - @Test - @WithMockUser(username = "test", roles = {"USER"}) - void secretRotationFailsForOtherUsers() throws Exception { - + private void assertSecretRotationForbidden(String clientId) throws Exception { ClientDetailsEntity client = - clientRepo.findByClientId("client").orElseThrow(ClientSuppliers.clientNotFound("client")); + clientRepo.findByClientId(clientId).orElseThrow(ClientSuppliers.clientNotFound(clientId)); final String url = String.format("%s/%s/secret", ClientManagementAPIController.ENDPOINT, client.getClientId()); @@ -406,17 +403,16 @@ void secretRotationFailsForOtherUsers() throws Exception { } @Test - @WithMockUser(username = "non-existent-user", roles = {"USER"}) - void secretRotationFailsWhenUserNotFound() throws Exception { + @WithMockUser(username = "test", roles = {"USER"}) + void secretRotationFailsForOtherUsers() throws Exception { - ClientDetailsEntity client = - clientRepo.findByClientId("client").orElseThrow(ClientSuppliers.clientNotFound("client")); + assertSecretRotationForbidden("client"); + } - final String url = - String.format("%s/%s/secret", ClientManagementAPIController.ENDPOINT, client.getClientId()); + @Test + @WithMockUser(username = "non-existent-user", roles = {"USER"}) + void secretRotationFailsWhenUserNotFound() throws Exception { - mvc.perform(post(url)) - .andExpect(FORBIDDEN) - .andExpect(jsonPath("$.error", containsString("access_denied"))); + assertSecretRotationForbidden("client-cred"); } } From 434c2745ad6fecbebb8313bef557587886477a84 Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Wed, 18 Jun 2025 12:53:20 +0200 Subject: [PATCH 27/37] Remove useless code from migration V109 --- .../java/db/migration/mysql/V109__HashClientSecret.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java b/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java index 0bc585211c..ff35ac6ecd 100644 --- a/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java +++ b/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java @@ -46,13 +46,9 @@ public void migrate(JdbcTemplate jdbcTemplate) throws DataAccessException { if (clientSecret == null) { continue; } + Long id = clientList.getLong("id"); - String secretHash = passwordEncoder.encode(clientSecret); - if (passwordEncoder.matches(clientSecret, secretHash)) { - LOG.info("Client secret already hashed"); - continue; - } jdbcTemplate.update("UPDATE client_details SET client_secret=? WHERE id=?", secretHash, id); } From 86274998b3d5f63c8e22a036f2ca9bd924a71910 Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Fri, 20 Jun 2025 18:11:37 +0200 Subject: [PATCH 28/37] Fix save new public client --- .../clients/client/client.component.js | 7 +++ .../myclients/myclient/myclient.component.js | 63 ++++++++++--------- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.js index 65d2213399..11d814a398 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.js +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.js @@ -136,6 +136,13 @@ body: 'Client saved!' }); + const isNewClientPublic = self.clientVal.token_endpoint_auth_method === 'none'; + + if (isNewClientPublic) { + $location.path('/clients'); + return; + } + var modalSecret = $uibModal.open({ templateUrl: '/resources/iam/apps/dashboard-app/components/clients/client/newclientsecretshow/newclientsecretshow.component.html', controller: ClientSecretViewController, diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/user/myclients/myclient/myclient.component.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/user/myclients/myclient/myclient.component.js index 9bd15e7365..a4629a37d6 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/user/myclients/myclient/myclient.component.js +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/user/myclients/myclient/myclient.component.js @@ -28,16 +28,16 @@ self.clipboardSuccess = clipboardSuccess; self.clipboardError = clipboardError; - - $ctrl.ok = function() { + + $ctrl.ok = function () { $uibModalInstance.close($ctrl.selected); }; - - $ctrl.closeModal = function() { + + $ctrl.closeModal = function () { $uibModalInstance.dismiss('cancel'); }; - $ctrl.toggleSecretVisibility = function() { + $ctrl.toggleSecretVisibility = function () { $ctrl.showSecret = !$ctrl.showSecret; }; @@ -101,23 +101,23 @@ var res = () => { var modalInstance = $uibModal.open({ - templateUrl: 'clientsecret-view.content.html', - controller: 'ModalClientSecretViewController', - resolve: { - data: function() { - return { - title: '', - message: '', - clientDetail: res, - }; + templateUrl: 'clientsecret-view.content.html', + controller: 'ModalClientSecretViewController', + resolve: { + data: function () { + return { + title: '', + message: '', + clientDetail: res, + }; + } } - } }); - - modalInstance.result.then(function(selectedItem) { - $ctrl.selected = selectedItem; - }, function() { - console.log('Dialog dismissed at: ' + new Date()); + + modalInstance.result.then(function (selectedItem) { + $ctrl.selected = selectedItem; + }, function () { + console.log('Dialog dismissed at: ' + new Date()); }); return res; } @@ -139,7 +139,13 @@ function registerClient() { return ClientRegistrationService.registerClient(self.clientVal).then(res => { - handleSuccess(res); + handleSuccess(res); + const isNewClientPublic = self.clientVal.token_endpoint_auth_method === 'none'; + + if (isNewClientPublic) { + $location.path('/home/clients'); + return res; + } var modalSecret = $uibModal.open({ templateUrl: '/resources/iam/apps/dashboard-app/components/clients/client/newclientsecretshow/newclientsecretshow.component.html', @@ -154,17 +160,16 @@ } } }); - modalSecret.result - .then(() => {$location.path('/home/clients');}) - .catch(() => { - toaster.pop({ - type: 'error', - body: errorMsg + .then(() => { $location.path('/home/clients'); }) + .catch(() => { + toaster.pop({ + type: 'error', + body: errorMsg + }); }); - }); - return res; + return res; }).catch(handleError); } From 11982790f2909809f3a1ec0f991cb86fda1c4073 Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Thu, 7 Aug 2025 17:20:20 +0200 Subject: [PATCH 29/37] Add new tests for security method IsClientOwner --- .../IamSecurityExpressionMethodsTests.java} | 97 ++++++++++++++++++- 1 file changed, 93 insertions(+), 4 deletions(-) rename iam-login-service/src/test/java/it/infn/mw/iam/test/{util/IamSecurityExpressionsTests.java => core/IamSecurityExpressionMethodsTests.java} (54%) diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java similarity index 54% rename from iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java rename to iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java index 8bfc487cae..c30e117977 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/util/IamSecurityExpressionsTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java @@ -13,39 +13,59 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package it.infn.mw.iam.test.util; +package it.infn.mw.iam.test.core; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Date; +import java.util.Optional; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit4.SpringRunner; +import org.testcontainers.shaded.com.google.common.collect.Sets; import it.infn.mw.iam.IamLoginService; import it.infn.mw.iam.api.account.AccountUtils; +import it.infn.mw.iam.api.client.service.ClientService; import it.infn.mw.iam.api.requests.GroupRequestUtils; import it.infn.mw.iam.api.requests.model.GroupRequestDto; +import it.infn.mw.iam.authn.util.Authorities; import it.infn.mw.iam.core.expression.IamSecurityExpressionMethods; import it.infn.mw.iam.core.userinfo.OAuth2AuthenticationScopeResolver; +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamAccountClient; +import it.infn.mw.iam.persistence.repository.IamAccountRepository; import it.infn.mw.iam.persistence.repository.IamGroupRequestRepository; import it.infn.mw.iam.persistence.repository.client.IamAccountClientRepository; import it.infn.mw.iam.persistence.repository.client.IamClientRepository; import it.infn.mw.iam.test.api.requests.GroupRequestsTestUtils; +import it.infn.mw.iam.test.util.oauth.MockOAuth2Request; +@SuppressWarnings("deprecation") @RunWith(SpringRunner.class) @SpringBootTest(classes = {IamLoginService.class}, webEnvironment = WebEnvironment.MOCK) -public class IamSecurityExpressionsTests extends GroupRequestsTestUtils { +public class IamSecurityExpressionMethodsTests extends GroupRequestsTestUtils { - @Autowired - private AccountUtils accountUtils; + public static final String TEST_CLIENT_ID = "client"; @Autowired private GroupRequestUtils groupRequestUtils; @@ -60,8 +80,26 @@ public class IamSecurityExpressionsTests extends GroupRequestsTestUtils { private IamAccountClientRepository accountClientRepo; @Autowired + private IamAccountRepository accountRepo; + + @Autowired + private ClientService clientService; + + @Autowired + private ClientDetailsEntityService clientDetailsService; + + @Mock private IamClientRepository clientRepo; + @Mock + OAuth2Authentication authentication; + + @Mock + AccountUtils accountUtils; + + @Spy + MockOAuth2Request oauth2Request = new MockOAuth2Request("clientId", new String[] {"openid", "profile"}); + @After public void destroy() { repo.deleteAll(); @@ -104,4 +142,55 @@ public void testIsNotAdmin() { assertFalse(getMethods().canManageGroupRequest(notMine.getUuid())); assertFalse(getMethods().userCanDeleteGroupRequest(notMine.getUuid())); } + + @Test + public void testIsClientOwnerNoAuthenticatedUser() { + when(accountUtils.getAuthenticatedUserAccount(Mockito.any())).thenReturn(null); + + assertFalse(getMethods().isClientOwner("client")); + } + + @Test + public void testIsClientOwnerIsAdmin() { + String owner = "admin"; + clientService.linkClientToAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID), + accountRepo.findByUsername(owner).get()); + + when(accountUtils.getAuthenticatedUserAccount(Mockito.any())).thenReturn(Optional.empty()); + IamAccount adminAccount = accountRepo.findByUsername(owner).orElseThrow(); + doReturn(Optional.of(adminAccount)).when(accountUtils).getAuthenticatedUserAccount(); + ClientDetailsEntity clientTest = clientDetailsService.loadClientByClientId(TEST_CLIENT_ID); + doReturn(Optional.of(clientTest)).when(clientRepo).findByClientId(TEST_CLIENT_ID); + + assertTrue(getMethods().isClientOwner(TEST_CLIENT_ID)); + } + + @Test + public void testIsClientOwnerIsUser() { + String owner = "test_200"; + clientService.linkClientToAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID), + accountRepo.findByUsername(owner).get()); + + IamAccount userAccount = accountRepo.findByUsername(owner).orElseThrow(); + doReturn(Optional.of(userAccount)).when(accountUtils).getAuthenticatedUserAccount(); + ClientDetailsEntity clientTest = clientDetailsService.loadClientByClientId(TEST_CLIENT_ID); + doReturn(Optional.of(clientTest)).when(clientRepo).findByClientId(TEST_CLIENT_ID); + + assertTrue(getMethods().isClientOwner(TEST_CLIENT_ID)); + } + + @Test + public void testIsClientOwnerIsNotUser() { + String owner = "test_200"; + String notOwner = "admin"; + clientService.linkClientToAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID), + accountRepo.findByUsername(owner).get()); + + IamAccount userAccount = accountRepo.findByUsername(notOwner).orElseThrow(); + doReturn(Optional.of(userAccount)).when(accountUtils).getAuthenticatedUserAccount(); + ClientDetailsEntity clientTest = clientDetailsService.loadClientByClientId(TEST_CLIENT_ID); + doReturn(Optional.of(clientTest)).when(clientRepo).findByClientId(TEST_CLIENT_ID); + + assertFalse(getMethods().isClientOwner(TEST_CLIENT_ID)); + } } From 376d84cd62a5c259704b44acf0c06e7527284e18 Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Thu, 7 Aug 2025 17:34:51 +0200 Subject: [PATCH 30/37] Rename migration V109 to V110 --- .../voms-deploy/assets/db/iam-test-dump.sql | 2 +- .../assets/db-primary/iam-test-dump.sql | 2 +- ...ecret.java => V110__HashClientSecret.java} | 8 +-- .../test/V100000_9__hash_client_test.sql | 2 - .../db/migration/test/V100000___test_data.sql | 50 +++++++++---------- 5 files changed, 29 insertions(+), 35 deletions(-) rename iam-persistence/src/main/java/db/migration/mysql/{V109__HashClientSecret.java => V110__HashClientSecret.java} (87%) delete mode 100644 iam-persistence/src/main/resources/db/migration/test/V100000_9__hash_client_test.sql diff --git a/compose/voms-deploy/assets/db/iam-test-dump.sql b/compose/voms-deploy/assets/db/iam-test-dump.sql index 134ca67138..597b43863b 100644 --- a/compose/voms-deploy/assets/db/iam-test-dump.sql +++ b/compose/voms-deploy/assets/db/iam-test-dump.sql @@ -656,7 +656,7 @@ CREATE TABLE `client_details` ( LOCK TABLES `client_details` WRITE; /*!40000 ALTER TABLE `client_details` DISABLE KEYS */; -INSERT INTO `client_details` VALUES (1,NULL,1,0,1,600,'client','secret',3600,NULL,NULL,'Test Client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(2,NULL,1,0,1,0,'tasks-app','secret',0,NULL,NULL,'Tasks App','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(3,NULL,1,0,1,600,'post-client','secret',3600,NULL,NULL,'Post client','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(4,NULL,1,0,1,600,'client-cred','secret',3600,NULL,NULL,'Client credentials','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(5,NULL,1,0,1,600,'password-grant','secret',3600,NULL,NULL,'Password grant client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(6,NULL,1,0,1,600,'scim-client-ro','secret',3600,NULL,NULL,'SCIM client (read-only)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(7,NULL,1,0,1,600,'scim-client-rw','secret',3600,NULL,NULL,'SCIM client (read-write)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(8,NULL,1,0,1,600,'token-exchange-actor','secret',3600,NULL,NULL,'Token Exchange grant client actor','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(9,NULL,1,0,1,600,'token-exchange-subject','secret',3600,NULL,NULL,'Token Exchange grant client subject','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(10,NULL,1,0,1,600,'registration-client','secret',3600,NULL,NULL,'Registration service test client','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(11,NULL,1,0,1,600,'token-lookup-client','secret',3600,NULL,NULL,'Token lookup client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(12,NULL,1,0,1,600,'device-code-client','secret',3600,NULL,NULL,'Device code client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,600),(13,NULL,1,0,0,600,'implicit-flow-client',NULL,3600,NULL,NULL,'Implicit Flow client',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,600),(14,NULL,1,0,0,600,'public-dc-client',NULL,3600,NULL,NULL,'Public Device Code client',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,600),(15,NULL,1,0,1,600,'jwt-auth-client_secret_jwt','c8e9eed0-e6e4-4a66-b16e-6f37096356a7',3600,NULL,NULL,'JWT Bearer Auth Client (client_secret_jwt)','SECRET_JWT',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'HS256',NULL,0,NULL,NULL,1,NULL,NULL,NULL,NULL,NULL),(16,NULL,1,0,1,600,'jwt-auth-private_key_jwt','secret',3600,NULL,NULL,'JWT Bearer Auth Client (private_key_jwt)','PRIVATE_KEY',NULL,NULL,NULL,NULL,NULL,NULL,'{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"kid\":\"rsa1\",\"n\":\"1y1CP181zqPNPlV1JDM7Xv0QnGswhSTHe8_XPZHxDTJkykpk_1BmgA3ovP62QRE2ORgsv5oSBI_Z_RaOc4Zx2FonjEJF2oBHtBjsAiF-pxGkM5ZPjFNgFTGp1yUUBjFDcEeIGCwPEyYSt93sQIP_0DRbViMUnpyn3xgM_a1dO5brEWR2n1Uqff1yA5NXfLS03qpl2dpH4HFY5-Zs4bvtJykpAOhoHuIQbz-hmxb9MZ3uTAwsx2HiyEJtz-suyTBHO3BM2o8UcCeyfa34ShPB8i86-sf78fOk2KeRIW1Bju3ANmdV3sxL0j29cesxKCZ06u2ZiGR3Srbft8EdLPzf-w\"}]}',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'RS256',NULL,0,NULL,NULL,1,NULL,NULL,NULL,NULL,NULL),(17,NULL,1,0,1,600,'admin-client-ro','secret',3600,NULL,NULL,'Admin client (read-only)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(18,NULL,1,0,1,600,'admin-client-rw','secret',3600,NULL,NULL,'Admin client (read-write)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL); +INSERT INTO `client_details` VALUES (1,NULL,1,0,1,600,'client','secret',3600,NULL,NULL,'Test Client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(2,NULL,1,0,1,0,'tasks-app','secret',0,NULL,NULL,'Tasks App','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(3,NULL,1,0,1,600,'post-client','secret',3600,NULL,NULL,'Post client','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(4,NULL,1,0,1,600,'client-cred','secret',3600,NULL,NULL,'Client credentials','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(5,NULL,1,0,1,600,'password-grant','secret',3600,NULL,NULL,'Password grant client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(6,NULL,1,0,1,600,'scim-client-ro','secret',3600,NULL,NULL,'SCIM client (read-only)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(7,NULL,1,0,1,600,'scim-client-rw','secret',3600,NULL,NULL,'SCIM client (read-write)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(8,NULL,1,0,1,600,'token-exchange-actor','secret',3600,NULL,NULL,'Token Exchange grant client actor','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(9,NULL,1,0,1,600,'token-exchange-subject','secret',3600,NULL,NULL,'Token Exchange grant client subject','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(10,NULL,1,0,1,600,'registration-client','secret',3600,NULL,NULL,'Registration service test client','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(11,NULL,1,0,1,600,'token-lookup-client','secret',3600,NULL,NULL,'Token lookup client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(12,NULL,1,0,1,600,'device-code-client','secret',3600,NULL,NULL,'Device code client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,600),(13,NULL,1,0,0,600,'implicit-flow-client',NULL,3600,NULL,NULL,'Implicit Flow client',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,600),(14,NULL,1,0,0,600,'public-dc-client',NULL,3600,NULL,NULL,'Public Device Code client',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,600),(15,NULL,1,0,1,600,'jwt-auth-client_secret_jwt','secret',3600,NULL,NULL,'JWT Bearer Auth Client (client_secret_jwt)','SECRET_JWT',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'HS256',NULL,0,NULL,NULL,1,NULL,NULL,NULL,NULL,NULL),(16,NULL,1,0,1,600,'jwt-auth-private_key_jwt','secret',3600,NULL,NULL,'JWT Bearer Auth Client (private_key_jwt)','PRIVATE_KEY',NULL,NULL,NULL,NULL,NULL,NULL,'{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"kid\":\"rsa1\",\"n\":\"1y1CP181zqPNPlV1JDM7Xv0QnGswhSTHe8_XPZHxDTJkykpk_1BmgA3ovP62QRE2ORgsv5oSBI_Z_RaOc4Zx2FonjEJF2oBHtBjsAiF-pxGkM5ZPjFNgFTGp1yUUBjFDcEeIGCwPEyYSt93sQIP_0DRbViMUnpyn3xgM_a1dO5brEWR2n1Uqff1yA5NXfLS03qpl2dpH4HFY5-Zs4bvtJykpAOhoHuIQbz-hmxb9MZ3uTAwsx2HiyEJtz-suyTBHO3BM2o8UcCeyfa34ShPB8i86-sf78fOk2KeRIW1Bju3ANmdV3sxL0j29cesxKCZ06u2ZiGR3Srbft8EdLPzf-w\"}]}',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'RS256',NULL,0,NULL,NULL,1,NULL,NULL,NULL,NULL,NULL),(17,NULL,1,0,1,600,'admin-client-ro','secret',3600,NULL,NULL,'Admin client (read-only)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL),(18,NULL,1,0,1,600,'admin-client-rw','secret',3600,NULL,NULL,'Admin client (read-write)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-01 10:57:22',NULL,1,NULL,NULL,NULL,NULL,NULL); /*!40000 ALTER TABLE `client_details` ENABLE KEYS */; UNLOCK TABLES; diff --git a/compose/voms-replica/assets/db-primary/iam-test-dump.sql b/compose/voms-replica/assets/db-primary/iam-test-dump.sql index f4bc1e825f..88527b266d 100644 --- a/compose/voms-replica/assets/db-primary/iam-test-dump.sql +++ b/compose/voms-replica/assets/db-primary/iam-test-dump.sql @@ -656,7 +656,7 @@ CREATE TABLE `client_details` ( LOCK TABLES `client_details` WRITE; /*!40000 ALTER TABLE `client_details` DISABLE KEYS */; -INSERT INTO `client_details` VALUES (1,NULL,1,0,1,600,'client','secret',3600,NULL,NULL,'Test Client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(2,NULL,1,0,1,0,'tasks-app','secret',0,NULL,NULL,'Tasks App','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(3,NULL,1,0,1,600,'post-client','secret',3600,NULL,NULL,'Post client','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(4,NULL,1,0,1,600,'client-cred','secret',3600,NULL,NULL,'Client credentials','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(5,NULL,1,0,1,600,'password-grant','secret',3600,NULL,NULL,'Password grant client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(6,NULL,1,0,1,600,'scim-client-ro','secret',3600,NULL,NULL,'SCIM client (read-only)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,600),(7,NULL,1,0,1,600,'scim-client-rw','secret',3600,NULL,NULL,'SCIM client (read-write)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,600),(8,NULL,1,0,1,600,'token-exchange-actor','secret',3600,NULL,NULL,'Token Exchange grant client actor','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(9,NULL,1,0,1,600,'token-exchange-subject','secret',3600,NULL,NULL,'Token Exchange grant client subject','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(10,NULL,1,0,1,600,'registration-client','secret',3600,NULL,NULL,'Registration service test client','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(11,NULL,1,0,1,600,'token-lookup-client','secret',3600,NULL,NULL,'Token lookup client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(12,NULL,1,0,1,600,'device-code-client','secret',3600,NULL,NULL,'Device code client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,600),(13,NULL,1,0,0,600,'implicit-flow-client',NULL,3600,NULL,NULL,'Implicit Flow client',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,600),(14,NULL,1,0,0,600,'public-dc-client',NULL,3600,NULL,NULL,'Public Device Code client',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,600),(15,NULL,1,0,1,600,'jwt-auth-client_secret_jwt','c8e9eed0-e6e4-4a66-b16e-6f37096356a7',3600,NULL,NULL,'JWT Bearer Auth Client (client_secret_jwt)','SECRET_JWT',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'HS256',NULL,0,NULL,NULL,1,NULL,NULL,NULL,NULL,NULL),(16,NULL,1,0,1,600,'jwt-auth-private_key_jwt','secret',3600,NULL,NULL,'JWT Bearer Auth Client (private_key_jwt)','PRIVATE_KEY',NULL,NULL,NULL,NULL,NULL,NULL,'{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"kid\":\"rsa1\",\"n\":\"1y1CP181zqPNPlV1JDM7Xv0QnGswhSTHe8_XPZHxDTJkykpk_1BmgA3ovP62QRE2ORgsv5oSBI_Z_RaOc4Zx2FonjEJF2oBHtBjsAiF-pxGkM5ZPjFNgFTGp1yUUBjFDcEeIGCwPEyYSt93sQIP_0DRbViMUnpyn3xgM_a1dO5brEWR2n1Uqff1yA5NXfLS03qpl2dpH4HFY5-Zs4bvtJykpAOhoHuIQbz-hmxb9MZ3uTAwsx2HiyEJtz-suyTBHO3BM2o8UcCeyfa34ShPB8i86-sf78fOk2KeRIW1Bju3ANmdV3sxL0j29cesxKCZ06u2ZiGR3Srbft8EdLPzf-w\"}]}',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'RS256',NULL,0,NULL,NULL,1,NULL,NULL,NULL,NULL,NULL),(17,NULL,1,0,1,600,'admin-client-ro','secret',3600,NULL,NULL,'Admin client (read-only)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(18,NULL,1,0,1,600,'admin-client-rw','secret',3600,NULL,NULL,'Admin client (read-write)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(19,NULL,1,0,1,600,'public-client',NULL,3600,3600,NULL,'Public client','NONE',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL); +INSERT INTO `client_details` VALUES (1,NULL,1,0,1,600,'client','secret',3600,NULL,NULL,'Test Client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(2,NULL,1,0,1,0,'tasks-app','secret',0,NULL,NULL,'Tasks App','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(3,NULL,1,0,1,600,'post-client','secret',3600,NULL,NULL,'Post client','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(4,NULL,1,0,1,600,'client-cred','secret',3600,NULL,NULL,'Client credentials','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(5,NULL,1,0,1,600,'password-grant','secret',3600,NULL,NULL,'Password grant client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(6,NULL,1,0,1,600,'scim-client-ro','secret',3600,NULL,NULL,'SCIM client (read-only)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,600),(7,NULL,1,0,1,600,'scim-client-rw','secret',3600,NULL,NULL,'SCIM client (read-write)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,600),(8,NULL,1,0,1,600,'token-exchange-actor','secret',3600,NULL,NULL,'Token Exchange grant client actor','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(9,NULL,1,0,1,600,'token-exchange-subject','secret',3600,NULL,NULL,'Token Exchange grant client subject','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(10,NULL,1,0,1,600,'registration-client','secret',3600,NULL,NULL,'Registration service test client','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(11,NULL,1,0,1,600,'token-lookup-client','secret',3600,NULL,NULL,'Token lookup client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(12,NULL,1,0,1,600,'device-code-client','secret',3600,NULL,NULL,'Device code client','SECRET_BASIC',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,600),(13,NULL,1,0,0,600,'implicit-flow-client',NULL,3600,NULL,NULL,'Implicit Flow client',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,600),(14,NULL,1,0,0,600,'public-dc-client',NULL,3600,NULL,NULL,'Public Device Code client',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,600),(15,NULL,1,0,1,600,'jwt-auth-client_secret_jwt','secret',3600,NULL,NULL,'JWT Bearer Auth Client (client_secret_jwt)','SECRET_JWT',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'HS256',NULL,0,NULL,NULL,1,NULL,NULL,NULL,NULL,NULL),(16,NULL,1,0,1,600,'jwt-auth-private_key_jwt','secret',3600,NULL,NULL,'JWT Bearer Auth Client (private_key_jwt)','PRIVATE_KEY',NULL,NULL,NULL,NULL,NULL,NULL,'{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"kid\":\"rsa1\",\"n\":\"1y1CP181zqPNPlV1JDM7Xv0QnGswhSTHe8_XPZHxDTJkykpk_1BmgA3ovP62QRE2ORgsv5oSBI_Z_RaOc4Zx2FonjEJF2oBHtBjsAiF-pxGkM5ZPjFNgFTGp1yUUBjFDcEeIGCwPEyYSt93sQIP_0DRbViMUnpyn3xgM_a1dO5brEWR2n1Uqff1yA5NXfLS03qpl2dpH4HFY5-Zs4bvtJykpAOhoHuIQbz-hmxb9MZ3uTAwsx2HiyEJtz-suyTBHO3BM2o8UcCeyfa34ShPB8i86-sf78fOk2KeRIW1Bju3ANmdV3sxL0j29cesxKCZ06u2ZiGR3Srbft8EdLPzf-w\"}]}',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'RS256',NULL,0,NULL,NULL,1,NULL,NULL,NULL,NULL,NULL),(17,NULL,1,0,1,600,'admin-client-ro','secret',3600,NULL,NULL,'Admin client (read-only)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(18,NULL,1,0,1,600,'admin-client-rw','secret',3600,NULL,NULL,'Admin client (read-write)','SECRET_POST',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL),(19,NULL,1,0,1,600,'public-client',NULL,3600,3600,NULL,'Public client','NONE',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,'2024-03-27 17:43:07',NULL,1,NULL,NULL,NULL,NULL,NULL); /*!40000 ALTER TABLE `client_details` ENABLE KEYS */; UNLOCK TABLES; diff --git a/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java b/iam-persistence/src/main/java/db/migration/mysql/V110__HashClientSecret.java similarity index 87% rename from iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java rename to iam-persistence/src/main/java/db/migration/mysql/V110__HashClientSecret.java index ff35ac6ecd..6612a8ca8c 100644 --- a/iam-persistence/src/main/java/db/migration/mysql/V109__HashClientSecret.java +++ b/iam-persistence/src/main/java/db/migration/mysql/V110__HashClientSecret.java @@ -25,9 +25,9 @@ import it.infn.mw.iam.persistence.migrations.BaseFlywayJavaMigrationAdapter; -public class V109__HashClientSecret extends BaseFlywayJavaMigrationAdapter { +public class V110__HashClientSecret extends BaseFlywayJavaMigrationAdapter { - public static final Logger LOG = LoggerFactory.getLogger(V109__HashClientSecret.class); + public static final Logger LOG = LoggerFactory.getLogger(V110__HashClientSecret.class); @Override public void migrate(JdbcTemplate jdbcTemplate) throws DataAccessException { @@ -36,7 +36,7 @@ public void migrate(JdbcTemplate jdbcTemplate) throws DataAccessException { PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(DEFAULT_ROUND); - LOG.debug("### START MIGRATION V109__HashClientSecret ###"); + LOG.debug("### START MIGRATION V110__HashClientSecret ###"); SqlRowSet clientList = jdbcTemplate.queryForRowSet("SELECT id, client_secret FROM client_details"); @@ -53,7 +53,7 @@ public void migrate(JdbcTemplate jdbcTemplate) throws DataAccessException { jdbcTemplate.update("UPDATE client_details SET client_secret=? WHERE id=?", secretHash, id); } - LOG.debug("### END MIGRATION V109__HashClientSecret ###"); + LOG.debug("### END MIGRATION V110__HashClientSecret ###"); } } diff --git a/iam-persistence/src/main/resources/db/migration/test/V100000_9__hash_client_test.sql b/iam-persistence/src/main/resources/db/migration/test/V100000_9__hash_client_test.sql deleted file mode 100644 index 0f43144d6e..0000000000 --- a/iam-persistence/src/main/resources/db/migration/test/V100000_9__hash_client_test.sql +++ /dev/null @@ -1,2 +0,0 @@ -UPDATE client_details SET client_secret = '$2a$12$P6AOS2.9DS6L.VaI1qjWnuVFbqUlpU449WDpISytV23H2ANUq0Gtu' WHERE client_secret = 'secret'; -UPDATE client_details SET client_secret = '$2a$12$I4lKKnqnZ0FPqiIf/5WyzONevtVwA/RjftzmhMDhRZPTT4NiRcMsa' WHERE client_secret = 'c8e9eed0-e6e4-4a66-b16e-6f37096356a7'; \ No newline at end of file diff --git a/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql b/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql index b42f42ac8a..9ab0244226 100644 --- a/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql +++ b/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql @@ -9,34 +9,30 @@ INSERT INTO system_scope(scope, description, icon, restricted, default_scope, st INSERT INTO client_details (id, client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection, - token_endpoint_auth_method, require_auth_time, device_code_validity_seconds, created_at, active) VALUES - (1, 'client', 'secret', 'Test Client', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), - (2, 'tasks-app', 'secret', 'Tasks App', false, null, 0, 0, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), - (3, 'post-client', 'secret', 'Post client', false, null, 3600,600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (4, 'client-cred', 'secret', 'Client credentials', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), - (5, 'password-grant', 'secret', 'Password grant client', false, null, 3600, 600, true, 'SECRET_BASIC',true, null, CURRENT_TIMESTAMP(), true), - (6, 'scim-client-ro', 'secret', 'SCIM client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP(), true), - (7, 'scim-client-rw', 'secret', 'SCIM client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP(), true), - (8, 'token-exchange-actor', 'secret', 'Token Exchange grant client actor', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (9, 'token-exchange-subject', 'secret', 'Token Exchange grant client subject', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (10, 'registration-client', 'secret', 'Registration service test client', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (11, 'token-lookup-client', 'secret', 'Token lookup client', false, null, 3600, 600, true, 'SECRET_BASIC', false, null, CURRENT_TIMESTAMP(), true), - (12, 'device-code-client', 'secret', 'Device code client', false, null, 3600, 600, true, 'SECRET_BASIC', false, 600, CURRENT_TIMESTAMP(), true), - (13, 'implicit-flow-client', null, 'Implicit Flow client', false, null, 3600, 600, false, null, false, 600, CURRENT_TIMESTAMP(), true), - (14, 'public-dc-client', null, 'Public Device Code client', false, null, 3600, 600, false, null, false, 600, CURRENT_TIMESTAMP(), true), - (17, 'admin-client-ro', 'secret', 'Admin client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (18, 'admin-client-rw', 'secret', 'Admin client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), - (19, 'public-client', null, 'Public client', false, 3600, 3600, 600, true, 'NONE', false, null, CURRENT_TIMESTAMP(), true), - (20, 'refresh-client', 'secret', 'Refresh Flow client', false, 36000, 3600, 600, true, 'SECRET_BASIC', true, 30, CURRENT_TIMESTAMP(), true); + token_endpoint_auth_method, require_auth_time, device_code_validity_seconds, created_at, token_endpoint_auth_signing_alg, jwks, active) VALUES + (1, 'client', 'secret', 'Test Client', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), null, null, true), + (2, 'tasks-app', 'secret', 'Tasks App', false, null, 0, 0, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), null, null, true), + (3, 'post-client', 'secret', 'Post client', false, null, 3600,600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), null, null, true), + (4, 'client-cred', 'secret', 'Client credentials', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), null, null, true), + (5, 'password-grant', 'secret', 'Password grant client', false, null, 3600, 600, true, 'SECRET_BASIC',true, null, CURRENT_TIMESTAMP(), null, null, true), + (6, 'scim-client-ro', 'secret', 'SCIM client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP(), null, null, true), + (7, 'scim-client-rw', 'secret', 'SCIM client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP(), null, null, true), + (8, 'token-exchange-actor', 'secret', 'Token Exchange grant client actor', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), null, null, true), + (9, 'token-exchange-subject', 'secret', 'Token Exchange grant client subject', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), null, null, true), + (10, 'registration-client', 'secret', 'Registration service test client', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), null, null, true), + (11, 'token-lookup-client', 'secret', 'Token lookup client', false, null, 3600, 600, true, 'SECRET_BASIC', false, null, CURRENT_TIMESTAMP(), null, null, true), + (12, 'device-code-client', 'secret', 'Device code client', false, null, 3600, 600, true, 'SECRET_BASIC', false, 600, CURRENT_TIMESTAMP(), null, null, true), + (13, 'implicit-flow-client', null, 'Implicit Flow client', false, null, 3600, 600, false, null, false, 600, CURRENT_TIMESTAMP(), null, null, true), + (14, 'public-dc-client', null, 'Public Device Code client', false, null, 3600, 600, false, null, false, 600, CURRENT_TIMESTAMP(), null, null, true), + (15, 'jwt-auth-client_secret_jwt', 'secret', 'JWT Bearer Auth Client (client_secret_jwt)', false, null, 3600, 600, true, 'SECRET_JWT', false, null, CURRENT_TIMESTAMP(), 'HS256', null, true), + (16, 'jwt-auth-private_key_jwt', 'secret', 'JWT Bearer Auth Client (private_key_jwt)', false, null, 3600, 600, true,'PRIVATE_KEY', false, null, CURRENT_TIMESTAMP(), 'RS256', + '{"keys":[{"kty":"RSA","e":"AQAB","kid":"rsa1","n":"1y1CP181zqPNPlV1JDM7Xv0QnGswhSTHe8_XPZHxDTJkykpk_1BmgA3ovP62QRE2ORgsv5oSBI_Z_RaOc4Zx2FonjEJF2oBHtBjsAiF-pxGkM5ZPjFNgFTGp1yUUBjFDcEeIGCwPEyYSt93sQIP_0DRbViMUnpyn3xgM_a1dO5brEWR2n1Uqff1yA5NXfLS03qpl2dpH4HFY5-Zs4bvtJykpAOhoHuIQbz-hmxb9MZ3uTAwsx2HiyEJtz-suyTBHO3BM2o8UcCeyfa34ShPB8i86-sf78fOk2KeRIW1Bju3ANmdV3sxL0j29cesxKCZ06u2ZiGR3Srbft8EdLPzf-w"}]}', true), + (17, 'admin-client-ro', 'secret', 'Admin client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), null, null, true), + (18, 'admin-client-rw', 'secret', 'Admin client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), null, null, true), + (19, 'public-client', null, 'Public client', false, 3600, 3600, 600, true, 'NONE', false, null, CURRENT_TIMESTAMP(), null, null, true), + (20, 'refresh-client', 'secret', 'Refresh Flow client', false, 36000, 3600, 600, true, 'SECRET_BASIC', true, 30, CURRENT_TIMESTAMP(), null, null, true); -INSERT INTO client_details (id, client_id, client_secret, client_name, dynamically_registered, - refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection, - token_endpoint_auth_method, require_auth_time, token_endpoint_auth_signing_alg, jwks, active) VALUES - (15, 'jwt-auth-client_secret_jwt', 'c8e9eed0-e6e4-4a66-b16e-6f37096356a7', 'JWT Bearer Auth Client (client_secret_jwt)', - false, null, 3600, 600, true, 'SECRET_JWT', false, 'HS256', null, true), - (16, 'jwt-auth-private_key_jwt', 'secret', 'JWT Bearer Auth Client (private_key_jwt)', - false, null, 3600, 600, true,'PRIVATE_KEY', false, 'RS256', - '{"keys":[{"kty":"RSA","e":"AQAB","kid":"rsa1","n":"1y1CP181zqPNPlV1JDM7Xv0QnGswhSTHe8_XPZHxDTJkykpk_1BmgA3ovP62QRE2ORgsv5oSBI_Z_RaOc4Zx2FonjEJF2oBHtBjsAiF-pxGkM5ZPjFNgFTGp1yUUBjFDcEeIGCwPEyYSt93sQIP_0DRbViMUnpyn3xgM_a1dO5brEWR2n1Uqff1yA5NXfLS03qpl2dpH4HFY5-Zs4bvtJykpAOhoHuIQbz-hmxb9MZ3uTAwsx2HiyEJtz-suyTBHO3BM2o8UcCeyfa34ShPB8i86-sf78fOk2KeRIW1Bju3ANmdV3sxL0j29cesxKCZ06u2ZiGR3Srbft8EdLPzf-w"}]}', true); +UPDATE client_details SET client_secret = '$2a$12$P6AOS2.9DS6L.VaI1qjWnuVFbqUlpU449WDpISytV23H2ANUq0Gtu' WHERE client_secret = 'secret'; INSERT INTO client_scope (owner_id, scope) VALUES (1, 'openid'), From 66d036cdd140db11e4bb748b80fab252fbf06050 Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Thu, 7 Aug 2025 17:38:55 +0200 Subject: [PATCH 31/37] Add update query --- .../resources/db/migration/prod/V3___basic_configuration.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/iam-persistence/src/main/resources/db/migration/prod/V3___basic_configuration.sql b/iam-persistence/src/main/resources/db/migration/prod/V3___basic_configuration.sql index 7b13ae0014..e638ce0450 100644 --- a/iam-persistence/src/main/resources/db/migration/prod/V3___basic_configuration.sql +++ b/iam-persistence/src/main/resources/db/migration/prod/V3___basic_configuration.sql @@ -24,7 +24,9 @@ INSERT INTO iam_account_authority(account_id, authority_id) VALUES INSERT INTO client_details (id, client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection, token_endpoint_auth_method) VALUES (1, 'client', 'secret', 'Test Client', false, null, 3600, 600, true, 'SECRET_BASIC'); - + +UPDATE client_details SET client_secret = '$2a$12$P6AOS2.9DS6L.VaI1qjWnuVFbqUlpU449WDpISytV23H2ANUq0Gtu' WHERE client_secret = 'secret'; + INSERT INTO client_scope (owner_id, scope) VALUES (1, 'openid'), (1, 'profile'), From bce3807574cc69aac8877d463986c5e3832507ce Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Thu, 7 Aug 2025 18:34:41 +0200 Subject: [PATCH 32/37] Fix tests --- .../IamSecurityExpressionMethodsTests.java | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java index c30e117977..99ee9d8295 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java @@ -30,29 +30,23 @@ import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.service.ClientDetailsEntityService; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.Spy; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit4.SpringRunner; -import org.testcontainers.shaded.com.google.common.collect.Sets; import it.infn.mw.iam.IamLoginService; import it.infn.mw.iam.api.account.AccountUtils; import it.infn.mw.iam.api.client.service.ClientService; import it.infn.mw.iam.api.requests.GroupRequestUtils; import it.infn.mw.iam.api.requests.model.GroupRequestDto; -import it.infn.mw.iam.authn.util.Authorities; import it.infn.mw.iam.core.expression.IamSecurityExpressionMethods; import it.infn.mw.iam.core.userinfo.OAuth2AuthenticationScopeResolver; -import it.infn.mw.iam.persistence.model.IamAccount; -import it.infn.mw.iam.persistence.model.IamAccountClient; import it.infn.mw.iam.persistence.repository.IamAccountRepository; import it.infn.mw.iam.persistence.repository.IamGroupRequestRepository; import it.infn.mw.iam.persistence.repository.client.IamAccountClientRepository; @@ -88,15 +82,15 @@ public class IamSecurityExpressionMethodsTests extends GroupRequestsTestUtils { @Autowired private ClientDetailsEntityService clientDetailsService; + @Autowired + AccountUtils accountUtils; + @Mock private IamClientRepository clientRepo; @Mock OAuth2Authentication authentication; - @Mock - AccountUtils accountUtils; - @Spy MockOAuth2Request oauth2Request = new MockOAuth2Request("clientId", new String[] {"openid", "profile"}); @@ -144,21 +138,18 @@ public void testIsNotAdmin() { } @Test + @WithMockUser(roles = {"ADMIN", "USER"}) public void testIsClientOwnerNoAuthenticatedUser() { - when(accountUtils.getAuthenticatedUserAccount(Mockito.any())).thenReturn(null); + // when(accountUtils.getAuthenticatedUserAccount(Mockito.any())).thenReturn(null); assertFalse(getMethods().isClientOwner("client")); } @Test + @WithMockUser(roles = {"ADMIN", "USER"}, username = TEST_ADMIN) public void testIsClientOwnerIsAdmin() { - String owner = "admin"; clientService.linkClientToAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID), - accountRepo.findByUsername(owner).get()); - - when(accountUtils.getAuthenticatedUserAccount(Mockito.any())).thenReturn(Optional.empty()); - IamAccount adminAccount = accountRepo.findByUsername(owner).orElseThrow(); - doReturn(Optional.of(adminAccount)).when(accountUtils).getAuthenticatedUserAccount(); + accountRepo.findByUsername(TEST_ADMIN).get()); ClientDetailsEntity clientTest = clientDetailsService.loadClientByClientId(TEST_CLIENT_ID); doReturn(Optional.of(clientTest)).when(clientRepo).findByClientId(TEST_CLIENT_ID); @@ -166,13 +157,12 @@ public void testIsClientOwnerIsAdmin() { } @Test + @WithMockUser(roles = {"ADMIN", "USER"}, username = "test_200") public void testIsClientOwnerIsUser() { String owner = "test_200"; clientService.linkClientToAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID), accountRepo.findByUsername(owner).get()); - IamAccount userAccount = accountRepo.findByUsername(owner).orElseThrow(); - doReturn(Optional.of(userAccount)).when(accountUtils).getAuthenticatedUserAccount(); ClientDetailsEntity clientTest = clientDetailsService.loadClientByClientId(TEST_CLIENT_ID); doReturn(Optional.of(clientTest)).when(clientRepo).findByClientId(TEST_CLIENT_ID); @@ -180,14 +170,12 @@ public void testIsClientOwnerIsUser() { } @Test + @WithMockUser(roles = {"ADMIN", "USER"}, username = TEST_ADMIN) public void testIsClientOwnerIsNotUser() { String owner = "test_200"; - String notOwner = "admin"; clientService.linkClientToAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID), accountRepo.findByUsername(owner).get()); - IamAccount userAccount = accountRepo.findByUsername(notOwner).orElseThrow(); - doReturn(Optional.of(userAccount)).when(accountUtils).getAuthenticatedUserAccount(); ClientDetailsEntity clientTest = clientDetailsService.loadClientByClientId(TEST_CLIENT_ID); doReturn(Optional.of(clientTest)).when(clientRepo).findByClientId(TEST_CLIENT_ID); From a82a368a2fd299c3ffd30b55d332c8fb9602e7e0 Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Thu, 7 Aug 2025 19:05:54 +0200 Subject: [PATCH 33/37] Delete data after tests --- .../mw/iam/test/core/IamSecurityExpressionMethodsTests.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java index 99ee9d8295..efe9b583a9 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java @@ -97,6 +97,10 @@ public class IamSecurityExpressionMethodsTests extends GroupRequestsTestUtils { @After public void destroy() { repo.deleteAll(); + clientService.unlinkClientFromAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID), + accountRepo.findByUsername(TEST_ADMIN).get()); + clientService.unlinkClientFromAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID), + accountRepo.findByUsername("test_200").get()); } private IamSecurityExpressionMethods getMethods() { @@ -140,8 +144,6 @@ public void testIsNotAdmin() { @Test @WithMockUser(roles = {"ADMIN", "USER"}) public void testIsClientOwnerNoAuthenticatedUser() { - // when(accountUtils.getAuthenticatedUserAccount(Mockito.any())).thenReturn(null); - assertFalse(getMethods().isClientOwner("client")); } From 77e6beba9e86452a9aa0bea306f29aff6f9674ed Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Tue, 12 Aug 2025 16:23:21 +0200 Subject: [PATCH 34/37] Cosmetic fixes --- .../mw/iam/core/expression/IamSecurityExpressionMethods.java | 5 +---- .../mw/iam/test/api/client/RegistrationAccessTokenTests.java | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java index a34e576e11..ddf4d4cbfd 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/expression/IamSecurityExpressionMethods.java @@ -165,9 +165,6 @@ public boolean hasAdminOrGMDashboardRoleOfGroup(String gid) { public boolean isClientOwner(String clientId) { Optional owner = accountUtils.getAuthenticatedUserAccount(); Optional client = clientRepo.findByClientId(clientId); - if (!owner.isPresent() || !client.isPresent()) { - return false; - } - return accountClientRepo.findByAccountAndClient(owner.get(), client.get()).isPresent(); + return owner.isPresent() && client.isPresent() && accountClientRepo.findByAccountAndClient(owner.get(), client.get()).isPresent(); } } diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/RegistrationAccessTokenTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/RegistrationAccessTokenTests.java index e2680a9c5b..6e8a08c908 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/RegistrationAccessTokenTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/RegistrationAccessTokenTests.java @@ -45,6 +45,7 @@ import it.infn.mw.iam.test.api.TestSupport; import it.infn.mw.iam.test.oauth.client_registration.ClientRegistrationTestSupport.ClientJsonStringBuilder; import it.infn.mw.iam.test.util.annotation.IamRandomPortIntegrationTest; +import it.infn.mw.iam.util.IamClientSecretEncoder; @IamRandomPortIntegrationTest public class RegistrationAccessTokenTests extends TestSupport { @@ -111,7 +112,7 @@ public void testRatWorkAsExpected() throws ParseException { .body() .as(RegisteredClientDTO.class); - assertThat(new BCryptPasswordEncoder(12).matches(registerResponse.getClientSecret(), getResponse.getClientSecret()), is(true)); + assertThat(new IamClientSecretEncoder().matches(registerResponse.getClientSecret(), getResponse.getClientSecret()), is(true)); assertThat(getResponse.getRegistrationAccessToken(), nullValue()); RegisteredClientDTO rotatedRatClient = From 155ef0d581b5c040ca27140ea688656f4c9c3c72 Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Thu, 28 Aug 2025 15:30:41 +0200 Subject: [PATCH 35/37] Fix updateClient with old clientSecret value --- .../service/DefaultClientManagementService.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java index 7f73bda324..e2b5811afe 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java @@ -168,13 +168,13 @@ private List getClientOwners(String clientId) { @Validated(OnClientUpdate.class) @Override - public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO client) + public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO clientDTO) throws ParseException { ClientDetailsEntity oldClient = clientService.findClientByClientId(clientId) .orElseThrow(ClientSuppliers.clientNotFound(clientId)); - ClientDetailsEntity newClient = converter.entityFromClientManagementRequest(client); + ClientDetailsEntity newClient = converter.entityFromClientManagementRequest(clientDTO); newClient.setId(oldClient.getId()); newClient.setCreatedAt(oldClient.getCreatedAt()); @@ -185,8 +185,11 @@ public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO cli if (NONE.equals(newClient.getTokenEndpointAuthMethod())) { newClient.setClientSecret(null); - } else if (isNull(client.getClientSecret())) { - client.setClientSecret(defaultsService.generateClientSecret()); + } else if (isNull(clientDTO.getClientSecret()) && isNull(oldClient.getClientSecret())) { + newClient.setClientSecret(defaultsService.generateClientSecret()); + } else { + // user cannot change the clientSecret on update + newClient.setClientSecret(oldClient.getClientSecret()); } newClient = clientService.updateClient(newClient); @@ -290,3 +293,7 @@ public RegisteredClientDTO rotateRegistrationAccessToken(@NotBlank String client } } + + + +// Fix updateClient with old clientSecret value \ No newline at end of file From f2751261d09d10f5458a973b219f2f819c03a6e0 Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Fri, 12 Sep 2025 15:38:34 +0200 Subject: [PATCH 36/37] Prevent direct update of the clientSecret value --- .../service/DefaultClientManagementService.java | 7 +------ .../service/DefaultClientRegistrationService.java | 11 ++++++++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java index e2b5811afe..a9856b6fbe 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java @@ -175,7 +175,6 @@ public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO cli .orElseThrow(ClientSuppliers.clientNotFound(clientId)); ClientDetailsEntity newClient = converter.entityFromClientManagementRequest(clientDTO); - newClient.setId(oldClient.getId()); newClient.setCreatedAt(oldClient.getCreatedAt()); newClient.setClientId(oldClient.getClientId()); @@ -188,7 +187,7 @@ public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO cli } else if (isNull(clientDTO.getClientSecret()) && isNull(oldClient.getClientSecret())) { newClient.setClientSecret(defaultsService.generateClientSecret()); } else { - // user cannot change the clientSecret on update + // Direct updates are disabled. Changes must be made via secret reset process newClient.setClientSecret(oldClient.getClientSecret()); } @@ -293,7 +292,3 @@ public RegisteredClientDTO rotateRegistrationAccessToken(@NotBlank String client } } - - - -// Fix updateClient with old clientSecret value \ No newline at end of file diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java index bfaf5169d0..42316ce05d 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java @@ -19,6 +19,7 @@ import static it.infn.mw.iam.config.client_registration.ClientRegistrationProperties.ClientRegistrationAuthorizationPolicy.ADMINISTRATORS; import static it.infn.mw.iam.config.client_registration.ClientRegistrationProperties.ClientRegistrationAuthorizationPolicy.ANYONE; import static it.infn.mw.iam.config.client_registration.ClientRegistrationProperties.ClientRegistrationAuthorizationPolicy.REGISTERED_USERS; +import static org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod.NONE; import static java.util.Objects.isNull; import static java.util.stream.Collectors.toSet; @@ -427,7 +428,6 @@ public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO req ClientDetailsEntity newClient = converter.entityFromRegistrationRequest(request); newClient.setId(oldClient.getId()); - newClient.setClientSecret(oldClient.getClientSecret()); newClient.setAccessTokenValiditySeconds(oldClient.getAccessTokenValiditySeconds()); newClient.setIdTokenValiditySeconds(oldClient.getIdTokenValiditySeconds()); newClient.setRefreshTokenValiditySeconds(oldClient.getRefreshTokenValiditySeconds()); @@ -439,6 +439,15 @@ public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO req newClient.setReuseRefreshToken(oldClient.isReuseRefreshToken()); newClient.setActive(oldClient.isActive()); + if (NONE.equals(newClient.getTokenEndpointAuthMethod())) { + newClient.setClientSecret(null); + } else if (isNull(request.getClientSecret()) && isNull(oldClient.getClientSecret())) { + newClient.setClientSecret(defaultsService.generateClientSecret()); + } else { + // Direct updates are disabled. Changes must be made via secret reset process + newClient.setClientSecret(oldClient.getClientSecret()); + } + if (registrationProperties.isAdminOnlyCustomScopes() && !accountUtils.isAdmin(authentication)) { removeCustomScopes(newClient); } From f0beac89064cc444fbab5c15f4e87c0866caaacd Mon Sep 17 00:00:00 2001 From: SteDev2 Date: Fri, 12 Sep 2025 17:54:25 +0200 Subject: [PATCH 37/37] Refactor client linking in tests --- .../IamSecurityExpressionMethodsTests.java | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java index efe9b583a9..55476530dc 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/core/IamSecurityExpressionMethodsTests.java @@ -18,10 +18,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import java.util.Date; import java.util.Optional; import org.junit.After; @@ -47,6 +44,7 @@ import it.infn.mw.iam.api.requests.model.GroupRequestDto; import it.infn.mw.iam.core.expression.IamSecurityExpressionMethods; import it.infn.mw.iam.core.userinfo.OAuth2AuthenticationScopeResolver; +import it.infn.mw.iam.persistence.model.IamAccount; import it.infn.mw.iam.persistence.repository.IamAccountRepository; import it.infn.mw.iam.persistence.repository.IamGroupRequestRepository; import it.infn.mw.iam.persistence.repository.client.IamAccountClientRepository; @@ -150,10 +148,11 @@ public void testIsClientOwnerNoAuthenticatedUser() { @Test @WithMockUser(roles = {"ADMIN", "USER"}, username = TEST_ADMIN) public void testIsClientOwnerIsAdmin() { - clientService.linkClientToAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID), - accountRepo.findByUsername(TEST_ADMIN).get()); ClientDetailsEntity clientTest = clientDetailsService.loadClientByClientId(TEST_CLIENT_ID); - doReturn(Optional.of(clientTest)).when(clientRepo).findByClientId(TEST_CLIENT_ID); + Optional account = accountRepo.findByUsername(TEST_ADMIN); + ClientDetailsEntity clientTestUpdate = clientService.linkClientToAccount(clientTest, account.get()); + + doReturn(Optional.of(clientTestUpdate)).when(clientRepo).findByClientId(TEST_CLIENT_ID); assertTrue(getMethods().isClientOwner(TEST_CLIENT_ID)); } @@ -162,11 +161,11 @@ public void testIsClientOwnerIsAdmin() { @WithMockUser(roles = {"ADMIN", "USER"}, username = "test_200") public void testIsClientOwnerIsUser() { String owner = "test_200"; - clientService.linkClientToAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID), - accountRepo.findByUsername(owner).get()); - ClientDetailsEntity clientTest = clientDetailsService.loadClientByClientId(TEST_CLIENT_ID); - doReturn(Optional.of(clientTest)).when(clientRepo).findByClientId(TEST_CLIENT_ID); + Optional account = accountRepo.findByUsername(owner); + ClientDetailsEntity clientTestUpdate = clientService.linkClientToAccount(clientTest, account.get()); + + doReturn(Optional.of(clientTestUpdate)).when(clientRepo).findByClientId(TEST_CLIENT_ID); assertTrue(getMethods().isClientOwner(TEST_CLIENT_ID)); } @@ -175,11 +174,11 @@ public void testIsClientOwnerIsUser() { @WithMockUser(roles = {"ADMIN", "USER"}, username = TEST_ADMIN) public void testIsClientOwnerIsNotUser() { String owner = "test_200"; - clientService.linkClientToAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID), - accountRepo.findByUsername(owner).get()); - ClientDetailsEntity clientTest = clientDetailsService.loadClientByClientId(TEST_CLIENT_ID); - doReturn(Optional.of(clientTest)).when(clientRepo).findByClientId(TEST_CLIENT_ID); + Optional account = accountRepo.findByUsername(owner); + ClientDetailsEntity clientTestUpdate = clientService.linkClientToAccount(clientTest, account.get()); + + doReturn(Optional.of(clientTestUpdate)).when(clientRepo).findByClientId(TEST_CLIENT_ID); assertFalse(getMethods().isClientOwner(TEST_CLIENT_ID)); }