Generate a client secret when a public client goes private

This can also be used as a way to get a new client secret for a private client by switching to public and back.
This commit is contained in:
Andreas Svanberg 2025-04-25 10:56:27 +02:00
parent 18945e22bf
commit 360119ad6a
Signed by: ansv7779
GPG Key ID: 729B051CFFD42F92
4 changed files with 49 additions and 5 deletions
src
main/java/se/su/dsv/oauth2
test/java/se/su/dsv/oauth2/web/client

@ -7,7 +7,7 @@ import java.util.Optional;
public interface ClientManagementService {
NewClient createClient(Principal owner, ClientData clientData);
Client updateClient(Principal principal, String id, ClientData clientData);
NewClient updateClient(Principal principal, String id, ClientData clientData);
Optional<Client> getClient(Principal owner, String id);

@ -54,7 +54,7 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme
}
@Override
public Client updateClient(final Principal principal, final String id, final ClientData clientData) {
public NewClient updateClient(final Principal principal, final String id, final ClientData clientData) {
boolean ownsClient = clientRepository.getOwners(id)
.contains(principal.getName());
if (!ownsClient) {
@ -64,11 +64,22 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme
ClientRow currentClient = clientRepository.getClientRowById(id)
.orElseThrow(() -> new IllegalArgumentException("No such client"));
String newClientSecret;
if (clientData.isPublic()) {
newClientSecret = null;
} else if (currentClient.isPublic()) {
newClientSecret = Util.generateAlphanumericString(32);
} else {
newClientSecret = currentClient.clientSecret();
}
boolean requiresNewSecret = currentClient.isPublic() && !clientData.isPublic();
ClientRow updated = new ClientRow(id, currentClient.clientId(), clientData.clientName(),
clientData.contactEmail(), getRedirectUri(clientData),
toScopeString(clientData), currentClient.clientSecret(), clientData.requiresConsent());
toScopeString(clientData), newClientSecret, clientData.requiresConsent());
clientRepository.update(updated);
return toClient(updated);
return new NewClient(id, currentClient.clientId(), requiresNewSecret ? newClientSecret : null);
}
private static String toScopeString(final ClientData clientData) {

@ -156,8 +156,10 @@ public class ClientAdminController {
newClientRequest.scopes(),
newClientRequest.contact(),
requiresConsent);
final Client updatedClient = clientManagementService.updateClient(principal, id, clientData);
final NewClient updatedClient = clientManagementService.updateClient(principal, id, clientData);
redirectAttributes.addFlashAttribute("message", "Client updated");
redirectAttributes.addFlashAttribute("clientId", updatedClient.clientId());
redirectAttributes.addFlashAttribute("clientSecret", updatedClient.clientSecret());
return "redirect:/admin/client/" + updatedClient.id();
}

@ -126,4 +126,35 @@ public class ClientAdminControllerTest {
Client client = clients.get(0);
assertThat(client.requiresConsent(), is(false));
}
@Test
public void switching_public_client_to_private_should_generate_secret() throws Exception {
String name = "My client";
String contactEmail = "developer@3rd-party.example";
String redirectUri = "https://3rd-party.example/oauth2/callback";
String principal = "third-party-developer";
MvcResult creationResult = mockMvc.perform(post("/admin/client/new")
.with(csrf())
.with(remoteUser(principal))
.formField("name", name)
.formField("contact", contactEmail)
.formField("redirectUri", redirectUri)
.formField("public_", "on"))
.andExpect(status().isFound())
.andExpect(flash().attribute("clientSecret", blankOrNullString()))
.andReturn();
String clientUrl = creationResult.getResponse().getRedirectedUrl();
assertNotNull(clientUrl);
mockMvc.perform(post(clientUrl + "/edit")
.with(csrf())
.with(remoteUser(principal))
.formField("name", name)
.formField("contact", contactEmail)
.formField("redirectUri", redirectUri))
.andExpect(status().is3xxRedirection())
.andExpect(flash().attribute("clientSecret", not(blankOrNullString())));
}
}