Skip to content

Save client secret hashed on the database#947

Open
SteDev2 wants to merge 37 commits intodevelopfrom
issue-923
Open

Save client secret hashed on the database#947
SteDev2 wants to merge 37 commits intodevelopfrom
issue-923

Conversation

@SteDev2
Copy link
Contributor

@SteDev2 SteDev2 commented Mar 26, 2025

No description provided.

@SteDev2 SteDev2 self-assigned this Mar 26, 2025
@SteDev2 SteDev2 added this to v1.12.0 Mar 26, 2025
@rmiccoli rmiccoli changed the title Issue 923 Save client secret hashed on the database Mar 26, 2025
@rmiccoli rmiccoli linked an issue Mar 26, 2025 that may be closed by this pull request
@rmiccoli rmiccoli moved this to In Progress in v1.12.0 Mar 27, 2025
@enricovianello enricovianello removed this from v1.12.0 Apr 1, 2025
@enricovianello enricovianello added the component/db Issue that includes one or more db migrations label Apr 2, 2025
@enricovianello enricovianello marked this pull request as ready for review April 17, 2025 13:00
} else if (isNull(client.getClientSecret())) {
client.setClientSecret(defaultsService.generateClientSecret());
} else {
newClient.setClientSecret(defaultsService.generateClientSecret());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In quali casi entriamo in questo else?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

è da rimuovere, altrimenti genera un nuovo secret ogni volta che modifichiamo il client

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some cases, when updating client data from the dashboard, the dashboard doesn't know the new ClientSecret hash, so it's updated with the old value. We don't want the user to change the secret or secret hash from the client update, but only from the new secret request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here's definitely a better way to write this check, but I discovered it after a bug search and it gave me bad credentials. I'll finish it in the next few days, or if anyone wants to fix it...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would preferably identify the "some cases" and properly address them with specific checks rather than a generic else, if there is no a valid reason to do it.

});
return res;

