Przeglądaj źródła

Polish JdbcRegisteredClientRepository

Issue gh-291
Joe Grandja 4 lat temu
rodzic
commit
1ae4f7aa13

+ 145 - 162
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepository.java

@@ -19,18 +19,16 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Timestamp;
 import java.sql.Types;
-import java.time.Duration;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
 
 import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.Module;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
@@ -38,24 +36,33 @@ import org.springframework.jdbc.core.JdbcOperations;
 import org.springframework.jdbc.core.PreparedStatementSetter;
 import org.springframework.jdbc.core.RowMapper;
 import org.springframework.jdbc.core.SqlParameterValue;
+import org.springframework.security.jackson2.SecurityJackson2Modules;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
-import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
-import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
+import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
 /**
- * JDBC-backed registered client repository
+ * A JDBC implementation of a {@link RegisteredClientRepository} that uses a
+ * {@link JdbcOperations} for {@link RegisteredClient} persistence.
+ *
+ * <p>
+ * <b>NOTE:</b> This {@code RegisteredClientRepository} depends on the table definition described in
+ * "classpath:org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql" and
+ * therefore MUST be defined in the database schema.
  *
  * @author Rafal Lewczuk
+ * @author Joe Grandja
  * @since 0.1.2
+ * @see RegisteredClientRepository
+ * @see RegisteredClient
+ * @see JdbcOperations
+ * @see RowMapper
  */
 public class JdbcRegisteredClientRepository implements RegisteredClientRepository {
 
-	private static final Map<String, AuthorizationGrantType> AUTHORIZATION_GRANT_TYPE_MAP;
-	private static final Map<String, ClientAuthenticationMethod> CLIENT_AUTHENTICATION_METHOD_MAP;
-
+	// @formatter:off
 	private static final String COLUMN_NAMES = "id, "
 			+ "client_id, "
 			+ "client_id_issued_at, "
@@ -68,20 +75,21 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
 			+ "scopes, "
 			+ "client_settings,"
 			+ "token_settings";
+	// @formatter:on
 
 	private static final String TABLE_NAME = "oauth2_registered_client";
 
 	private static final String LOAD_REGISTERED_CLIENT_SQL = "SELECT " + COLUMN_NAMES + " FROM " + TABLE_NAME + " WHERE ";
 
+	// @formatter:off
 	private static final String INSERT_REGISTERED_CLIENT_SQL = "INSERT INTO " + TABLE_NAME
-			+ "(" + COLUMN_NAMES + ") values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+			+ "(" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+	// @formatter:on
 
+	private final JdbcOperations jdbcOperations;
 	private RowMapper<RegisteredClient> registeredClientRowMapper;
-
 	private Function<RegisteredClient, List<SqlParameterValue>> registeredClientParametersMapper;
 
-	private final JdbcOperations jdbcOperations;
-
 	/**
 	 * Constructs a {@code JdbcRegisteredClientRepository} using the provided parameters.
 	 *
@@ -90,55 +98,21 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
 	public JdbcRegisteredClientRepository(JdbcOperations jdbcOperations) {
 		Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
 		this.jdbcOperations = jdbcOperations;
-		this.registeredClientRowMapper = new DefaultRegisteredClientRowMapper();
-		this.registeredClientParametersMapper = new DefaultRegisteredClientParametersMapper();
-	}
-
-	/**
-	 * Allows changing of {@link RegisteredClient} row mapper implementation
-	 *
-	 * @param registeredClientRowMapper mapper implementation
-	 */
-	public final void setRegisteredClientRowMapper(RowMapper<RegisteredClient> registeredClientRowMapper) {
-		Assert.notNull(registeredClientRowMapper, "registeredClientRowMapper cannot be null");
-		this.registeredClientRowMapper = registeredClientRowMapper;
-	}
-
-	/**
-	 * Allows changing of SQL parameter mapper for {@link RegisteredClient}
-	 *
-	 * @param registeredClientParametersMapper mapper implementation
-	 */
-	public final void setRegisteredClientParametersMapper(Function<RegisteredClient, List<SqlParameterValue>> registeredClientParametersMapper) {
-		Assert.notNull(registeredClientParametersMapper, "registeredClientParameterMapper cannot be null");
-		this.registeredClientParametersMapper = registeredClientParametersMapper;
-	}
-
-	protected final JdbcOperations getJdbcOperations() {
-		return this.jdbcOperations;
-	}
-
-	protected final RowMapper<RegisteredClient> getRegisteredClientRowMapper() {
-		return this.registeredClientRowMapper;
-	}
-
-	protected final Function<RegisteredClient, List<SqlParameterValue>> getRegisteredClientParametersMapper() {
-		return this.registeredClientParametersMapper;
+		this.registeredClientRowMapper = new RegisteredClientRowMapper();
+		this.registeredClientParametersMapper = new RegisteredClientParametersMapper();
 	}
 
 	@Override
 	public void save(RegisteredClient registeredClient) {
 		Assert.notNull(registeredClient, "registeredClient cannot be null");
-		RegisteredClient foundClient = findBy("id = ? OR client_id = ?",
+		RegisteredClient existingRegisteredClient = findBy("id = ? OR client_id = ?",
 				registeredClient.getId(), registeredClient.getClientId());
-
-		if (foundClient != null) {
-			Assert.isTrue(!foundClient.getId().equals(registeredClient.getId()),
+		if (existingRegisteredClient != null) {
+			Assert.isTrue(!existingRegisteredClient.getId().equals(registeredClient.getId()),
 					"Registered client must be unique. Found duplicate identifier: " + registeredClient.getId());
-			Assert.isTrue(!foundClient.getClientId().equals(registeredClient.getClientId()),
+			Assert.isTrue(!existingRegisteredClient.getClientId().equals(registeredClient.getClientId()),
 					"Registered client must be unique. Found duplicate client identifier: " + registeredClient.getClientId());
 		}
-
 		List<SqlParameterValue> parameters = this.registeredClientParametersMapper.apply(registeredClient);
 		PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
 		this.jdbcOperations.update(INSERT_REGISTERED_CLIENT_SQL, pss);
@@ -156,80 +130,95 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
 		return findBy("client_id = ?", clientId);
 	}
 
-	private RegisteredClient findBy(String condStr, Object... args) {
+	private RegisteredClient findBy(String filter, Object... args) {
 		List<RegisteredClient> result = this.jdbcOperations.query(
-				LOAD_REGISTERED_CLIENT_SQL + condStr,
-				this.registeredClientRowMapper, args);
+				LOAD_REGISTERED_CLIENT_SQL + filter, this.registeredClientRowMapper, args);
 		return !result.isEmpty() ? result.get(0) : null;
 	}
 
-	public static class DefaultRegisteredClientRowMapper implements RowMapper<RegisteredClient> {
+	/**
+	 * Sets the {@link RowMapper} used for mapping the current row in {@code java.sql.ResultSet} to {@link RegisteredClient}.
+	 * The default is {@link RegisteredClientRowMapper}.
+	 *
+	 * @param registeredClientRowMapper the {@link RowMapper} used for mapping the current row in {@code ResultSet} to {@link RegisteredClient}
+	 */
+	public final void setRegisteredClientRowMapper(RowMapper<RegisteredClient> registeredClientRowMapper) {
+		Assert.notNull(registeredClientRowMapper, "registeredClientRowMapper cannot be null");
+		this.registeredClientRowMapper = registeredClientRowMapper;
+	}
+
+	/**
+	 * Sets the {@code Function} used for mapping {@link RegisteredClient} to a {@code List} of {@link SqlParameterValue}.
+	 * The default is {@link RegisteredClientParametersMapper}.
+	 *
+	 * @param registeredClientParametersMapper the {@code Function} used for mapping {@link RegisteredClient} to a {@code List} of {@link SqlParameterValue}
+	 */
+	public final void setRegisteredClientParametersMapper(Function<RegisteredClient, List<SqlParameterValue>> registeredClientParametersMapper) {
+		Assert.notNull(registeredClientParametersMapper, "registeredClientParametersMapper cannot be null");
+		this.registeredClientParametersMapper = registeredClientParametersMapper;
+	}
+
+	protected final JdbcOperations getJdbcOperations() {
+		return this.jdbcOperations;
+	}
+
+	protected final RowMapper<RegisteredClient> getRegisteredClientRowMapper() {
+		return this.registeredClientRowMapper;
+	}
+
+	protected final Function<RegisteredClient, List<SqlParameterValue>> getRegisteredClientParametersMapper() {
+		return this.registeredClientParametersMapper;
+	}
 
+	/**
+	 * The default {@link RowMapper} that maps the current row in
+	 * {@code java.sql.ResultSet} to {@link RegisteredClient}.
+	 */
+	public static class RegisteredClientRowMapper implements RowMapper<RegisteredClient> {
 		private ObjectMapper objectMapper = new ObjectMapper();
 
+		public RegisteredClientRowMapper() {
+			ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader();
+			List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
+			this.objectMapper.registerModules(securityModules);
+			this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
+		}
+
 		@Override
 		public RegisteredClient mapRow(ResultSet rs, int rowNum) throws SQLException {
-			Set<String> clientScopes = parseList(rs.getString("scopes"));
-			Set<String> authGrantTypes = parseList(rs.getString("authorization_grant_types"));
-			Set<String> clientAuthMethods = parseList(rs.getString("client_authentication_methods"));
-			Set<String> redirectUris = parseList(rs.getString("redirect_uris"));
-			Timestamp clientIssuedAt = rs.getTimestamp("client_id_issued_at");
+			Timestamp clientIdIssuedAt = rs.getTimestamp("client_id_issued_at");
 			Timestamp clientSecretExpiresAt = rs.getTimestamp("client_secret_expires_at");
-			String clientSecret = rs.getString("client_secret");
-			RegisteredClient.Builder builder = RegisteredClient
-					.withId(rs.getString("id"))
+			Set<String> clientAuthenticationMethods = StringUtils.commaDelimitedListToSet(rs.getString("client_authentication_methods"));
+			Set<String> authorizationGrantTypes = StringUtils.commaDelimitedListToSet(rs.getString("authorization_grant_types"));
+			Set<String> redirectUris = StringUtils.commaDelimitedListToSet(rs.getString("redirect_uris"));
+			Set<String> clientScopes = StringUtils.commaDelimitedListToSet(rs.getString("scopes"));
+
+			// @formatter:off
+			RegisteredClient.Builder builder = RegisteredClient.withId(rs.getString("id"))
 					.clientId(rs.getString("client_id"))
-					.clientIdIssuedAt(clientIssuedAt != null ? clientIssuedAt.toInstant() : null)
-					.clientSecret(clientSecret)
+					.clientIdIssuedAt(clientIdIssuedAt != null ? clientIdIssuedAt.toInstant() : null)
+					.clientSecret(rs.getString("client_secret"))
 					.clientSecretExpiresAt(clientSecretExpiresAt != null ? clientSecretExpiresAt.toInstant() : null)
 					.clientName(rs.getString("client_name"))
-					.authorizationGrantTypes((grantTypes) -> authGrantTypes.forEach(authGrantType ->
-							grantTypes.add(AUTHORIZATION_GRANT_TYPE_MAP.get(authGrantType))))
-					.clientAuthenticationMethods((authenticationMethods) -> clientAuthMethods.forEach(clientAuthMethod ->
-							authenticationMethods.add(CLIENT_AUTHENTICATION_METHOD_MAP.get(clientAuthMethod))))
+					.clientAuthenticationMethods((authenticationMethods) ->
+							clientAuthenticationMethods.forEach(authenticationMethod ->
+									authenticationMethods.add(resolveClientAuthenticationMethod(authenticationMethod))))
+					.authorizationGrantTypes((grantTypes) ->
+							authorizationGrantTypes.forEach(grantType ->
+									grantTypes.add(resolveAuthorizationGrantType(grantType))))
 					.redirectUris((uris) -> uris.addAll(redirectUris))
 					.scopes((scopes) -> scopes.addAll(clientScopes));
+			// @formatter:on
 
-			RegisteredClient registeredClient = builder.build();
-
-			String tokenSettingsJson = rs.getString("token_settings");
-			if (tokenSettingsJson != null) {
-				Map<String, Object> settings = parseMap(tokenSettingsJson);
-				TokenSettings tokenSettings = registeredClient.getTokenSettings();
-
-				Number accessTokenTTL = (Number) settings.get("access_token_ttl");
-				if (accessTokenTTL != null) {
-					tokenSettings.accessTokenTimeToLive(Duration.ofMillis(accessTokenTTL.longValue()));
-				}
-
-				Number refreshTokenTTL = (Number) settings.get("refresh_token_ttl");
-				if (refreshTokenTTL != null) {
-					tokenSettings.refreshTokenTimeToLive(Duration.ofMillis(refreshTokenTTL.longValue()));
-				}
-
-				Boolean reuseRefreshTokens = (Boolean) settings.get("reuse_refresh_tokens");
-				if (reuseRefreshTokens != null) {
-					tokenSettings.reuseRefreshTokens(reuseRefreshTokens);
-				}
-			}
+			Map<String, Object> clientSettingsMap = parseMap(rs.getString("client_settings"));
+			builder.clientSettings(clientSettings ->
+					clientSettings.settings().putAll(clientSettingsMap));
 
-			String clientSettingsJson = rs.getString("client_settings");
-			if (clientSettingsJson != null) {
-				Map<String, Object> settings = parseMap(clientSettingsJson);
-				ClientSettings clientSettings = registeredClient.getClientSettings();
+			Map<String, Object> tokenSettingsMap = parseMap(rs.getString("token_settings"));
+			builder.tokenSettings(tokenSettings ->
+					tokenSettings.settings().putAll(tokenSettingsMap));
 
-				Boolean requireProofKey = (Boolean) settings.get("require_proof_key");
-				if (requireProofKey != null) {
-					clientSettings.requireProofKey(requireProofKey);
-				}
-
-				Boolean requireUserConsent = (Boolean) settings.get("require_user_consent");
-				if (requireUserConsent != null) {
-					clientSettings.requireUserConsent(requireUserConsent);
-				}
-			}
-
-			return registeredClient;
+			return builder.build();
 		}
 
 		public final void setObjectMapper(ObjectMapper objectMapper) {
@@ -241,10 +230,6 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
 			return this.objectMapper;
 		}
 
-		private Set<String> parseList(String s) {
-			return s != null ? StringUtils.commaDelimitedListToSet(s) : Collections.emptySet();
-		}
-
 		private Map<String, Object> parseMap(String data) {
 			try {
 				return this.objectMapper.readValue(data, new TypeReference<Map<String, Object>>() {});
@@ -253,54 +238,73 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
 			}
 		}
 
-	}
+		private static AuthorizationGrantType resolveAuthorizationGrantType(String authorizationGrantType) {
+			if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) {
+				return AuthorizationGrantType.AUTHORIZATION_CODE;
+			} else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(authorizationGrantType)) {
+				return AuthorizationGrantType.CLIENT_CREDENTIALS;
+			} else if (AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(authorizationGrantType)) {
+				return AuthorizationGrantType.REFRESH_TOKEN;
+			}
+			return new AuthorizationGrantType(authorizationGrantType);		// Custom authorization grant type
+		}
+
+		private static ClientAuthenticationMethod resolveClientAuthenticationMethod(String clientAuthenticationMethod) {
+			if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue().equals(clientAuthenticationMethod)) {
+				return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
+			} else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientAuthenticationMethod)) {
+				return ClientAuthenticationMethod.CLIENT_SECRET_POST;
+			} else if (ClientAuthenticationMethod.NONE.getValue().equals(clientAuthenticationMethod)) {
+				return ClientAuthenticationMethod.NONE;
+			}
+			return new ClientAuthenticationMethod(clientAuthenticationMethod);		// Custom client authentication method
+		}
 
-	public static class DefaultRegisteredClientParametersMapper implements Function<RegisteredClient, List<SqlParameterValue>> {
+	}
 
+	/**
+	 * The default {@code Function} that maps {@link RegisteredClient} to a
+	 * {@code List} of {@link SqlParameterValue}.
+	 */
+	public static class RegisteredClientParametersMapper implements Function<RegisteredClient, List<SqlParameterValue>> {
 		private ObjectMapper objectMapper = new ObjectMapper();
 
+		public RegisteredClientParametersMapper() {
+			ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader();
+			List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
+			this.objectMapper.registerModules(securityModules);
+			this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
+		}
+
 		@Override
 		public List<SqlParameterValue> apply(RegisteredClient registeredClient) {
-			List<String> clientAuthenticationMethodNames = new ArrayList<>(registeredClient.getClientAuthenticationMethods().size());
-			for (ClientAuthenticationMethod clientAuthenticationMethod : registeredClient.getClientAuthenticationMethods()) {
-				clientAuthenticationMethodNames.add(clientAuthenticationMethod.getValue());
-			}
-
-			List<String> authorizationGrantTypeNames = new ArrayList<>(registeredClient.getAuthorizationGrantTypes().size());
-			for (AuthorizationGrantType authorizationGrantType : registeredClient.getAuthorizationGrantTypes()) {
-				authorizationGrantTypeNames.add(authorizationGrantType.getValue());
-			}
-
-			Instant issuedAt = registeredClient.getClientIdIssuedAt() != null ?
-					registeredClient.getClientIdIssuedAt() : Instant.now();
+			Timestamp clientIdIssuedAt = registeredClient.getClientIdIssuedAt() != null ?
+					Timestamp.from(registeredClient.getClientIdIssuedAt()) : Timestamp.from(Instant.now());
 
 			Timestamp clientSecretExpiresAt = registeredClient.getClientSecretExpiresAt() != null ?
 					Timestamp.from(registeredClient.getClientSecretExpiresAt()) : null;
 
-			Map<String, Object> clientSettings = new HashMap<>();
-			clientSettings.put("require_proof_key", registeredClient.getClientSettings().requireProofKey());
-			clientSettings.put("require_user_consent", registeredClient.getClientSettings().requireUserConsent());
-			String clientSettingsJson = writeMap(clientSettings);
+			List<String> clientAuthenticationMethods = new ArrayList<>(registeredClient.getClientAuthenticationMethods().size());
+			registeredClient.getClientAuthenticationMethods().forEach(clientAuthenticationMethod ->
+					clientAuthenticationMethods.add(clientAuthenticationMethod.getValue()));
 
-			Map<String, Object> tokenSettings = new HashMap<>();
-			tokenSettings.put("access_token_ttl", registeredClient.getTokenSettings().accessTokenTimeToLive().toMillis());
-			tokenSettings.put("reuse_refresh_tokens", registeredClient.getTokenSettings().reuseRefreshTokens());
-			tokenSettings.put("refresh_token_ttl", registeredClient.getTokenSettings().refreshTokenTimeToLive().toMillis());
-			String tokenSettingsJson = writeMap(tokenSettings);
+			List<String> authorizationGrantTypes = new ArrayList<>(registeredClient.getAuthorizationGrantTypes().size());
+			registeredClient.getAuthorizationGrantTypes().forEach(authorizationGrantType ->
+					authorizationGrantTypes.add(authorizationGrantType.getValue()));
 
 			return Arrays.asList(
 					new SqlParameterValue(Types.VARCHAR, registeredClient.getId()),
 					new SqlParameterValue(Types.VARCHAR, registeredClient.getClientId()),
-					new SqlParameterValue(Types.TIMESTAMP, Timestamp.from(issuedAt)),
+					new SqlParameterValue(Types.TIMESTAMP, clientIdIssuedAt),
 					new SqlParameterValue(Types.VARCHAR, registeredClient.getClientSecret()),
 					new SqlParameterValue(Types.TIMESTAMP, clientSecretExpiresAt),
 					new SqlParameterValue(Types.VARCHAR, registeredClient.getClientName()),
-					new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(clientAuthenticationMethodNames)),
-					new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(authorizationGrantTypeNames)),
+					new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(clientAuthenticationMethods)),
+					new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(authorizationGrantTypes)),
 					new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(registeredClient.getRedirectUris())),
 					new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(registeredClient.getScopes())),
