From 1ae4f7aa13e3ace33fc6fa484f3656782f00b310 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Thu, 8 Jul 2021 10:21:36 -0400 Subject: [PATCH] Polish JdbcRegisteredClientRepository Issue gh-291 --- .../JdbcRegisteredClientRepository.java | 309 +++++++------- .../authorization/jackson2/DurationMixin.java | 49 +++ ...uth2AuthorizationServerJackson2Module.java | 8 + .../jackson2/SignatureAlgorithmMixin.java | 34 ++ .../oauth2-registered-client-schema.sql | 8 +- .../JdbcRegisteredClientRepositoryTests.java | 388 +++++++----------- .../client/TestRegisteredClients.java | 7 +- ...custom-oauth2-registered-client-schema.sql | 8 +- 8 files changed, 403 insertions(+), 408 deletions(-) create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/DurationMixin.java create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/SignatureAlgorithmMixin.java diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepository.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepository.java index f2612ba3..c9e8d893 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepository.java +++ b/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. + * + *

+ * NOTE: 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 AUTHORIZATION_GRANT_TYPE_MAP; - private static final Map CLIENT_AUTHENTICATION_METHOD_MAP; - + // @formatter:off private static final String COLUMN_NAMES = "id, " + "client_id, " + "client_id_issued_at, " @@ -68,19 +75,20 @@ 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - - private RowMapper registeredClientRowMapper; - - private Function> registeredClientParametersMapper; + + "(" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + // @formatter:on private final JdbcOperations jdbcOperations; + private RowMapper registeredClientRowMapper; + private Function> registeredClientParametersMapper; /** * 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 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> registeredClientParametersMapper) { - Assert.notNull(registeredClientParametersMapper, "registeredClientParameterMapper cannot be null"); - this.registeredClientParametersMapper = registeredClientParametersMapper; - } - - protected final JdbcOperations getJdbcOperations() { - return this.jdbcOperations; - } - - protected final RowMapper getRegisteredClientRowMapper() { - return this.registeredClientRowMapper; - } - - protected final Function> 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 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 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 { + /** + * 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 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> registeredClientParametersMapper) { + Assert.notNull(registeredClientParametersMapper, "registeredClientParametersMapper cannot be null"); + this.registeredClientParametersMapper = registeredClientParametersMapper; + } + + protected final JdbcOperations getJdbcOperations() { + return this.jdbcOperations; + } + + protected final RowMapper getRegisteredClientRowMapper() { + return this.registeredClientRowMapper; + } + + protected final Function> 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 { private ObjectMapper objectMapper = new ObjectMapper(); + public RegisteredClientRowMapper() { + ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader(); + List securityModules = SecurityJackson2Modules.getModules(classLoader); + this.objectMapper.registerModules(securityModules); + this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); + } + @Override public RegisteredClient mapRow(ResultSet rs, int rowNum) throws SQLException { - Set clientScopes = parseList(rs.getString("scopes")); - Set authGrantTypes = parseList(rs.getString("authorization_grant_types")); - Set clientAuthMethods = parseList(rs.getString("client_authentication_methods")); - Set 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 clientAuthenticationMethods = StringUtils.commaDelimitedListToSet(rs.getString("client_authentication_methods")); + Set authorizationGrantTypes = StringUtils.commaDelimitedListToSet(rs.getString("authorization_grant_types")); + Set redirectUris = StringUtils.commaDelimitedListToSet(rs.getString("redirect_uris")); + Set 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(); + Map clientSettingsMap = parseMap(rs.getString("client_settings")); + builder.clientSettings(clientSettings -> + clientSettings.settings().putAll(clientSettingsMap)); - String tokenSettingsJson = rs.getString("token_settings"); - if (tokenSettingsJson != null) { - Map settings = parseMap(tokenSettingsJson); - TokenSettings tokenSettings = registeredClient.getTokenSettings(); + Map tokenSettingsMap = parseMap(rs.getString("token_settings")); + builder.tokenSettings(tokenSettings -> + tokenSettings.settings().putAll(tokenSettingsMap)); - 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); - } - } - - String clientSettingsJson = rs.getString("client_settings"); - if (clientSettingsJson != null) { - Map settings = parseMap(clientSettingsJson); - ClientSettings clientSettings = registeredClient.getClientSettings(); - - 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 parseList(String s) { - return s != null ? StringUtils.commaDelimitedListToSet(s) : Collections.emptySet(); - } - private Map parseMap(String data) { try { return this.objectMapper.readValue(data, new TypeReference>() {}); @@ -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> { - + /** + * The default {@code Function} that maps {@link RegisteredClient} to a + * {@code List} of {@link SqlParameterValue}. + */ + public static class RegisteredClientParametersMapper implements Function> { private ObjectMapper objectMapper = new ObjectMapper(); + public RegisteredClientParametersMapper() { + ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader(); + List securityModules = SecurityJackson2Modules.getModules(classLoader); + this.objectMapper.registerModules(securityModules); + this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); + } + @Override public List apply(RegisteredClient registeredClient) { - List clientAuthenticationMethodNames = new ArrayList<>(registeredClient.getClientAuthenticationMethods().size()); - for (ClientAuthenticationMethod clientAuthenticationMethod : registeredClient.getClientAuthenticationMethods()) { - clientAuthenticationMethodNames.add(clientAuthenticationMethod.getValue()); - } - - List 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 clientSettings = new HashMap<>(); - clientSettings.put("require_proof_key", registeredClient.getClientSettings().requireProofKey()); - clientSettings.put("require_user_consent", registeredClient.getClientSettings().requireUserConsent()); - String clientSettingsJson = writeMap(clientSettings); + List clientAuthenticationMethods = new ArrayList<>(registeredClient.getClientAuthenticationMethods().size()); + registeredClient.getClientAuthenticationMethods().forEach(clientAuthenticationMethod -> + clientAuthenticationMethods.add(clientAuthenticationMethod.getValue())); - Map 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 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 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 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); - } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/DurationMixin.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/DurationMixin.java new file mode 100644 index 00000000..cfd95003 --- /dev/null +++ b/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(); + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java index e6ab7a7d..f3c1d4b2 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java +++ b/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 *

  • {@link UnmodifiableMapMixin}
  • *
  • {@link HashSetMixin}
  • *
  • {@link OAuth2AuthorizationRequestMixin}
  • + *
  • {@link DurationMixin}
  • + *
  • {@link SignatureAlgorithmMixin}
  • * * * 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); } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/SignatureAlgorithmMixin.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/SignatureAlgorithmMixin.java new file mode 100644 index 00000000..d2a9e1d5 --- /dev/null +++ b/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 { +} diff --git a/oauth2-authorization-server/src/main/resources/org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql b/oauth2-authorization-server/src/main/resources/org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql index aaa2fa2d..a1202307 100644 --- a/oauth2-authorization-server/src/main/resources/org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql +++ b/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) ); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepositoryTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepositoryTests.java index 4e9a03a7..ebc19495 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepositoryTests.java +++ b/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 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; - private RegisteredClient registeredClient; - - private EmbeddedDatabase db; - - private JdbcOperations jdbcOperations; - @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,28 +97,71 @@ public class JdbcRegisteredClientRepositoryTests { } @Test - public void whenSetNullRegisteredClientParameterMapperThenThrow() { + public void setRegisteredClientParametersMapperWhenNullThenThrowIllegalArgumentException() { // @formatter:off assertThatIllegalArgumentException() .isThrownBy(() -> this.registeredClientRepository.setRegisteredClientParametersMapper(null)) - .withMessage("registeredClientParameterMapper cannot be null"); + .withMessage("registeredClientParametersMapper cannot be null"); // @formatter:on } @Test - public void findByIdWhenFoundThenFound() { - String id = this.registeredClient.getId(); - assertRegisteredClientIsEqualTo(this.registeredClientRepository.findById(id), this.registeredClient); + public void saveWhenRegisteredClientNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.registeredClientRepository.save(null)) + .withMessageContaining("registeredClient cannot be null"); } @Test - public void findByIdWhenNotFoundThenNull() { - RegisteredClient client = this.registeredClientRepository.findById("noooope"); - assertThat(client).isNull(); + public void saveWhenExistingIdThenThrowIllegalArgumentException() { + 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()); } @Test - public void findByIdWhenNullThenThrowIllegalArgumentException() { + public void saveWhenExistingClientIdThenThrowIllegalArgumentException() { + 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 saveWhenNewThenSaved() { + RegisteredClient expectedRegisteredClient = TestRegisteredClients.registeredClient().build(); + this.registeredClientRepository.save(expectedRegisteredClient); + RegisteredClient registeredClient = this.registeredClientRepository.findById(expectedRegisteredClient.getId()); + assertThat(registeredClient).isEqualTo(expectedRegisteredClient); + } + + @Test + public void saveLoadRegisteredClientWhenCustomStrategiesSetThenCalled() throws Exception { + RowMapper registeredClientRowMapper = spy( + new JdbcRegisteredClientRepository.RegisteredClientRowMapper()); + this.registeredClientRepository.setRegisteredClientRowMapper(registeredClientRowMapper); + Function> registeredClientParametersMapper = spy( + new JdbcRegisteredClientRepository.RegisteredClientParametersMapper()); + this.registeredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper); + + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + this.registeredClientRepository.save(registeredClient); + RegisteredClient result = this.registeredClientRepository.findById(registeredClient.getId()); + assertThat(result).isEqualTo(registeredClient); + verify(registeredClientRowMapper).mapRow(any(), anyInt()); + verify(registeredClientParametersMapper).apply(any()); + } + + @Test + public void findByIdWhenIdNullThenThrowIllegalArgumentException() { // @formatter:off assertThatIllegalArgumentException() .isThrownBy(() -> this.registeredClientRepository.findById(null)) @@ -137,19 +170,21 @@ public class JdbcRegisteredClientRepositoryTests { } @Test - public void findByClientIdWhenFoundThenFound() { - String id = this.registeredClient.getClientId(); - assertRegisteredClientIsEqualTo(this.registeredClientRepository.findByClientId(id), this.registeredClient); + public void findByIdWhenExistsThenFound() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + this.registeredClientRepository.save(registeredClient); + RegisteredClient result = this.registeredClientRepository.findById(registeredClient.getId()); + assertThat(result).isEqualTo(registeredClient); } @Test - public void findByClientIdWhenNotFoundThenNull() { - RegisteredClient client = this.registeredClientRepository.findByClientId("noooope"); - assertThat(client).isNull(); + public void findByIdWhenNotExistsThenNotFound() { + RegisteredClient result = this.registeredClientRepository.findById("not-exists"); + assertThat(result).isNull(); } @Test - public void findByClientIdWhenNullThenThrowIllegalArgumentException() { + public void findByClientIdWhenClientIdNullThenThrowIllegalArgumentException() { // @formatter:off assertThatIllegalArgumentException() .isThrownBy(() -> this.registeredClientRepository.findByClientId(null)) @@ -158,149 +193,33 @@ public class JdbcRegisteredClientRepositoryTests { } @Test - public void saveWhenNullThenThrowIllegalArgumentException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> this.registeredClientRepository.save(null)) - .withMessageContaining("registeredClient cannot be null"); - } - - @Test - public void saveWhenExistingIdThenThrowIllegalArgumentException() { - RegisteredClient registeredClient = createRegisteredClient( - this.registeredClient.getId(), "client-id-2", "client-secret-2"); - assertThatIllegalArgumentException() - .isThrownBy(() -> this.registeredClientRepository.save(registeredClient)) - .withMessage("Registered client must be unique. Found duplicate identifier: " + registeredClient.getId()); - } - - @Test - public void saveWhenExistingClientIdThenThrowIllegalArgumentException() { - RegisteredClient registeredClient = createRegisteredClient( - "client-2", this.registeredClient.getClientId(), "client-secret-2"); - 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()); + public void findByClientIdWhenExistsThenFound() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); this.registeredClientRepository.save(registeredClient); - RegisteredClient savedClient = this.registeredClientRepository.findById(registeredClient.getId()); - assertRegisteredClientIsEqualTo(savedClient, registeredClient); + RegisteredClient result = this.registeredClientRepository.findByClientId(registeredClient.getClientId()); + assertThat(result).isEqualTo(registeredClient); } @Test - public void saveWhenSavedAndFindByIdThenFound() { - RegisteredClient registeredClient = createRegisteredClient(); - this.registeredClientRepository.save(registeredClient); - RegisteredClient savedClient = this.registeredClientRepository.findById(registeredClient.getId()); - assertRegisteredClientIsEqualTo(savedClient, registeredClient); - } - - @Test - public void saveWhenSavedAndFindByClientIdThenFound() { - RegisteredClient registeredClient = createRegisteredClient(); - this.registeredClientRepository.save(registeredClient); - RegisteredClient savedClient = this.registeredClientRepository.findByClientId(registeredClient.getClientId()); - assertRegisteredClientIsEqualTo(savedClient, registeredClient); - } - - @Test - public void saveWhenPublicClientSavedAndFindByClientIdThenFound() { - RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient().build(); - this.registeredClientRepository.save(registeredClient); - RegisteredClient savedClient = this.registeredClientRepository.findByClientId(registeredClient.getClientId()); - assertRegisteredClientIsEqualTo(savedClient, 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); - } - - @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); - - RegisteredClient retrievedClient = this.registeredClientRepository.findById(client.getId()); - - assertRegisteredClientIsEqualTo(retrievedClient, client); + 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 result = getJdbcOperations() - .query(LOAD_REGISTERED_CLIENT_SQL + filter, getRegisteredClientRowMapper(), args); + List result = getJdbcOperations().query( + LOAD_REGISTERED_CLIENT_SQL + filter, getRegisteredClientRowMapper(), args); return !result.isEmpty() ? result.get(0) : null; } private static final class CustomRegisteredClientRowMapper implements RowMapper { - - private static final Map AUTHORIZATION_GRANT_TYPE_MAP; - private static final Map CLIENT_AUTHENTICATION_METHOD_MAP; - private final ObjectMapper objectMapper = new ObjectMapper(); + private CustomRegisteredClientRowMapper() { + ClassLoader classLoader = CustomJdbcRegisteredClientRepository.class.getClassLoader(); + List securityModules = SecurityJackson2Modules.getModules(classLoader); + this.objectMapper.registerModules(securityModules); + this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); + } + @Override public RegisteredClient mapRow(ResultSet rs, int rowNum) throws SQLException { - Set clientScopes = StringUtils.commaDelimitedListToSet(rs.getString("scopes")); - Set authGrantTypes = StringUtils.commaDelimitedListToSet(rs.getString("authorizationGrantTypes")); - Set clientAuthMethods = StringUtils.commaDelimitedListToSet(rs.getString("clientAuthenticationMethods")); - Set 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 clientAuthenticationMethods = StringUtils.commaDelimitedListToSet(rs.getString("clientAuthenticationMethods")); + Set authorizationGrantTypes = StringUtils.commaDelimitedListToSet(rs.getString("authorizationGrantTypes")); + Set redirectUris = StringUtils.commaDelimitedListToSet(rs.getString("redirectUris")); + Set 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 - RegisteredClient registeredClient = builder.build(); - registeredClient.getClientSettings().settings().putAll(parseMap(rs.getString("clientSettings"))); - registeredClient.getTokenSettings().settings().putAll(parseMap(rs.getString("tokenSettings"))); + Map clientSettingsMap = parseMap(rs.getString("clientSettings")); + builder.clientSettings(clientSettings -> + clientSettings.settings().putAll(clientSettingsMap)); - return registeredClient; + Map tokenSettingsMap = parseMap(rs.getString("tokenSettings")); + builder.tokenSettings(tokenSettings -> + tokenSettings.settings().putAll(tokenSettingsMap)); + + return builder.build(); } private Map parseMap(String data) { @@ -424,26 +340,26 @@ public class JdbcRegisteredClientRepositoryTests { } } - static { - Map 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); + return new AuthorizationGrantType(authorizationGrantType); // Custom authorization grant type + } - Map cm = new HashMap<>(); - for (ClientAuthenticationMethod c : Arrays.asList( - ClientAuthenticationMethod.NONE, - ClientAuthenticationMethod.BASIC, - ClientAuthenticationMethod.POST)) { - cm.put(c.getValue(), c); + 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 } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java index 93ae41f3..81f4e7b1 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java +++ b/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") diff --git a/oauth2-authorization-server/src/test/resources/org/springframework/security/oauth2/server/authorization/client/custom-oauth2-registered-client-schema.sql b/oauth2-authorization-server/src/test/resources/org/springframework/security/oauth2/server/authorization/client/custom-oauth2-registered-client-schema.sql index 28e727f4..64d3a487 100644 --- a/oauth2-authorization-server/src/test/resources/org/springframework/security/oauth2/server/authorization/client/custom-oauth2-registered-client-schema.sql +++ b/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) );