var res = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var res = () => {
const res = () => {

return res;

var res = () => {
var modalInstance = $uibModal.open({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var modalInstance = $uibModal.open({
const modalInstance = $uibModal.open({

modalInstance.result.then(function (selectedItem) {
$ctrl.selected = selectedItem;
}, function () {
console.log('Dialog dismissed at: ' + new Date());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this log on the browser console?

}, function () {
console.log('Dialog dismissed at: ' + new Date());
});
return res;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are returning lambda function, is it correct?

return res;
}

var modalSecret = $uibModal.open({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var modalSecret = $uibModal.open({
const modalSecret = $uibModal.open({

assertEquals(client.getIdTokenValiditySeconds(), clientDB.get().getIdTokenValiditySeconds());
assertEquals(client.getDeviceCodeValiditySeconds(), clientDB.get().getDeviceCodeValiditySeconds());
assertEquals(client.getDeviceCodeValiditySeconds(),
clientDB.get().getDeviceCodeValiditySeconds());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are calling clientDB.get() at least four times.
Call it only once a store the result in a variable

clientDB = clientRepo.findByClientId(client.getClientId());
assertEquals(client.getAccessTokenValiditySeconds(), clientDB.get().getAccessTokenValiditySeconds());
assertEquals(client.getRefreshTokenValiditySeconds(), clientDB.get().getRefreshTokenValiditySeconds());
assertEquals(client.getAccessTokenValiditySeconds(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above


String responseJson = mvc
.perform(post(ClientRegistrationApiController.ENDPOINT).contentType(APPLICATION_JSON)
.content(clientJson))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.content(clientJson))
.content(clientJson))

RegisteredClientDTO client = mapper.readValue(responseJson, RegisteredClientDTO.class);
String clientSecret = client.getClientSecret();

final String url =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that final is unnecessary in this context


String responseJson = mvc
.perform(post(ClientManagementAPIController.ENDPOINT).contentType(APPLICATION_JSON)
.content(clientJson))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.content(clientJson))
.content(clientJson))

(don't copy/past to much)

RegisteredClientDTO client = mapper.readValue(responseJson, RegisteredClientDTO.class);
String clientSecret = client.getClientSecret();

final String url =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

ClientDetailsEntity client =
clientRepo.findByClientId(clientId).orElseThrow(ClientSuppliers.clientNotFound(clientId));

final String url =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

@WithMockUser(username = "test", roles = {"USER"})
void secretRotationFailsForOtherUsers() throws Exception {

assertSecretRotationForbidden("client");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can use a better client id rather than "client"

@WithMockUser(username = "non-existent-user", roles = {"USER"})
void secretRotationFailsWhenUserNotFound() throws Exception {

assertSecretRotationForbidden("client-cred");
Copy link
Contributor

@jacogasp jacogasp Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

.as(RegisteredClientDTO.class);

assertThat(getResponse.getClientSecret(), is(registerResponse.getClientSecret()));
assertThat(new IamClientSecretEncoder().matches(registerResponse.getClientSecret(), getResponse.getClientSecret()), is(true));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we split this line in multiple lines? Is very hard to read

repo.deleteAll();
clientService.unlinkClientFromAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID),
accountRepo.findByUsername(TEST_ADMIN).get());
clientService.unlinkClientFromAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please call clientDetailsService.loadClientByClientId(TEST_CLIENT_ID) only once and store the result.
This very long calls are really hard to read

@Test
@WithMockUser(roles = {"ADMIN", "USER"}, username = TEST_ADMIN)
public void testIsClientOwnerIsAdmin() {
clientService.linkClientToAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comment

@WithMockUser(roles = {"ADMIN", "USER"}, username = "test_200")
public void testIsClientOwnerIsUser() {
String owner = "test_200";
clientService.linkClientToAccount(clientDetailsService.loadClientByClientId(TEST_CLIENT_ID),
Copy link
Contributor

@jacogasp jacogasp Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not repeat again, check also the next lines

throws JOSEException {

JWSSigner signer = new MACSigner(CLIENT_ID_SECRET_JWT_SECRET);
ClientDetailsEntity client = clientRepo.findByClientId(CLIENT_ID_SECRET_JWT).orElseThrow();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the .orElseThrow() call necessary? On the next line, if the client does not exist (is null) the client.getClientSecret() will throw a NullPointerException

client.setScope(Sets.newHashSet("openid", "profile", "email"));
clientRepo.save(client);

try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cannot understand this try without the catch.
If we have an error here, this will disappear in the cosmic galactic void without any evidence


if (NONE.equals(newClient.getTokenEndpointAuthMethod())) {
newClient.setClientSecret(null);
} else if (isNull(request.getClientSecret()) && isNull(oldClient.getClientSecret())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curiosity: what is the difference between NONE.equals and isNull? Can we use the same check to be consistent?

} 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a secret reset process? Do you mean calling the reset secret API endpoint?
And what do you mean with "direct updates?" Do you mean passing the secret in the PUT request to edit the client?
If I'm getting it right, we should add a test that checks that the secret is ignored from the PUT request or a bad request error is thrown

doReturn(Optional.of(clientTest)).when(clientRepo).findByClientId(TEST_CLIENT_ID);
Optional<IamAccount> account = accountRepo.findByUsername(TEST_ADMIN);
ClientDetailsEntity clientTestUpdate = clientService.linkClientToAccount(clientTest, account.get());

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

doReturn(Optional.of(clientTest)).when(clientRepo).findByClientId(TEST_CLIENT_ID);
Optional<IamAccount> account = accountRepo.findByUsername(owner);
ClientDetailsEntity clientTestUpdate = clientService.linkClientToAccount(clientTest, account.get());

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

doReturn(Optional.of(clientTest)).when(clientRepo).findByClientId(TEST_CLIENT_ID);
Optional<IamAccount> account = accountRepo.findByUsername(owner);
ClientDetailsEntity clientTestUpdate = clientService.linkClientToAccount(clientTest, account.get());

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client secret hashed on DataBase

6 participants