-					new SqlParameterValue(Types.VARCHAR, clientSettingsJson),
-					new SqlParameterValue(Types.VARCHAR, tokenSettingsJson));
+					new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getClientSettings().settings())),
+					new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getTokenSettings().settings())));
 		}
 
 		public final void setObjectMapper(ObjectMapper objectMapper) {
@@ -322,25 +326,4 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
 
 	}
 
-	static {
-		Map<String, AuthorizationGrantType> am = new HashMap<>();
-		for (AuthorizationGrantType a : Arrays.asList(
-				AuthorizationGrantType.AUTHORIZATION_CODE,
-				AuthorizationGrantType.REFRESH_TOKEN,
-				AuthorizationGrantType.CLIENT_CREDENTIALS,
-				AuthorizationGrantType.PASSWORD,
-				AuthorizationGrantType.IMPLICIT)) {
-			am.put(a.getValue(), a);
-		}
-		AUTHORIZATION_GRANT_TYPE_MAP = Collections.unmodifiableMap(am);
-
-		Map<String, ClientAuthenticationMethod> cm = new HashMap<>();
-		for (ClientAuthenticationMethod c : Arrays.asList(
-				ClientAuthenticationMethod.NONE,
-				ClientAuthenticationMethod.BASIC,
-				ClientAuthenticationMethod.POST)) {
-			cm.put(c.getValue(), c);
-		}
-		CLIENT_AUTHENTICATION_METHOD_MAP = Collections.unmodifiableMap(cm);
-	}
 }

