Refactor out storage specifics from the ClientManager

In preparation of being able to change how clients are stored (from JDBC to in-memory)
This commit is contained in:
Andreas Svanberg 2025-03-26 11:05:41 +01:00
parent 411bba57b2
commit 6039d6b34b
Signed by: ansv7779
GPG Key ID: 729B051CFFD42F92
5 changed files with 160 additions and 87 deletions

@ -0,0 +1,79 @@
package se.su.dsv.oauth2;
import org.springframework.jdbc.core.simple.JdbcClient;
import se.su.dsv.oauth2.admin.repository.ClientRepository;
import se.su.dsv.oauth2.admin.repository.ClientRow;
import java.security.Principal;
import java.util.List;
import java.util.Optional;
public class JDBCClientRepository implements ClientRepository {
public final JdbcClient jdbc;
public JDBCClientRepository(final JdbcClient jdbc) {
this.jdbc = jdbc;
}
public JdbcClient getJdbc() {
return jdbc;
}
@Override
public void addClientOwner(final String principalName, final String id) {
getJdbc().sql("INSERT INTO client_owner (client_id, owner) VALUES (:clientId, :owner)")
.param("clientId", id)
.param("owner", principalName)
.update();
}
@Override
public List<ClientRow> getClients(final Principal owner) {
return getJdbc().sql("SELECT id, client_id, name, contact_email, redirect_uri, scopes, client_secret FROM client WHERE id IN (SELECT client_id FROM client_owner WHERE owner = :owner)")
.param("owner", owner.getName())
.query(ClientRow.class)
.list();
}
@Override
public void removeOwner(final String id, final String owner) {
getJdbc().sql("DELETE FROM client_owner WHERE client_id = :id AND owner = :owner")
.param("id", id)
.param("owner", owner)
.update();
}
@Override
public Optional<ClientRow> getClientRowById(final String id) {
return getJdbc().sql("SELECT id, client_id, name, contact_email, redirect_uri, scopes, client_secret FROM client WHERE id = :id")
.param("id", id)
.query(ClientRow.class)
.optional();
}
@Override
public Optional<ClientRow> getClientRowByClientId(final String clientId) {
return getJdbc().sql("SELECT id, client_id, name, contact_email, redirect_uri, scopes, client_secret FROM client WHERE client_id = :clientId")
.param("clientId", clientId)
.query(ClientRow.class)
.optional();
}
@Override
public void addNewClient(final ClientRow clientRow) {
getJdbc().sql("""
INSERT INTO client (id, client_id, client_secret, name, redirect_uri, contact_email, scopes)
VALUES (:id, :clientId, :clientSecret, :name, :redirectUri, :contactEmail, :scopes)
""")
.paramSource(clientRow)
.update();
}
@Override
public List<String> getOwners(final String id) {
return getJdbc().sql("SELECT owner FROM client_owner WHERE client_id = :clientId")
.param("clientId", id)
.query(String.class)
.list();
}
}

@ -0,0 +1,13 @@
package se.su.dsv.oauth2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.simple.JdbcClient;
@Configuration
public class PersistentConfiguration {
@Bean
public JDBCClientRepository jdbcClientRepository(JdbcClient jdbcClient) {
return new JDBCClientRepository(jdbcClient);
}
}