+ 49 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/DurationMixin.java

@@ -0,0 +1,49 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.oauth2.server.authorization.jackson2;
+
+import java.time.Duration;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+/**
+ * This mixin class is used to serialize/deserialize {@link Duration}.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ * @see Duration
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE,
+		isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE,
+		creatorVisibility = JsonAutoDetect.Visibility.NONE)
+abstract class DurationMixin {
+
+	@JsonCreator
+	static void ofSeconds(@JsonProperty("seconds") long seconds, @JsonProperty("nano") long nanoAdjustment) {
+	}
+
+	@JsonGetter("seconds")
+	abstract long getSeconds();
+
+	@JsonGetter("nano")
+	abstract int getNano();
+
+}

+ 8 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java

@@ -15,6 +15,7 @@
  */
 package org.springframework.security.oauth2.server.authorization.jackson2;
 
+import java.time.Duration;
 import java.util.Collections;
 import java.util.HashSet;
 
@@ -23,6 +24,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule;
 
 import org.springframework.security.jackson2.SecurityJackson2Modules;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 
 /**
  * Jackson {@code Module} for {@code spring-authorization-server}, that registers the
@@ -32,6 +34,8 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ
  * <li>{@link UnmodifiableMapMixin}</li>
  * <li>{@link HashSetMixin}</li>
  * <li>{@link OAuth2AuthorizationRequestMixin}</li>
+ * <li>{@link DurationMixin}</li>
+ * <li>{@link SignatureAlgorithmMixin}</li>
  * </ul>
  *
  * If not already enabled, default typing will be automatically enabled as type info is
@@ -52,6 +56,8 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ
  * @see UnmodifiableMapMixin
  * @see HashSetMixin
  * @see OAuth2AuthorizationRequestMixin
+ * @see DurationMixin
+ * @see SignatureAlgorithmMixin
  */
 public class OAuth2AuthorizationServerJackson2Module extends SimpleModule {
 
@@ -66,6 +72,8 @@ public class OAuth2AuthorizationServerJackson2Module extends SimpleModule {
 				UnmodifiableMapMixin.class);
 		context.setMixInAnnotations(HashSet.class, HashSetMixin.class);
 		context.setMixInAnnotations(OAuth2AuthorizationRequest.class, OAuth2AuthorizationRequestMixin.class);
+		context.setMixInAnnotations(Duration.class, DurationMixin.class);
+		context.setMixInAnnotations(SignatureAlgorithm.class, SignatureAlgorithmMixin.class);
 	}
 
 }

+ 34 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/SignatureAlgorithmMixin.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.oauth2.server.authorization.jackson2;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+
+/**
+ * This mixin class is used to serialize/deserialize {@link SignatureAlgorithm}.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ * @see SignatureAlgorithm
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
+		isGetterVisibility = JsonAutoDetect.Visibility.NONE)
+abstract class SignatureAlgorithmMixin {
+}

+ 4 - 4
oauth2-authorization-server/src/main/resources/org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql

@@ -4,12 +4,12 @@ CREATE TABLE oauth2_registered_client (
     client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
     client_secret varchar(200) DEFAULT NULL,
     client_secret_expires_at timestamp DEFAULT NULL,
-    client_name varchar(200),
+    client_name varchar(200) NOT NULL,
     client_authentication_methods varchar(1000) NOT NULL,
     authorization_grant_types varchar(1000) NOT NULL,
-    redirect_uris varchar(1000) NOT NULL,
+    redirect_uris varchar(1000) DEFAULT NULL,
     scopes varchar(1000) NOT NULL,
-    client_settings varchar(1000) DEFAULT NULL,
-    token_settings varchar(1000) DEFAULT NULL,
+    client_settings varchar(2000) NOT NULL,
+    token_settings varchar(2000) NOT NULL,
     PRIMARY KEY (id)
 );

+ 145 - 229
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepositoryTests.java

@@ -18,16 +18,13 @@ package org.springframework.security.oauth2.server.authorization.client;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Timestamp;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
 
 import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.Module;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.junit.After;
 import org.junit.Before;
@@ -39,57 +36,50 @@ import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.jdbc.core.PreparedStatementSetter;
 import org.springframework.jdbc.core.RowMapper;
 import org.springframework.jdbc.core.SqlParameterValue;
-import org.springframework.jdbc.datasource.DriverManagerDataSource;
 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
+import org.springframework.security.jackson2.SecurityJackson2Modules;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
 import org.springframework.util.StringUtils;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 
 /**
- * JDBC-backed registered client repository tests
+ * Tests for {@link JdbcRegisteredClientRepository}.
  *
  * @author Rafal Lewczuk
  * @author Steve Riesenberg
- * @since 0.1.2
+ * @author Joe Grandja
  */
 public class JdbcRegisteredClientRepositoryTests {
-
-	private static final String REGISTERED_CLIENT_SCHEMA_SQL_RESOURCE = "/org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql";
-	private static final String CUSTOM_REGISTERED_CLIENT_SCHEMA_SQL_RESOURCE = "/org/springframework/security/oauth2/server/authorization/client/custom-oauth2-registered-client-schema.sql";
-
-	private DriverManagerDataSource dataSource;
-
-	private JdbcRegisteredClientRepository registeredClientRepository;
-
-	private RegisteredClient registeredClient;
-
+	private static final String OAUTH2_REGISTERED_CLIENT_SCHEMA_SQL_RESOURCE = "/org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql";
+	private static final String OAUTH2_CUSTOM_REGISTERED_CLIENT_SCHEMA_SQL_RESOURCE = "/org/springframework/security/oauth2/server/authorization/client/custom-oauth2-registered-client-schema.sql";
 	private EmbeddedDatabase db;
-
 	private JdbcOperations jdbcOperations;
+	private JdbcRegisteredClientRepository registeredClientRepository;
 
 	@Before
-	public void setup() throws Exception {
-		this.db = createDb(REGISTERED_CLIENT_SCHEMA_SQL_RESOURCE);
+	public void setUp() {
+		this.db = createDb(OAUTH2_REGISTERED_CLIENT_SCHEMA_SQL_RESOURCE);
 		this.jdbcOperations = new JdbcTemplate(this.db);
-
 		this.registeredClientRepository = new JdbcRegisteredClientRepository(this.jdbcOperations);
-		this.registeredClient = TestRegisteredClients.registeredClient().build();
-
-		this.registeredClientRepository.save(this.registeredClient);
 	}
 
 	@After
-	public void destroyDatabase() {
+	public void tearDown() {
 		this.db.shutdown();
 	}
 
 	@Test
-	public void whenJdbcOperationsNullThenThrow() {
+	public void constructorWhenJdbcOperationsIsNullThenThrowIllegalArgumentException() {
 		// @formatter:off
 		assertThatIllegalArgumentException()
 				.isThrownBy(() -> new JdbcRegisteredClientRepository(null))
@@ -98,7 +88,7 @@ public class JdbcRegisteredClientRepositoryTests {
 	}
 
 	@Test
-	public void whenSetNullRegisteredClientRowMapperThenThrow() {
+	public void setRegisteredClientRowMapperWhenNullThenThrowIllegalArgumentException() {
 		// @formatter:off
 		assertThatIllegalArgumentException()
 				.isThrownBy(() -> this.registeredClientRepository.setRegisteredClientRowMapper(null))
@@ -107,58 +97,16 @@ public class JdbcRegisteredClientRepositoryTests {
 	}
 
 	@Test
-	public void whenSetNullRegisteredClientParameterMapperThenThrow() {
+	public void setRegisteredClientParametersMapperWhenNullThenThrowIllegalArgumentException() {
 		// @formatter:off
 		assertThatIllegalArgumentException()
 				.isThrownBy(() -> this.registeredClientRepository.setRegisteredClientParametersMapper(null))
-				.withMessage("registeredClientParameterMapper cannot be null");
-		// @formatter:on
-	}
-
-	@Test
-	public void findByIdWhenFoundThenFound() {
-		String id = this.registeredClient.getId();
-		assertRegisteredClientIsEqualTo(this.registeredClientRepository.findById(id), this.registeredClient);
-	}
-
-	@Test
-	public void findByIdWhenNotFoundThenNull() {
-		RegisteredClient client = this.registeredClientRepository.findById("noooope");
-		assertThat(client).isNull();
-	}
-
-	@Test
-	public void findByIdWhenNullThenThrowIllegalArgumentException() {
-		// @formatter:off
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> this.registeredClientRepository.findById(null))
-				.withMessage("id cannot be empty");
+				.withMessage("registeredClientParametersMapper cannot be null");
 		// @formatter:on
 	}
 
 	@Test
-	public void findByClientIdWhenFoundThenFound() {
-		String id = this.registeredClient.getClientId();
-		assertRegisteredClientIsEqualTo(this.registeredClientRepository.findByClientId(id), this.registeredClient);
-	}
-
-	@Test
-	public void findByClientIdWhenNotFoundThenNull() {
-		RegisteredClient client = this.registeredClientRepository.findByClientId("noooope");
-		assertThat(client).isNull();
-	}
-
-	@Test
-	public void findByClientIdWhenNullThenThrowIllegalArgumentException() {
-		// @formatter:off
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> this.registeredClientRepository.findByClientId(null))
-				.withMessage("clientId cannot be empty");
-		// @formatter:on
-	}
-
-	@Test
-	public void saveWhenNullThenThrowIllegalArgumentException() {
+	public void saveWhenRegisteredClientNullThenThrowIllegalArgumentException() {
 		assertThatIllegalArgumentException()
 				.isThrownBy(() -> this.registeredClientRepository.save(null))
 				.withMessageContaining("registeredClient cannot be null");
@@ -166,8 +114,9 @@ public class JdbcRegisteredClientRepositoryTests {
 
 	@Test
 	public void saveWhenExistingIdThenThrowIllegalArgumentException() {
-		RegisteredClient registeredClient = createRegisteredClient(
-				this.registeredClient.getId(), "client-id-2", "client-secret-2");
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		this.registeredClientRepository.save(registeredClient);
+
 		assertThatIllegalArgumentException()
 				.isThrownBy(() -> this.registeredClientRepository.save(registeredClient))
 				.withMessage("Registered client must be unique. Found duplicate identifier: " + registeredClient.getId());
@@ -175,132 +124,102 @@ public class JdbcRegisteredClientRepositoryTests {
 
 	@Test
 	public void saveWhenExistingClientIdThenThrowIllegalArgumentException() {
-		RegisteredClient registeredClient = createRegisteredClient(
-				"client-2", this.registeredClient.getClientId(), "client-secret-2");
+		RegisteredClient existingRegisteredClient = TestRegisteredClients.registeredClient().build();
+		this.registeredClientRepository.save(existingRegisteredClient);
+		RegisteredClient registeredClient = RegisteredClient.from(existingRegisteredClient)
+				.id("registration-2")
+				.build();
+
 		assertThatIllegalArgumentException()
 				.isThrownBy(() -> this.registeredClientRepository.save(registeredClient))
 				.withMessage("Registered client must be unique. Found duplicate client identifier: " + registeredClient.getClientId());
 	}
 
 	@Test
-	public void saveWhenExistingClientSecretThenSuccess() {
-		RegisteredClient registeredClient = createRegisteredClient(
-				"client-2", "client-id-2", this.registeredClient.getClientSecret());
-		this.registeredClientRepository.save(registeredClient);
-		RegisteredClient savedClient = this.registeredClientRepository.findById(registeredClient.getId());
-		assertRegisteredClientIsEqualTo(savedClient, registeredClient);
+	public void saveWhenNewThenSaved() {
+		RegisteredClient expectedRegisteredClient = TestRegisteredClients.registeredClient().build();
+		this.registeredClientRepository.save(expectedRegisteredClient);
+		RegisteredClient registeredClient = this.registeredClientRepository.findById(expectedRegisteredClient.getId());
+		assertThat(registeredClient).isEqualTo(expectedRegisteredClient);
 	}
 
 	@Test
-	public void saveWhenSavedAndFindByIdThenFound() {
-		RegisteredClient registeredClient = createRegisteredClient();
+	public void saveLoadRegisteredClientWhenCustomStrategiesSetThenCalled() throws Exception {
+		RowMapper<RegisteredClient> registeredClientRowMapper = spy(
+				new JdbcRegisteredClientRepository.RegisteredClientRowMapper());
+		this.registeredClientRepository.setRegisteredClientRowMapper(registeredClientRowMapper);
+		Function<RegisteredClient, List<SqlParameterValue>> registeredClientParametersMapper = spy(
+				new JdbcRegisteredClientRepository.RegisteredClientParametersMapper());
+		this.registeredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper);
+
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		this.registeredClientRepository.save(registeredClient);
-		RegisteredClient savedClient = this.registeredClientRepository.findById(registeredClient.getId());
-		assertRegisteredClientIsEqualTo(savedClient, registeredClient);
+		RegisteredClient result = this.registeredClientRepository.findById(registeredClient.getId());
+		assertThat(result).isEqualTo(registeredClient);
+		verify(registeredClientRowMapper).mapRow(any(), anyInt());
+		verify(registeredClientParametersMapper).apply(any());
 	}
 
 	@Test
-	public void saveWhenSavedAndFindByClientIdThenFound() {
-		RegisteredClient registeredClient = createRegisteredClient();
-		this.registeredClientRepository.save(registeredClient);
-		RegisteredClient savedClient = this.registeredClientRepository.findByClientId(registeredClient.getClientId());
-		assertRegisteredClientIsEqualTo(savedClient, registeredClient);
+	public void findByIdWhenIdNullThenThrowIllegalArgumentException() {
+		// @formatter:off
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> this.registeredClientRepository.findById(null))
+				.withMessage("id cannot be empty");
+		// @formatter:on
 	}
 
 	@Test
-	public void saveWhenPublicClientSavedAndFindByClientIdThenFound() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient().build();
+	public void findByIdWhenExistsThenFound() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		this.registeredClientRepository.save(registeredClient);
-		RegisteredClient savedClient = this.registeredClientRepository.findByClientId(registeredClient.getClientId());
-		assertRegisteredClientIsEqualTo(savedClient, registeredClient);
+		RegisteredClient result = this.registeredClientRepository.findById(registeredClient.getId());
+		assertThat(result).isEqualTo(registeredClient);
 	}
 
 	@Test
-	public void saveWhenMultiplePublicClientsSavedAndFindByIdThenFound() {
-		RegisteredClient registeredClient1 = TestRegisteredClients.registeredPublicClient()
-				.id("1").clientId("a").build();
-		RegisteredClient registeredClient2 = TestRegisteredClients.registeredPublicClient()
-				.id("2").clientId("b").build();
-		this.registeredClientRepository.save(registeredClient1);
-		this.registeredClientRepository.save(registeredClient2);
-		RegisteredClient savedClient = this.registeredClientRepository.findByClientId(registeredClient2.getClientId());
-		assertRegisteredClientIsEqualTo(savedClient, registeredClient2);
+	public void findByIdWhenNotExistsThenNotFound() {
+		RegisteredClient result = this.registeredClientRepository.findById("not-exists");
+		assertThat(result).isNull();
 	}
 
 	@Test
-	public void whenSaveRegistrationWithAllAttrsThenSaved() {
-		Instant issuedAt = Instant.now(), expiresAt = issuedAt.plus(Duration.ofDays(30));
-		RegisteredClient client = TestRegisteredClients.registeredClient2()
-				.clientIdIssuedAt(issuedAt)
-				.clientSecretExpiresAt(expiresAt)
-				.clientSecret("secret2")
-				.clientName("some_client_name")
-				.redirectUri("https://example2.com")
-				.clientSettings(cs -> {
-					cs.requireProofKey(true);
-					cs.requireUserConsent(true);
-				})
-				.tokenSettings(ts -> {
-					ts.accessTokenTimeToLive(Duration.ofMinutes(3));
-					ts.reuseRefreshTokens(true);
-					ts.refreshTokenTimeToLive(Duration.ofMinutes(300));
-				})
-				.build();
-
-		this.registeredClientRepository.save(client);
+	public void findByClientIdWhenClientIdNullThenThrowIllegalArgumentException() {
+		// @formatter:off
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> this.registeredClientRepository.findByClientId(null))
+				.withMessage("clientId cannot be empty");
+		// @formatter:on
+	}
 
-		RegisteredClient retrievedClient = this.registeredClientRepository.findById(client.getId());
+	@Test
+	public void findByClientIdWhenExistsThenFound() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		this.registeredClientRepository.save(registeredClient);
+		RegisteredClient result = this.registeredClientRepository.findByClientId(registeredClient.getClientId());
+		assertThat(result).isEqualTo(registeredClient);
+	}
 
-		assertRegisteredClientIsEqualTo(retrievedClient, client);
+	@Test
+	public void findByClientIdWhenNotExistsThenNotFound() {
+		RegisteredClient result = this.registeredClientRepository.findByClientId("not-exists");
+		assertThat(result).isNull();
 	}
 
 	@Test
 	public void tableDefinitionWhenCustomThenAbleToOverride() {
-		EmbeddedDatabase db = createDb(CUSTOM_REGISTERED_CLIENT_SCHEMA_SQL_RESOURCE);
-		CustomJdbcRegisteredClientRepository registeredClientRepository =
+		EmbeddedDatabase db = createDb(OAUTH2_CUSTOM_REGISTERED_CLIENT_SCHEMA_SQL_RESOURCE);
+		RegisteredClientRepository registeredClientRepository =
 				new CustomJdbcRegisteredClientRepository(new JdbcTemplate(db));
-		registeredClientRepository.save(this.registeredClient);
-		RegisteredClient foundClient1 = registeredClientRepository.findById(this.registeredClient.getId());
-		assertThat(foundClient1).isNotNull();
-		assertRegisteredClientIsEqualTo(foundClient1, this.registeredClient);
-		RegisteredClient foundClient2 = registeredClientRepository.findByClientId(this.registeredClient.getClientId());
-		assertThat(foundClient2).isNotNull();
-		assertRegisteredClientIsEqualTo(foundClient2, this.registeredClient);
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		registeredClientRepository.save(registeredClient);
+		RegisteredClient foundRegisteredClient1 = registeredClientRepository.findById(registeredClient.getId());
+		assertThat(foundRegisteredClient1).isEqualTo(registeredClient);
+		RegisteredClient foundRegisteredClient2 = registeredClientRepository.findByClientId(registeredClient.getClientId());
+		assertThat(foundRegisteredClient2).isEqualTo(registeredClient);
 		db.shutdown();
 	}
 
-	private void assertRegisteredClientIsEqualTo(RegisteredClient rc, RegisteredClient ref) {
-		assertThat(rc).isNotNull();
-		assertThat(rc.getId()).isEqualTo(ref.getId());
-		assertThat(rc.getClientId()).isEqualTo(ref.getClientId());
-
-		if (ref.getClientIdIssuedAt() != null) {
-			// This can be set to default value
-			Instant inst = ref.getClientIdIssuedAt();
-			assertThat(rc.getClientIdIssuedAt()).isBetween(inst.minusMillis(1), inst.plusMillis(1));
-		}
-
-		assertThat(rc.getClientSecret()).isEqualTo(ref.getClientSecret());
-
-		if (ref.getClientSecretExpiresAt() != null) {
-			Instant inst = ref.getClientSecretExpiresAt();
-			assertThat(rc.getClientSecretExpiresAt()).isBetween(inst.minusMillis(1), inst.plusMillis(1));
-		} else {
-			assertThat(rc.getClientSecretExpiresAt()).isNull();
-		}
-
-		assertThat(rc.getClientName()).isEqualTo(ref.getClientName());
-		assertThat(rc.getClientAuthenticationMethods()).isEqualTo(ref.getClientAuthenticationMethods());
-		assertThat(rc.getAuthorizationGrantTypes()).isEqualTo(ref.getAuthorizationGrantTypes());
-		assertThat(rc.getRedirectUris()).isEqualTo(ref.getRedirectUris());
-		assertThat(rc.getScopes()).isEqualTo(ref.getScopes());
-		assertThat(rc.getClientSettings().requireUserConsent()).isEqualTo(ref.getClientSettings().requireUserConsent());
-		assertThat(rc.getClientSettings().requireProofKey()).isEqualTo(ref.getClientSettings().requireProofKey());
-		assertThat(rc.getTokenSettings().reuseRefreshTokens()).isEqualTo(ref.getTokenSettings().reuseRefreshTokens());
-		assertThat(rc.getTokenSettings().accessTokenTimeToLive()).isEqualTo(ref.getTokenSettings().accessTokenTimeToLive());
-		assertThat(rc.getTokenSettings().refreshTokenTimeToLive()).isEqualTo(ref.getTokenSettings().refreshTokenTimeToLive());
-	}
-
 	private static EmbeddedDatabase createDb(String schema) {
 		// @formatter:off
 		return new EmbeddedDatabaseBuilder()
@@ -312,25 +231,9 @@ public class JdbcRegisteredClientRepositoryTests {
 		// @formatter:on
 	}
 
-	private static RegisteredClient createRegisteredClient() {
-		return createRegisteredClient("client-2", "client-id-2", "client-secret-2");
-	}
-
-	private static RegisteredClient createRegisteredClient(String id, String clientId, String clientSecret) {
-		// @formatter:off
-		return RegisteredClient.withId(id)
-				.clientId(clientId)
-				.clientSecret(clientSecret)
-				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
-				.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
-				.redirectUri("https://client.example.com")
-				.scope("scope1")
-				.build();
-		// @formatter:on
-	}
-
 	private static final class CustomJdbcRegisteredClientRepository extends JdbcRegisteredClientRepository {
 
+		// @formatter:off
 		private static final String COLUMN_NAMES = "id, "
 				+ "clientId, "
 				+ "clientIdIssuedAt, "
@@ -343,15 +246,18 @@ public class JdbcRegisteredClientRepositoryTests {
 				+ "scopes, "
 				+ "clientSettings,"
 				+ "tokenSettings";
+		// @formatter:on
 
 		private static final String TABLE_NAME = "oauth2RegisteredClient";
 
 		private static final String LOAD_REGISTERED_CLIENT_SQL = "SELECT " + COLUMN_NAMES + " FROM " + TABLE_NAME + " WHERE ";
 
+		// @formatter:off
 		private static final String INSERT_REGISTERED_CLIENT_SQL = "INSERT INTO " + TABLE_NAME
-				+ " (" + COLUMN_NAMES + ") values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+				+ " (" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+		// @formatter:on
 
-		CustomJdbcRegisteredClientRepository(JdbcOperations jdbcOperations) {
+		private CustomJdbcRegisteredClientRepository(JdbcOperations jdbcOperations) {
 			super(jdbcOperations);
 			setRegisteredClientRowMapper(new CustomRegisteredClientRowMapper());
 		}
@@ -374,46 +280,56 @@ public class JdbcRegisteredClientRepositoryTests {
 		}
 
 		private RegisteredClient findBy(String filter, Object... args) {
-			List<RegisteredClient> result = getJdbcOperations()
-					.query(LOAD_REGISTERED_CLIENT_SQL + filter, getRegisteredClientRowMapper(), args);
+			List<RegisteredClient> result = getJdbcOperations().query(
+					LOAD_REGISTERED_CLIENT_SQL + filter, getRegisteredClientRowMapper(), args);
 			return !result.isEmpty() ? result.get(0) : null;
 		}
 
 		private static final class CustomRegisteredClientRowMapper implements RowMapper<RegisteredClient> {
-
-			private static final Map<String, AuthorizationGrantType> AUTHORIZATION_GRANT_TYPE_MAP;
-			private static final Map<String, ClientAuthenticationMethod> CLIENT_AUTHENTICATION_METHOD_MAP;
-
 			private final ObjectMapper objectMapper = new ObjectMapper();
 
+			private CustomRegisteredClientRowMapper() {
+				ClassLoader classLoader = CustomJdbcRegisteredClientRepository.class.getClassLoader();
+				List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
+				this.objectMapper.registerModules(securityModules);
+				this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
+			}
+
 			@Override
 			public RegisteredClient mapRow(ResultSet rs, int rowNum) throws SQLException {
-				Set<String> clientScopes = StringUtils.commaDelimitedListToSet(rs.getString("scopes"));
-				Set<String> authGrantTypes = StringUtils.commaDelimitedListToSet(rs.getString("authorizationGrantTypes"));
-				Set<String> clientAuthMethods = StringUtils.commaDelimitedListToSet(rs.getString("clientAuthenticationMethods"));
-				Set<String> redirectUris = StringUtils.commaDelimitedListToSet(rs.getString("redirectUris"));
-				Timestamp clientIssuedAt = rs.getTimestamp("clientIdIssuedAt");
+				Timestamp clientIdIssuedAt = rs.getTimestamp("clientIdIssuedAt");
 				Timestamp clientSecretExpiresAt = rs.getTimestamp("clientSecretExpiresAt");
-				String clientSecret = rs.getString("clientSecret");
-				RegisteredClient.Builder builder = RegisteredClient
-						.withId(rs.getString("id"))
+				Set<String> clientAuthenticationMethods = StringUtils.commaDelimitedListToSet(rs.getString("clientAuthenticationMethods"));
+				Set<String> authorizationGrantTypes = StringUtils.commaDelimitedListToSet(rs.getString("authorizationGrantTypes"));
+				Set<String> redirectUris = StringUtils.commaDelimitedListToSet(rs.getString("redirectUris"));
+				Set<String> clientScopes = StringUtils.commaDelimitedListToSet(rs.getString("scopes"));
+
+				// @formatter:off
+				RegisteredClient.Builder builder = RegisteredClient.withId(rs.getString("id"))
 						.clientId(rs.getString("clientId"))
-						.clientIdIssuedAt(clientIssuedAt != null ? clientIssuedAt.toInstant() : null)
-						.clientSecret(clientSecret)
+						.clientIdIssuedAt(clientIdIssuedAt != null ? clientIdIssuedAt.toInstant() : null)
+						.clientSecret(rs.getString("clientSecret"))
 						.clientSecretExpiresAt(clientSecretExpiresAt != null ? clientSecretExpiresAt.toInstant() : null)
 						.clientName(rs.getString("clientName"))
-						.authorizationGrantTypes((grantTypes) -> authGrantTypes.forEach(authGrantType ->
-								grantTypes.add(AUTHORIZATION_GRANT_TYPE_MAP.get(authGrantType))))
-						.clientAuthenticationMethods((authenticationMethods) -> clientAuthMethods.forEach(clientAuthMethod ->
-								authenticationMethods.add(CLIENT_AUTHENTICATION_METHOD_MAP.get(clientAuthMethod))))
+						.clientAuthenticationMethods((authenticationMethods) ->
+								clientAuthenticationMethods.forEach(authenticationMethod ->
+										authenticationMethods.add(resolveClientAuthenticationMethod(authenticationMethod))))
+						.authorizationGrantTypes((grantTypes) ->
+								authorizationGrantTypes.forEach(grantType ->
+										grantTypes.add(resolveAuthorizationGrantType(grantType))))
 						.redirectUris((uris) -> uris.addAll(redirectUris))
 						.scopes((scopes) -> scopes.addAll(clientScopes));
+				// @formatter:on
+
+				Map<String, Object> clientSettingsMap = parseMap(rs.getString("clientSettings"));
+				builder.clientSettings(clientSettings ->
+						clientSettings.settings().putAll(clientSettingsMap));
 
-				RegisteredClient registeredClient = builder.build();
-				registeredClient.getClientSettings().settings().putAll(parseMap(rs.getString("clientSettings")));
-				registeredClient.getTokenSettings().settings().putAll(parseMap(rs.getString("tokenSettings")));
+				Map<String, Object> tokenSettingsMap = parseMap(rs.getString("tokenSettings"));
+				builder.tokenSettings(tokenSettings ->
+						tokenSettings.settings().putAll(tokenSettingsMap));
 
-				return registeredClient;
+				return builder.build();
 			}
 
 			private Map<String, Object> parseMap(String data) {
@@ -424,26 +340,26 @@ public class JdbcRegisteredClientRepositoryTests {
 				}
 			}
 
-			static {
-				Map<String, AuthorizationGrantType> am = new HashMap<>();
-				for (AuthorizationGrantType a : Arrays.asList(
-						AuthorizationGrantType.AUTHORIZATION_CODE,
-						AuthorizationGrantType.REFRESH_TOKEN,
-						AuthorizationGrantType.CLIENT_CREDENTIALS,
-						AuthorizationGrantType.PASSWORD,
-						AuthorizationGrantType.IMPLICIT)) {
-					am.put(a.getValue(), a);
+			private static AuthorizationGrantType resolveAuthorizationGrantType(String authorizationGrantType) {
+				if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) {
+					return AuthorizationGrantType.AUTHORIZATION_CODE;
+				} else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(authorizationGrantType)) {
+					return AuthorizationGrantType.CLIENT_CREDENTIALS;
+				} else if (AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(authorizationGrantType)) {
+					return AuthorizationGrantType.REFRESH_TOKEN;
 				}
-				AUTHORIZATION_GRANT_TYPE_MAP = Collections.unmodifiableMap(am);
-
-				Map<String, ClientAuthenticationMethod> cm = new HashMap<>();
-				for (ClientAuthenticationMethod c : Arrays.asList(
-						ClientAuthenticationMethod.NONE,
-						ClientAuthenticationMethod.BASIC,
-						ClientAuthenticationMethod.POST)) {
-					cm.put(c.getValue(), c);
+				return new AuthorizationGrantType(authorizationGrantType);		// Custom authorization grant type
+			}
+
+			private static ClientAuthenticationMethod resolveClientAuthenticationMethod(String clientAuthenticationMethod) {
+				if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue().equals(clientAuthenticationMethod)) {
+					return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
+				} else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientAuthenticationMethod)) {
+					return ClientAuthenticationMethod.CLIENT_SECRET_POST;
+				} else if (ClientAuthenticationMethod.NONE.getValue().equals(clientAuthenticationMethod)) {
+					return ClientAuthenticationMethod.NONE;
 				}
-				CLIENT_AUTHENTICATION_METHOD_MAP = Collections.unmodifiableMap(cm);
+				return new ClientAuthenticationMethod(clientAuthenticationMethod);		// Custom client authentication method
 			}
 
 		}

+ 6 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,6 +15,8 @@
  */
 package org.springframework.security.oauth2.server.authorization.client;
 
+import java.time.Instant;
+
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 
@@ -26,6 +28,7 @@ public class TestRegisteredClients {
 	public static RegisteredClient.Builder registeredClient() {
 		return RegisteredClient.withId("registration-1")
 				.clientId("client-1")
+				.clientIdIssuedAt(Instant.now())
 				.clientSecret("secret")
 				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
 				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
@@ -37,6 +40,7 @@ public class TestRegisteredClients {
 	public static RegisteredClient.Builder registeredClient2() {
 		return RegisteredClient.withId("registration-2")
 				.clientId("client-2")
+				.clientIdIssuedAt(Instant.now())
 				.clientSecret("secret")
 				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
 				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
@@ -51,6 +55,7 @@ public class TestRegisteredClients {
 	public static RegisteredClient.Builder registeredPublicClient() {
 		return RegisteredClient.withId("registration-3")
 				.clientId("client-3")
+				.clientIdIssuedAt(Instant.now())
 				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
 				.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
 				.redirectUri("https://example.com")

+ 4 - 4
oauth2-authorization-server/src/test/resources/org/springframework/security/oauth2/server/authorization/client/custom-oauth2-registered-client-schema.sql

@@ -4,12 +4,12 @@ CREATE TABLE oauth2RegisteredClient (
     clientIdIssuedAt timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
     clientSecret varchar(200) DEFAULT NULL,
     clientSecretExpiresAt timestamp DEFAULT NULL,
-    clientName varchar(200),
+    clientName varchar(200) NOT NULL,
     clientAuthenticationMethods varchar(1000) NOT NULL,
     authorizationGrantTypes varchar(1000) NOT NULL,
-    redirectUris varchar(1000) NOT NULL,
+    redirectUris varchar(1000) DEFAULT NULL,
     scopes varchar(1000) NOT NULL,
-    clientSettings varchar(1000) DEFAULT NULL,
-    tokenSettings varchar(1000) DEFAULT NULL,
+    clientSettings varchar(2000) NOT NULL,
+    tokenSettings varchar(2000) NOT NULL,
     PRIMARY KEY (id)
 );