@ -1,6 +1,5 @@
package se.su.dsv.oauth2.admin;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
@ -10,29 +9,28 @@ import org.springframework.security.oauth2.server.authorization.settings.ClientS
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.stereotype.Service;
import se.su.dsv.oauth2.admin.repository.ClientRepository;
import se.su.dsv.oauth2.admin.repository.ClientRow;
import java.security.Principal;
import java.time.Duration;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@Service
public class ClientManager implements RegisteredClientRepository, ClientManagementService {
private final PasswordEncoder passwordEncoder;
private final JdbcClient jdbc;
private final ClientRepository clientRepository;
public ClientManager(
PasswordEncoder passwordEncoder,
JdbcClient jdbc)
ClientRepository clientRepository)
{
this.passwordEncoder = passwordEncoder;
this.jdbc = jdbc;
this.clientRepository = clientRepository;
}
@Override
@ -42,49 +40,32 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme
String clientSecret = clientData.isPublic() ? null : Util.generateAlphanumericString(32);
String encodedClientSecret = clientSecret == null ? null : passwordEncoder.encode(clientSecret);
String redirectURI = clientData.redirectURI() != null ? clientData.redirectURI().toString() : null;
String scopeString = String.join(" ", clientData.scopes());
jdbc.sql("""
INSERT INTO client (id, client_id, client_secret, name, redirect_uri, contact_email, scopes)
VALUES (:id, :clientId, :clientSecret, :name, :redirectUri, :contactEmail, :scopes)
""")
.param("id", id)
.param("clientId", clientId)
.param("clientSecret", encodedClientSecret)
.param("name", clientData.clientName())
.param("redirectUri", redirectURI)
.param("contactEmail", clientData.contactEmail())
.param("scopes", String.join(" ", clientData.scopes()))
.update();
ClientRow clientRow = new ClientRow(id, clientId, clientData.clientName(), clientData.contactEmail(),
redirectURI, scopeString, encodedClientSecret);
addClientOwner(owner.getName(), id);
clientRepository.addNewClient(clientRow);
clientRepository.addClientOwner(owner.getName(), id);
return new NewClient(id, clientId, clientSecret);
}
private void addClientOwner(final String principalName, final String id) {
jdbc.sql("INSERT INTO client_owner (client_id, owner) VALUES (:clientId, :owner)")
.param("clientId", id)
.param("owner", principalName)
.update();
}
@Override
public Optional<Client> getClient(final Principal principal, final String id) {
boolean ownsClient = getOwners(id).contains(principal.getName());
boolean ownsClient = clientRepository.getOwners(id).contains(principal.getName());
if (!ownsClient) {
return Optional.empty();
}
return getClientRowById(id)
return clientRepository.getClientRowById(id)
.map(this::toClient);
}
@Override
public List<Client> getClients(final Principal owner) {
return jdbc.sql("SELECT id, client_id, name, contact_email, redirect_uri, scopes, client_secret FROM client WHERE id IN (SELECT client_id FROM client_owner WHERE owner = :owner)")
.param("owner", owner.getName())
.query(ClientRow.class)
.list()
return clientRepository.getClients(owner)
.stream()
.map(this::toClient)
.toList();
@ -92,48 +73,28 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme
@Override
public void addOwner(final Principal currentUser, final String id, final String newOwnerPrincipal) {
if (!getOwners(id).contains(currentUser.getName())) {
if (!clientRepository.getOwners(id).contains(currentUser.getName())) {
throw new IllegalStateException(currentUser.getName() + " is not an owner of the client");
}
jdbc.sql("INSERT INTO client_owner (client_id, owner) VALUES (:id, :owner) ON DUPLICATE KEY UPDATE owner = owner")
.param("id", id)
.param("owner", newOwnerPrincipal)
.update();
clientRepository.addClientOwner(newOwnerPrincipal, id);
}
@Override
public boolean removeOwner(final Principal currentUser, final String id, final String owner) {
if (!getOwners(id).contains(currentUser.getName())) {
if (!clientRepository.getOwners(id).contains(currentUser.getName())) {
throw new IllegalStateException(currentUser.getName() + " is not an owner of the client");
}
if (currentUser.getName().equals(owner)) {
return false;
} else {
jdbc.sql("DELETE FROM client_owner WHERE client_id = :id AND owner = :owner")
.param("id", id)
.param("owner", owner)
.update();
clientRepository.removeOwner(id, owner);
return true;
}
}
private Optional<ClientRow> getClientRowById(final String id) {
return jdbc.sql("SELECT id, client_id, name, contact_email, redirect_uri, scopes, client_secret FROM client WHERE id = :id")
.param("id", id)
.query(ClientRow.class)
.optional();
}
private Optional<ClientRow> getClientRowByClientId(final String clientId) {
return jdbc.sql("SELECT id, client_id, name, contact_email, redirect_uri, scopes, client_secret FROM client WHERE client_id = :clientId")
.param("clientId", clientId)
.query(ClientRow.class)
.optional();
}
private Client toClient(final ClientRow clientRow) {
List<String> owners = getOwners(clientRow.id());
List<String> owners = clientRepository.getOwners(clientRow.id());
owners.sort(Comparator.naturalOrder());
Set<String> scopes = clientRow.scopeSet();
boolean isPublic = clientRow.isPublic();
@ -148,13 +109,6 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme
owners);
}
private List<String> getOwners(final String id) {
return jdbc.sql("SELECT owner FROM client_owner WHERE client_id = :clientId")
.param("clientId", id)
.query(String.class)
.list();
}
// Used by various components of the OAuth 2.0 infrastructure to upgrade
// the client secret if necessary based on the PasswordEncoder bean.
@Override
@ -164,14 +118,14 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme
@Override
public RegisteredClient findById(final String id) {
return getClientRowById(id)
return clientRepository.getClientRowById(id)
.map(ClientManager::toRegisteredClient)
.orElse(null);
}
@Override
public RegisteredClient findByClientId(final String clientId) {
return getClientRowByClientId(clientId)
return clientRepository.getClientRowByClientId(clientId)
.map(ClientManager::toRegisteredClient)
.orElse(null);
}
@ -216,24 +170,4 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme
.scopes(currentScopes -> currentScopes.addAll(clientRow.scopeSet()))
.build();
}
private record ClientRow(
String id,
String clientId,
String name,
String contactEmail,
String redirectUri,
String scopes,
String clientSecret)
{
private Set<String> scopeSet() {
return Arrays.stream(this.scopes.split(" "))
.filter(Predicate.not(String::isBlank))
.collect(Collectors.toUnmodifiableSet());
}
private boolean isPublic() {
return clientSecret == null;
}
}
}

@ -0,0 +1,21 @@
package se.su.dsv.oauth2.admin.repository;
import java.security.Principal;
import java.util.List;
import java.util.Optional;
public interface ClientRepository {
void addNewClient(ClientRow clientRow);
List<ClientRow> getClients(Principal owner);
void addClientOwner(String principalName, String id);
void removeOwner(String id, String owner);
List<String> getOwners(String id);
Optional<ClientRow> getClientRowById(String id);
Optional<ClientRow> getClientRowByClientId(String clientId);
}

@ -0,0 +1,26 @@
package se.su.dsv.oauth2.admin.repository;
import java.util.Arrays;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public record ClientRow(
String id,
String clientId,
String name,
String contactEmail,
String redirectUri,
String scopes,
String clientSecret)
{
public Set<String> scopeSet() {
return Arrays.stream(this.scopes.split(" "))
.filter(Predicate.not(String::isBlank))
.collect(Collectors.toUnmodifiableSet());
}
public boolean isPublic() {
return clientSecret == null;
}
}