From 04f3fe8af9c0f918acd9b70ce0e52ce42020d5ca Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Sun, 26 Jan 2020 07:58:24 -0500 Subject: [PATCH] Add Jackson support for oauth2-client session related classes Fixes gh-4886 --- .../jackson2/SecurityJackson2Modules.java | 16 +- gradle/dependency-management.gradle | 1 + .../spring-security-oauth2-client.gradle | 4 +- .../ClientRegistrationDeserializer.java | 84 +++++ .../jackson2/ClientRegistrationMixin.java | 40 ++ .../jackson2/DefaultOAuth2UserMixin.java | 49 +++ .../client/jackson2/DefaultOidcUserMixin.java | 51 +++ .../oauth2/client/jackson2/JsonNodeUtils.java | 68 ++++ .../jackson2/OAuth2AccessTokenMixin.java | 53 +++ .../OAuth2AuthenticationTokenMixin.java | 49 +++ ...Auth2AuthorizationRequestDeserializer.java | 76 ++++ .../OAuth2AuthorizationRequestMixin.java | 40 ++ .../jackson2/OAuth2AuthorizedClientMixin.java | 49 +++ .../jackson2/OAuth2ClientJackson2Module.java | 105 ++++++ .../jackson2/OAuth2RefreshTokenMixin.java | 48 +++ .../jackson2/OAuth2UserAuthorityMixin.java | 46 +++ .../client/jackson2/OidcIdTokenMixin.java | 51 +++ .../jackson2/OidcUserAuthorityMixin.java | 47 +++ .../client/jackson2/OidcUserInfoMixin.java | 44 +++ .../oauth2/client/jackson2/StdConverters.java | 92 +++++ .../jackson2/UnmodifiableMapDeserializer.java | 52 +++ .../client/jackson2/UnmodifiableMapMixin.java | 42 +++ .../TestOAuth2AuthenticationTokens.java | 24 +- .../OAuth2AuthenticationTokenMixinTests.java | 351 ++++++++++++++++++ .../OAuth2AuthorizationRequestMixinTests.java | 197 ++++++++++ .../OAuth2AuthorizedClientMixinTests.java | 325 ++++++++++++++++ .../oauth2/core/oidc/user/TestOidcUsers.java | 49 ++- .../oauth2/core/user/TestOAuth2Users.java | 24 +- 28 files changed, 2044 insertions(+), 33 deletions(-) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationDeserializer.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOidcUserMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/JsonNodeUtils.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AccessTokenMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestDeserializer.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2ClientJackson2Module.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2RefreshTokenMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2UserAuthorityMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcIdTokenMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcUserAuthorityMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcUserInfoMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/StdConverters.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapDeserializer.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapMixin.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixinTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestMixinTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixinTests.java diff --git a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java index f42d4549fd..f10fefa8fc 100644 --- a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java +++ b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2020 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. @@ -61,6 +61,7 @@ import java.util.Set; * mapper.registerModule(new WebJackson2Module()); * mapper.registerModule(new WebServletJackson2Module()); * mapper.registerModule(new WebServerJackson2Module()); + * mapper.registerModule(new OAuth2ClientJackson2Module()); * * * @author Jitendra Singh. @@ -77,6 +78,10 @@ public final class SecurityJackson2Modules { ); private static final String webServletJackson2ModuleClass = "org.springframework.security.web.jackson2.WebServletJackson2Module"; + private static final String oauth2ClientJackson2ModuleClass = + "org.springframework.security.oauth2.client.jackson2.OAuth2ClientJackson2Module"; + private static final String javaTimeJackson2ModuleClass = + "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"; private SecurityJackson2Modules() { } @@ -121,6 +126,12 @@ public final class SecurityJackson2Modules { if (ClassUtils.isPresent("javax.servlet.http.Cookie", loader)) { addToModulesList(loader, modules, webServletJackson2ModuleClass); } + if (ClassUtils.isPresent("org.springframework.security.oauth2.client.OAuth2AuthorizedClient", loader)) { + addToModulesList(loader, modules, oauth2ClientJackson2ModuleClass); + } + if (ClassUtils.isPresent(javaTimeJackson2ModuleClass, loader)) { + addToModulesList(loader, modules, javaTimeJackson2ModuleClass); + } return modules; } @@ -188,8 +199,11 @@ public final class SecurityJackson2Modules { "java.util.Collections$UnmodifiableRandomAccessList", "java.util.Collections$SingletonList", "java.util.Date", + "java.time.Instant", + "java.net.URL", "java.util.TreeMap", "java.util.HashMap", + "java.util.LinkedHashMap", "org.springframework.security.core.context.SecurityContextImpl" ))); diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index f96ded3a9d..07265452f0 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -28,6 +28,7 @@ dependencies { constraints { management "ch.qos.logback:logback-classic:1.+" management "com.fasterxml.jackson.core:jackson-databind:2.+" + management 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.+' management "com.google.appengine:appengine-api-1.0-sdk:$gaeVersion" management "com.google.appengine:appengine-api-labs:$gaeVersion" management "com.google.appengine:appengine-api-stubs:$gaeVersion" diff --git a/oauth2/oauth2-client/spring-security-oauth2-client.gradle b/oauth2/oauth2-client/spring-security-oauth2-client.gradle index e73446c28f..d43a0849e1 100644 --- a/oauth2/oauth2-client/spring-security-oauth2-client.gradle +++ b/oauth2/oauth2-client/spring-security-oauth2-client.gradle @@ -10,15 +10,17 @@ dependencies { optional project(':spring-security-oauth2-jose') optional 'io.projectreactor:reactor-core' optional 'org.springframework:spring-webflux' + optional 'com.fasterxml.jackson.core:jackson-databind' + optional 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' testCompile project(path: ':spring-security-oauth2-core', configuration: 'tests') testCompile project(path: ':spring-security-oauth2-jose', configuration: 'tests') testCompile powerMock2Dependencies testCompile 'com.squareup.okhttp3:mockwebserver' - testCompile 'com.fasterxml.jackson.core:jackson-databind' testCompile 'io.projectreactor.netty:reactor-netty' testCompile 'io.projectreactor:reactor-test' testCompile 'io.projectreactor.tools:blockhound' + testCompile 'org.skyscreamer:jsonassert' provided 'javax.servlet:javax.servlet-api' } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationDeserializer.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationDeserializer.java new file mode 100644 index 0000000000..86895f328d --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationDeserializer.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.util.StdConverter; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +import java.io.IOException; + +import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.MAP_TYPE_REFERENCE; +import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.SET_TYPE_REFERENCE; +import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.findObjectNode; +import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.findStringValue; +import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.findValue; + +/** + * A {@code JsonDeserializer} for {@link ClientRegistration}. + * + * @author Joe Grandja + * @since 5.3 + * @see ClientRegistration + * @see ClientRegistrationMixin + */ +final class ClientRegistrationDeserializer extends JsonDeserializer { + private static final StdConverter CLIENT_AUTHENTICATION_METHOD_CONVERTER = + new StdConverters.ClientAuthenticationMethodConverter(); + private static final StdConverter AUTHORIZATION_GRANT_TYPE_CONVERTER = + new StdConverters.AuthorizationGrantTypeConverter(); + private static final StdConverter AUTHENTICATION_METHOD_CONVERTER = + new StdConverters.AuthenticationMethodConverter(); + + @Override + public ClientRegistration deserialize(JsonParser parser, DeserializationContext context) throws IOException { + ObjectMapper mapper = (ObjectMapper) parser.getCodec(); + JsonNode clientRegistrationNode = mapper.readTree(parser); + JsonNode providerDetailsNode = findObjectNode(clientRegistrationNode, "providerDetails"); + JsonNode userInfoEndpointNode = findObjectNode(providerDetailsNode, "userInfoEndpoint"); + + return ClientRegistration + .withRegistrationId(findStringValue(clientRegistrationNode, "registrationId")) + .clientId(findStringValue(clientRegistrationNode, "clientId")) + .clientSecret(findStringValue(clientRegistrationNode, "clientSecret")) + .clientAuthenticationMethod( + CLIENT_AUTHENTICATION_METHOD_CONVERTER.convert( + findObjectNode(clientRegistrationNode, "clientAuthenticationMethod"))) + .authorizationGrantType( + AUTHORIZATION_GRANT_TYPE_CONVERTER.convert( + findObjectNode(clientRegistrationNode, "authorizationGrantType"))) + .redirectUriTemplate(findStringValue(clientRegistrationNode, "redirectUriTemplate")) + .scope(findValue(clientRegistrationNode, "scopes", SET_TYPE_REFERENCE, mapper)) + .clientName(findStringValue(clientRegistrationNode, "clientName")) + .authorizationUri(findStringValue(providerDetailsNode, "authorizationUri")) + .tokenUri(findStringValue(providerDetailsNode, "tokenUri")) + .userInfoUri(findStringValue(userInfoEndpointNode, "uri")) + .userInfoAuthenticationMethod( + AUTHENTICATION_METHOD_CONVERTER.convert( + findObjectNode(userInfoEndpointNode, "authenticationMethod"))) + .userNameAttributeName(findStringValue(userInfoEndpointNode, "userNameAttributeName")) + .jwkSetUri(findStringValue(providerDetailsNode, "jwkSetUri")) + .providerConfigurationMetadata(findValue(providerDetailsNode, "configurationMetadata", MAP_TYPE_REFERENCE, mapper)) + .build(); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationMixin.java new file mode 100644 index 0000000000..a60708495c --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationMixin.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.springframework.security.oauth2.client.registration.ClientRegistration; + +/** + * This mixin class is used to serialize/deserialize {@link ClientRegistration}. + * It also registers a custom deserializer {@link ClientRegistrationDeserializer}. + * + * @author Joe Grandja + * @since 5.3 + * @see ClientRegistration + * @see ClientRegistrationDeserializer + * @see OAuth2ClientJackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(using = ClientRegistrationDeserializer.class) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class ClientRegistrationMixin { +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserMixin.java new file mode 100644 index 0000000000..056b27d59e --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserMixin.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; + +import java.util.Collection; +import java.util.Map; + +/** + * This mixin class is used to serialize/deserialize {@link DefaultOAuth2User}. + * + * @author Joe Grandja + * @since 5.3 + * @see DefaultOAuth2User + * @see OAuth2ClientJackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class DefaultOAuth2UserMixin { + + @JsonCreator + DefaultOAuth2UserMixin( + @JsonProperty("authorities") Collection authorities, + @JsonProperty("attributes") Map attributes, + @JsonProperty("nameAttributeKey") String nameAttributeKey) { + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOidcUserMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOidcUserMixin.java new file mode 100644 index 0000000000..003b6e707e --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOidcUserMixin.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; + +import java.util.Collection; + +/** + * This mixin class is used to serialize/deserialize {@link DefaultOidcUser}. + * + * @author Joe Grandja + * @since 5.3 + * @see DefaultOidcUser + * @see OAuth2ClientJackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(value = {"attributes"}, ignoreUnknown = true) +abstract class DefaultOidcUserMixin { + + @JsonCreator + DefaultOidcUserMixin( + @JsonProperty("authorities") Collection authorities, + @JsonProperty("idToken") OidcIdToken idToken, + @JsonProperty("userInfo") OidcUserInfo userInfo, + @JsonProperty("nameAttributeKey") String nameAttributeKey) { + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/JsonNodeUtils.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/JsonNodeUtils.java new file mode 100644 index 0000000000..d9320227b7 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/JsonNodeUtils.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.Map; +import java.util.Set; + +/** + * Utility class for {@code JsonNode}. + * + * @author Joe Grandja + * @since 5.3 + */ +abstract class JsonNodeUtils { + static final TypeReference> SET_TYPE_REFERENCE = new TypeReference>() {}; + static final TypeReference> MAP_TYPE_REFERENCE = new TypeReference>() {}; + + + static String findStringValue(JsonNode jsonNode, String fieldName) { + if (jsonNode == null) { + return null; + } + JsonNode nodeValue = jsonNode.findValue(fieldName); + if (nodeValue != null && nodeValue.isTextual()) { + return nodeValue.asText(); + } + return null; + } + + static T findValue(JsonNode jsonNode, String fieldName, TypeReference valueTypeReference, ObjectMapper mapper) { + if (jsonNode == null) { + return null; + } + JsonNode nodeValue = jsonNode.findValue(fieldName); + if (nodeValue != null && nodeValue.isContainerNode()) { + return (T) mapper.convertValue(nodeValue, valueTypeReference); + } + return null; + } + + static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) { + if (jsonNode == null) { + return null; + } + JsonNode nodeValue = jsonNode.findValue(fieldName); + if (nodeValue != null && nodeValue.isObject()) { + return nodeValue; + } + return null; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AccessTokenMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AccessTokenMixin.java new file mode 100644 index 0000000000..705ecf376d --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AccessTokenMixin.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.util.StdDateFormat; +import org.springframework.security.oauth2.core.OAuth2AccessToken; + +import java.time.Instant; +import java.util.Set; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2AccessToken}. + * + * @author Joe Grandja + * @since 5.3 + * @see OAuth2AccessToken + * @see OAuth2ClientJackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class OAuth2AccessTokenMixin { + + @JsonCreator + OAuth2AccessTokenMixin( + @JsonProperty("tokenType") @JsonDeserialize(converter = StdConverters.AccessTokenTypeConverter.class) OAuth2AccessToken.TokenType tokenType, + @JsonProperty("tokenValue") String tokenValue, + @JsonProperty("issuedAt") @JsonFormat(pattern = StdDateFormat.DATE_FORMAT_STR_ISO8601, timezone = "UTC") Instant issuedAt, + @JsonProperty("expiresAt") @JsonFormat(pattern = StdDateFormat.DATE_FORMAT_STR_ISO8601, timezone = "UTC") Instant expiresAt, + @JsonProperty("scopes") Set scopes) { + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixin.java new file mode 100644 index 0000000000..ebd3c1b77c --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixin.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2AuthenticationToken}. + * + * @author Joe Grandja + * @since 5.3 + * @see OAuth2AuthenticationToken + * @see OAuth2ClientJackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(value = {"authenticated"}, ignoreUnknown = true) +abstract class OAuth2AuthenticationTokenMixin { + + @JsonCreator + OAuth2AuthenticationTokenMixin( + @JsonProperty("principal") OAuth2User principal, + @JsonProperty("authorities") Collection authorities, + @JsonProperty("authorizedClientRegistrationId") String authorizedClientRegistrationId) { + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestDeserializer.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestDeserializer.java new file mode 100644 index 0000000000..ae3d64ae38 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestDeserializer.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.util.StdConverter; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; + +import java.io.IOException; + +import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.MAP_TYPE_REFERENCE; +import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.SET_TYPE_REFERENCE; +import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.findObjectNode; +import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.findStringValue; +import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.findValue; + +/** + * A {@code JsonDeserializer} for {@link OAuth2AuthorizationRequest}. + * + * @author Joe Grandja + * @since 5.3 + * @see OAuth2AuthorizationRequest + * @see OAuth2AuthorizationRequestMixin + */ +final class OAuth2AuthorizationRequestDeserializer extends JsonDeserializer { + private static final StdConverter AUTHORIZATION_GRANT_TYPE_CONVERTER = + new StdConverters.AuthorizationGrantTypeConverter(); + + @Override + public OAuth2AuthorizationRequest deserialize(JsonParser parser, DeserializationContext context) throws IOException { + ObjectMapper mapper = (ObjectMapper) parser.getCodec(); + JsonNode authorizationRequestNode = mapper.readTree(parser); + + AuthorizationGrantType authorizationGrantType = AUTHORIZATION_GRANT_TYPE_CONVERTER.convert( + findObjectNode(authorizationRequestNode, "authorizationGrantType")); + + OAuth2AuthorizationRequest.Builder builder; + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationGrantType)) { + builder = OAuth2AuthorizationRequest.authorizationCode(); + } else if (AuthorizationGrantType.IMPLICIT.equals(authorizationGrantType)) { + builder = OAuth2AuthorizationRequest.implicit(); + } else { + throw new JsonParseException(parser, "Invalid authorizationGrantType"); + } + + return builder + .authorizationUri(findStringValue(authorizationRequestNode, "authorizationUri")) + .clientId(findStringValue(authorizationRequestNode, "clientId")) + .redirectUri(findStringValue(authorizationRequestNode, "redirectUri")) + .scopes(findValue(authorizationRequestNode, "scopes", SET_TYPE_REFERENCE, mapper)) + .state(findStringValue(authorizationRequestNode, "state")) + .additionalParameters(findValue(authorizationRequestNode, "additionalParameters", MAP_TYPE_REFERENCE, mapper)) + .authorizationRequestUri(findStringValue(authorizationRequestNode, "authorizationRequestUri")) + .attributes(findValue(authorizationRequestNode, "attributes", MAP_TYPE_REFERENCE, mapper)) + .build(); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestMixin.java new file mode 100644 index 0000000000..4e85728a1f --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestMixin.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2AuthorizationRequest}. + * It also registers a custom deserializer {@link OAuth2AuthorizationRequestDeserializer}. + * + * @author Joe Grandja + * @since 5.3 + * @see OAuth2AuthorizationRequest + * @see OAuth2AuthorizationRequestDeserializer + * @see OAuth2ClientJackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(using = OAuth2AuthorizationRequestDeserializer.class) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class OAuth2AuthorizationRequestMixin { +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixin.java new file mode 100644 index 0000000000..3ca81104eb --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixin.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2AuthorizedClient}. + * + * @author Joe Grandja + * @since 5.3 + * @see OAuth2AuthorizedClient + * @see OAuth2ClientJackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class OAuth2AuthorizedClientMixin { + + @JsonCreator + OAuth2AuthorizedClientMixin( + @JsonProperty("clientRegistration") ClientRegistration clientRegistration, + @JsonProperty("principalName") String principalName, + @JsonProperty("accessToken") OAuth2AccessToken accessToken, + @JsonProperty("refreshToken") OAuth2RefreshToken refreshToken) { + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2ClientJackson2Module.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2ClientJackson2Module.java new file mode 100644 index 0000000000..fa8dbcb200 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2ClientJackson2Module.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; + +import java.util.Collections; + +/** + * Jackson {@code Module} for {@code spring-security-oauth2-client}, + * that registers the following mix-in annotations: + * + *
    + *
  • {@link OAuth2AuthorizationRequestMixin}
  • + *
  • {@link ClientRegistrationMixin}
  • + *
  • {@link OAuth2AccessTokenMixin}
  • + *
  • {@link OAuth2RefreshTokenMixin}
  • + *
  • {@link OAuth2AuthorizedClientMixin}
  • + *
  • {@link OAuth2UserAuthorityMixin}
  • + *
  • {@link DefaultOAuth2UserMixin}
  • + *
  • {@link OidcIdTokenMixin}
  • + *
  • {@link OidcUserInfoMixin}
  • + *
  • {@link OidcUserAuthorityMixin}
  • + *
  • {@link DefaultOidcUserMixin}
  • + *
  • {@link OAuth2AuthenticationTokenMixin}
  • + *
+ * + * If not already enabled, default typing will be automatically enabled + * as type info is required to properly serialize/deserialize objects. + * In order to use this module just add it to your {@code ObjectMapper} configuration. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new OAuth2ClientJackson2Module());
+ * 
+ * + * NOTE: Use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get a list of all security modules. + * + * @author Joe Grandja + * @since 5.3 + * @see SecurityJackson2Modules + * @see OAuth2AuthorizationRequestMixin + * @see ClientRegistrationMixin + * @see OAuth2AccessTokenMixin + * @see OAuth2RefreshTokenMixin + * @see OAuth2AuthorizedClientMixin + * @see OAuth2UserAuthorityMixin + * @see DefaultOAuth2UserMixin + * @see OidcIdTokenMixin + * @see OidcUserInfoMixin + * @see OidcUserAuthorityMixin + * @see DefaultOidcUserMixin + * @see OAuth2AuthenticationTokenMixin + */ +public class OAuth2ClientJackson2Module extends SimpleModule { + + public OAuth2ClientJackson2Module() { + super(OAuth2ClientJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null)); + } + + @Override + public void setupModule(SetupContext context) { + SecurityJackson2Modules.enableDefaultTyping(context.getOwner()); + context.setMixInAnnotations(Collections.unmodifiableMap(Collections.emptyMap()).getClass(), UnmodifiableMapMixin.class); + context.setMixInAnnotations(OAuth2AuthorizationRequest.class, OAuth2AuthorizationRequestMixin.class); + context.setMixInAnnotations(ClientRegistration.class, ClientRegistrationMixin.class); + context.setMixInAnnotations(OAuth2AccessToken.class, OAuth2AccessTokenMixin.class); + context.setMixInAnnotations(OAuth2RefreshToken.class, OAuth2RefreshTokenMixin.class); + context.setMixInAnnotations(OAuth2AuthorizedClient.class, OAuth2AuthorizedClientMixin.class); + context.setMixInAnnotations(OAuth2UserAuthority.class, OAuth2UserAuthorityMixin.class); + context.setMixInAnnotations(DefaultOAuth2User.class, DefaultOAuth2UserMixin.class); + context.setMixInAnnotations(OidcIdToken.class, OidcIdTokenMixin.class); + context.setMixInAnnotations(OidcUserInfo.class, OidcUserInfoMixin.class); + context.setMixInAnnotations(OidcUserAuthority.class, OidcUserAuthorityMixin.class); + context.setMixInAnnotations(DefaultOidcUser.class, DefaultOidcUserMixin.class); + context.setMixInAnnotations(OAuth2AuthenticationToken.class, OAuth2AuthenticationTokenMixin.class); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2RefreshTokenMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2RefreshTokenMixin.java new file mode 100644 index 0000000000..7393d6e8b9 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2RefreshTokenMixin.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.util.StdDateFormat; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; + +import java.time.Instant; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2RefreshToken}. + * + * @author Joe Grandja + * @since 5.3 + * @see OAuth2RefreshToken + * @see OAuth2ClientJackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class OAuth2RefreshTokenMixin { + + @JsonCreator + OAuth2RefreshTokenMixin( + @JsonProperty("tokenValue") String tokenValue, + @JsonProperty("issuedAt") @JsonFormat(pattern = StdDateFormat.DATE_FORMAT_STR_ISO8601, timezone = "UTC") Instant issuedAt) { + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2UserAuthorityMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2UserAuthorityMixin.java new file mode 100644 index 0000000000..02509a3575 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2UserAuthorityMixin.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; + +import java.util.Map; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2UserAuthority}. + * + * @author Joe Grandja + * @since 5.3 + * @see OAuth2UserAuthority + * @see OAuth2ClientJackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class OAuth2UserAuthorityMixin { + + @JsonCreator + OAuth2UserAuthorityMixin( + @JsonProperty("authority") String authority, + @JsonProperty("attributes") Map attributes) { + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcIdTokenMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcIdTokenMixin.java new file mode 100644 index 0000000000..32c1d51891 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcIdTokenMixin.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.util.StdDateFormat; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; + +import java.time.Instant; +import java.util.Map; + +/** + * This mixin class is used to serialize/deserialize {@link OidcIdToken}. + * + * @author Joe Grandja + * @since 5.3 + * @see OidcIdToken + * @see OAuth2ClientJackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class OidcIdTokenMixin { + + @JsonCreator + OidcIdTokenMixin( + @JsonProperty("tokenValue") String tokenValue, + @JsonProperty("issuedAt") @JsonFormat(pattern = StdDateFormat.DATE_FORMAT_STR_ISO8601, timezone = "UTC") Instant issuedAt, + @JsonProperty("expiresAt") @JsonFormat(pattern = StdDateFormat.DATE_FORMAT_STR_ISO8601, timezone = "UTC") Instant expiresAt, + @JsonProperty("claims") Map claims) { + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcUserAuthorityMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcUserAuthorityMixin.java new file mode 100644 index 0000000000..1ab4de1ac2 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcUserAuthorityMixin.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; + +/** + * This mixin class is used to serialize/deserialize {@link OidcUserAuthority}. + * + * @author Joe Grandja + * @since 5.3 + * @see OidcUserAuthority + * @see OAuth2ClientJackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(value = {"attributes"}, ignoreUnknown = true) +abstract class OidcUserAuthorityMixin { + + @JsonCreator + OidcUserAuthorityMixin( + @JsonProperty("authority") String authority, + @JsonProperty("idToken") OidcIdToken idToken, + @JsonProperty("userInfo") OidcUserInfo userInfo) { + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcUserInfoMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcUserInfoMixin.java new file mode 100644 index 0000000000..89b131fbdb --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcUserInfoMixin.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; + +import java.util.Map; + +/** + * This mixin class is used to serialize/deserialize {@link OidcUserInfo}. + * + * @author Joe Grandja + * @since 5.3 + * @see OidcUserInfo + * @see OAuth2ClientJackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class OidcUserInfoMixin { + + @JsonCreator + OidcUserInfoMixin(@JsonProperty("claims") Map claims) { + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/StdConverters.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/StdConverters.java new file mode 100644 index 0000000000..10510e5baf --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/StdConverters.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.util.StdConverter; +import org.springframework.security.oauth2.core.AuthenticationMethod; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; + +import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.findStringValue; + +/** + * {@code StdConverter} implementations. + * + * @author Joe Grandja + * @since 5.3 + */ +abstract class StdConverters { + + static final class AccessTokenTypeConverter extends StdConverter { + @Override + public OAuth2AccessToken.TokenType convert(JsonNode jsonNode) { + String value = findStringValue(jsonNode, "value"); + if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(value)) { + return OAuth2AccessToken.TokenType.BEARER; + } + return null; + } + } + + static final class ClientAuthenticationMethodConverter extends StdConverter { + @Override + public ClientAuthenticationMethod convert(JsonNode jsonNode) { + String value = findStringValue(jsonNode, "value"); + if (ClientAuthenticationMethod.BASIC.getValue().equalsIgnoreCase(value)) { + return ClientAuthenticationMethod.BASIC; + } else if (ClientAuthenticationMethod.POST.getValue().equalsIgnoreCase(value)) { + return ClientAuthenticationMethod.POST; + } else if (ClientAuthenticationMethod.NONE.getValue().equalsIgnoreCase(value)) { + return ClientAuthenticationMethod.NONE; + } + return null; + } + } + + static final class AuthorizationGrantTypeConverter extends StdConverter { + @Override + public AuthorizationGrantType convert(JsonNode jsonNode) { + String value = findStringValue(jsonNode, "value"); + if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equalsIgnoreCase(value)) { + return AuthorizationGrantType.AUTHORIZATION_CODE; + } else if (AuthorizationGrantType.IMPLICIT.getValue().equalsIgnoreCase(value)) { + return AuthorizationGrantType.IMPLICIT; + } else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equalsIgnoreCase(value)) { + return AuthorizationGrantType.CLIENT_CREDENTIALS; + } else if (AuthorizationGrantType.PASSWORD.getValue().equalsIgnoreCase(value)) { + return AuthorizationGrantType.PASSWORD; + } + return null; + } + } + + static final class AuthenticationMethodConverter extends StdConverter { + @Override + public AuthenticationMethod convert(JsonNode jsonNode) { + String value = findStringValue(jsonNode, "value"); + if (AuthenticationMethod.HEADER.getValue().equalsIgnoreCase(value)) { + return AuthenticationMethod.HEADER; + } else if (AuthenticationMethod.FORM.getValue().equalsIgnoreCase(value)) { + return AuthenticationMethod.FORM; + } else if (AuthenticationMethod.QUERY.getValue().equalsIgnoreCase(value)) { + return AuthenticationMethod.QUERY; + } + return null; + } + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapDeserializer.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapDeserializer.java new file mode 100644 index 0000000000..e7f97a1b87 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapDeserializer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A {@code JsonDeserializer} for {@link Collections#unmodifiableMap(Map)}. + * + * @author Joe Grandja + * @since 5.3 + * @see Collections#unmodifiableMap(Map) + * @see UnmodifiableMapMixin + */ +final class UnmodifiableMapDeserializer extends JsonDeserializer> { + + @Override + public Map deserialize(JsonParser parser, DeserializationContext context) throws IOException { + ObjectMapper mapper = (ObjectMapper) parser.getCodec(); + JsonNode mapNode = mapper.readTree(parser); + Map result = new LinkedHashMap<>(); + if (mapNode != null && mapNode.isObject()) { + Iterable> fields = mapNode::fields; + for (Map.Entry field : fields) { + result.put(field.getKey(), mapper.readValue(field.getValue().traverse(mapper), Object.class)); + } + } + return Collections.unmodifiableMap(result); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapMixin.java new file mode 100644 index 0000000000..18753d4154 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapMixin.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.Collections; +import java.util.Map; + +/** + * This mixin class is used to serialize/deserialize {@link Collections#unmodifiableMap(Map)}. + * It also registers a custom deserializer {@link UnmodifiableMapDeserializer}. + * + * @author Joe Grandja + * @since 5.3 + * @see Collections#unmodifiableMap(Map) + * @see UnmodifiableMapDeserializer + * @see OAuth2ClientJackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(using = UnmodifiableMapDeserializer.class) +abstract class UnmodifiableMapMixin { + + @JsonCreator + UnmodifiableMapMixin(Map map) { + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/TestOAuth2AuthenticationTokens.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/TestOAuth2AuthenticationTokens.java index df60ad55a3..e3b3815287 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/TestOAuth2AuthenticationTokens.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/TestOAuth2AuthenticationTokens.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -16,11 +16,9 @@ package org.springframework.security.oauth2.client.authentication; -import java.util.Collection; - -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.oauth2.core.user.TestOAuth2Users; /** @@ -28,10 +26,16 @@ import org.springframework.security.oauth2.core.user.TestOAuth2Users; * @since 5.2 */ public class TestOAuth2AuthenticationTokens { - public static OAuth2AuthenticationToken authenticated(String... roles) { - OAuth2User principal = TestOAuth2Users.create(); - Collection authorities = AuthorityUtils.createAuthorityList(roles); + + public static OAuth2AuthenticationToken authenticated() { + DefaultOAuth2User principal = TestOAuth2Users.create(); String registrationId = "registration-id"; - return new OAuth2AuthenticationToken(principal, authorities, registrationId); + return new OAuth2AuthenticationToken(principal, principal.getAuthorities(), registrationId); + } + + public static OAuth2AuthenticationToken oidcAuthenticated() { + DefaultOidcUser principal = TestOidcUsers.create(); + String registrationId = "registration-id"; + return new OAuth2AuthenticationToken(principal, principal.getAuthorities(), registrationId); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixinTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixinTests.java new file mode 100644 index 0000000000..f0b9793719 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixinTests.java @@ -0,0 +1,351 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.junit.Before; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthenticationTokens; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.NAME; + +/** + * Tests for {@link OAuth2AuthenticationTokenMixin}. + * + * @author Joe Grandja + */ +public class OAuth2AuthenticationTokenMixinTests { + private static DateFormat dateFormatter; + private ObjectMapper mapper; + + @Before + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = new ObjectMapper(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + this.mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + dateFormatter = this.mapper.getDateFormat(); + } + + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + // OidcUser + OAuth2AuthenticationToken authentication = TestOAuth2AuthenticationTokens.oidcAuthenticated(); + String expectedJson = asJson(authentication); + String json = this.mapper.writeValueAsString(authentication); + JSONAssert.assertEquals(expectedJson, json, true); + + // OAuth2User + authentication = TestOAuth2AuthenticationTokens.authenticated(); + expectedJson = asJson(authentication); + json = this.mapper.writeValueAsString(authentication); + JSONAssert.assertEquals(expectedJson, json, true); + } + + @Test + public void serializeWhenRequiredAttributesOnlyThenSerializes() throws Exception { + DefaultOidcUser principal = TestOidcUsers.create(); + principal = new DefaultOidcUser(principal.getAuthorities(), principal.getIdToken()); + OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken( + principal, Collections.emptyList(), "registration-id"); + String expectedJson = asJson(authentication); + String json = this.mapper.writeValueAsString(authentication); + JSONAssert.assertEquals(expectedJson, json, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + OAuth2AuthenticationToken authentication = TestOAuth2AuthenticationTokens.oidcAuthenticated(); + String json = asJson(authentication); + assertThatThrownBy(() -> new ObjectMapper().readValue(json, OAuth2AuthenticationToken.class)) + .isInstanceOf(JsonProcessingException.class); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + // OidcUser + OAuth2AuthenticationToken expectedAuthentication = TestOAuth2AuthenticationTokens.oidcAuthenticated(); + String json = asJson(expectedAuthentication); + OAuth2AuthenticationToken authentication = this.mapper.readValue(json, OAuth2AuthenticationToken.class); + assertThat(authentication.getAuthorities()) + .containsExactlyElementsOf(expectedAuthentication.getAuthorities()); + assertThat(authentication.getDetails()) + .isEqualTo(expectedAuthentication.getDetails()); + assertThat(authentication.isAuthenticated()) + .isEqualTo(expectedAuthentication.isAuthenticated()); + assertThat(authentication.getAuthorizedClientRegistrationId()) + .isEqualTo(expectedAuthentication.getAuthorizedClientRegistrationId()); + DefaultOidcUser expectedOidcUser = (DefaultOidcUser) expectedAuthentication.getPrincipal(); + DefaultOidcUser oidcUser = (DefaultOidcUser) authentication.getPrincipal(); + assertThat(oidcUser.getAuthorities().containsAll(expectedOidcUser.getAuthorities())).isTrue(); + assertThat(oidcUser.getAttributes()) + .containsExactlyEntriesOf(expectedOidcUser.getAttributes()); + assertThat(oidcUser.getName()) + .isEqualTo(expectedOidcUser.getName()); + OidcIdToken expectedIdToken = expectedOidcUser.getIdToken(); + OidcIdToken idToken = oidcUser.getIdToken(); + assertThat(idToken.getTokenValue()) + .isEqualTo(expectedIdToken.getTokenValue()); + assertThat(idToken.getIssuedAt()) + .isEqualTo(expectedIdToken.getIssuedAt()); + assertThat(idToken.getExpiresAt()) + .isEqualTo(expectedIdToken.getExpiresAt()); + assertThat(idToken.getClaims()) + .containsExactlyEntriesOf(expectedIdToken.getClaims()); + OidcUserInfo expectedUserInfo = expectedOidcUser.getUserInfo(); + OidcUserInfo userInfo = oidcUser.getUserInfo(); + assertThat(userInfo.getClaims()) + .containsExactlyEntriesOf(expectedUserInfo.getClaims()); + + // OAuth2User + expectedAuthentication = TestOAuth2AuthenticationTokens.authenticated(); + json = asJson(expectedAuthentication); + authentication = this.mapper.readValue(json, OAuth2AuthenticationToken.class); + assertThat(authentication.getAuthorities()) + .containsExactlyElementsOf(expectedAuthentication.getAuthorities()); + assertThat(authentication.getDetails()) + .isEqualTo(expectedAuthentication.getDetails()); + assertThat(authentication.isAuthenticated()) + .isEqualTo(expectedAuthentication.isAuthenticated()); + assertThat(authentication.getAuthorizedClientRegistrationId()) + .isEqualTo(expectedAuthentication.getAuthorizedClientRegistrationId()); + DefaultOAuth2User expectedOauth2User = (DefaultOAuth2User) expectedAuthentication.getPrincipal(); + DefaultOAuth2User oauth2User = (DefaultOAuth2User) authentication.getPrincipal(); + assertThat(oauth2User.getAuthorities().containsAll(expectedOauth2User.getAuthorities())).isTrue(); + assertThat(oauth2User.getAttributes()) + .containsExactlyEntriesOf(expectedOauth2User.getAttributes()); + assertThat(oauth2User.getName()) + .isEqualTo(expectedOauth2User.getName()); + } + + @Test + public void deserializeWhenRequiredAttributesOnlyThenDeserializes() throws Exception { + DefaultOidcUser expectedPrincipal = TestOidcUsers.create(); + expectedPrincipal = new DefaultOidcUser(expectedPrincipal.getAuthorities(), expectedPrincipal.getIdToken()); + OAuth2AuthenticationToken expectedAuthentication = new OAuth2AuthenticationToken( + expectedPrincipal, Collections.emptyList(), "registration-id"); + String json = asJson(expectedAuthentication); + OAuth2AuthenticationToken authentication = this.mapper.readValue(json, OAuth2AuthenticationToken.class); + assertThat(authentication.getAuthorities()).isEmpty(); + assertThat(authentication.getDetails()) + .isEqualTo(expectedAuthentication.getDetails()); + assertThat(authentication.isAuthenticated()) + .isEqualTo(expectedAuthentication.isAuthenticated()); + assertThat(authentication.getAuthorizedClientRegistrationId()) + .isEqualTo(expectedAuthentication.getAuthorizedClientRegistrationId()); + DefaultOidcUser principal = (DefaultOidcUser) authentication.getPrincipal(); + assertThat(principal.getAuthorities().containsAll(expectedPrincipal.getAuthorities())).isTrue(); + assertThat(principal.getAttributes()) + .containsExactlyEntriesOf(expectedPrincipal.getAttributes()); + assertThat(principal.getName()) + .isEqualTo(expectedPrincipal.getName()); + OidcIdToken expectedIdToken = expectedPrincipal.getIdToken(); + OidcIdToken idToken = principal.getIdToken(); + assertThat(idToken.getTokenValue()) + .isEqualTo(expectedIdToken.getTokenValue()); + assertThat(idToken.getIssuedAt()) + .isEqualTo(expectedIdToken.getIssuedAt()); + assertThat(idToken.getExpiresAt()) + .isEqualTo(expectedIdToken.getExpiresAt()); + assertThat(idToken.getClaims()) + .containsExactlyEntriesOf(expectedIdToken.getClaims()); + assertThat(principal.getUserInfo()).isNull(); + } + + private static String asJson(OAuth2AuthenticationToken authentication) { + String principalJson = authentication.getPrincipal() instanceof DefaultOidcUser ? + asJson((DefaultOidcUser) authentication.getPrincipal()) : + asJson((DefaultOAuth2User) authentication.getPrincipal()); + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken\",\n" + + " \"principal\": " + principalJson + ",\n" + + " \"authorities\": " + asJson(authentication.getAuthorities(), "java.util.Collections$UnmodifiableRandomAccessList") + ",\n" + + " \"authorizedClientRegistrationId\": \"" + authentication.getAuthorizedClientRegistrationId() + "\",\n" + + " \"details\": null\n" + + "}"; + // @formatter:on + } + + private static String asJson(DefaultOAuth2User oauth2User) { + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.user.DefaultOAuth2User\",\n" + + " \"authorities\": " + asJson(oauth2User.getAuthorities(), "java.util.Collections$UnmodifiableSet") + ",\n" + + " \"attributes\": {\n" + + " \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" + + " \"username\": \"user\"\n" + + " },\n" + + " \"nameAttributeKey\": \"username\"\n" + + " }"; + // @formatter:on + } + + private static String asJson(DefaultOidcUser oidcUser) { + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser\",\n" + + " \"authorities\": " + asJson(oidcUser.getAuthorities(), "java.util.Collections$UnmodifiableSet") + ",\n" + + " \"idToken\": " + asJson(oidcUser.getIdToken()) + ",\n" + + " \"userInfo\": " + asJson(oidcUser.getUserInfo()) + ",\n" + + " \"nameAttributeKey\": \"" + IdTokenClaimNames.SUB + "\"\n" + + " }"; + // @formatter:on + } + + private static String asJson(Collection authorities, String classTypeInfo) { + OAuth2UserAuthority oauth2UserAuthority = null; + OidcUserAuthority oidcUserAuthority = null; + List simpleAuthorities = new ArrayList<>(); + for (GrantedAuthority authority : authorities) { + if (authority instanceof OidcUserAuthority) { + oidcUserAuthority = (OidcUserAuthority) authority; + } else if (authority instanceof OAuth2UserAuthority) { + oauth2UserAuthority = (OAuth2UserAuthority) authority; + } else if (authority instanceof SimpleGrantedAuthority) { + simpleAuthorities.add((SimpleGrantedAuthority) authority); + } + } + String authoritiesJson = oidcUserAuthority != null ? + asJson(oidcUserAuthority) : + oauth2UserAuthority != null ? + asJson(oauth2UserAuthority) : + ""; + if (!simpleAuthorities.isEmpty()) { + if (!StringUtils.isEmpty(authoritiesJson)) { + authoritiesJson += ","; + } + authoritiesJson += asJson(simpleAuthorities); + } + // @formatter:off + return "[\n" + + " \"" + classTypeInfo + "\",\n" + + " [" + authoritiesJson + "]\n" + + " ]"; + // @formatter:on + } + + private static String asJson(OAuth2UserAuthority oauth2UserAuthority) { + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.user.OAuth2UserAuthority\",\n" + + " \"authority\": \"" + oauth2UserAuthority.getAuthority() + "\",\n" + + " \"attributes\": {\n" + + " \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" + + " \"username\": \"user\"\n" + + " }\n" + + " }"; + // @formatter:on + } + + private static String asJson(OidcUserAuthority oidcUserAuthority) { + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority\",\n" + + " \"authority\": \"" + oidcUserAuthority.getAuthority() + "\",\n" + + " \"idToken\": " + asJson(oidcUserAuthority.getIdToken()) + ",\n" + + " \"userInfo\": " + asJson(oidcUserAuthority.getUserInfo()) + "\n" + + " }"; + // @formatter:on + } + + private static String asJson(List simpleAuthorities) { + // @formatter:off + return simpleAuthorities.stream() + .map(authority -> "{\n" + + " \"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\",\n" + + " \"authority\": \"" + authority.getAuthority() + "\"\n" + + " }") + .collect(Collectors.joining(",")); + // @formatter:on + } + + private static String asJson(OidcIdToken idToken) { + String aud = ""; + if (!CollectionUtils.isEmpty(idToken.getAudience())) { + aud = StringUtils.collectionToDelimitedString(idToken.getAudience(), ",", "\"", "\""); + } + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.oidc.OidcIdToken\",\n" + + " \"tokenValue\": \"" + idToken.getTokenValue() + "\",\n" + + " \"issuedAt\": \"" + dateFormatter.format(Date.from(idToken.getIssuedAt())) + "\",\n" + + " \"expiresAt\": \"" + dateFormatter.format(Date.from(idToken.getExpiresAt())) + "\",\n" + + " \"claims\": {\n" + + " \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" + + " \"iat\": [\n" + + " \"java.time.Instant\",\n" + + " \"" + idToken.getIssuedAt().toString() + "\"\n" + + " ],\n" + + " \"exp\": [\n" + + " \"java.time.Instant\",\n" + + " \"" + idToken.getExpiresAt().toString() + "\"\n" + + " ],\n" + + " \"sub\": \"" + idToken.getSubject() + "\",\n" + + " \"iss\": \"" + idToken.getIssuer() + "\",\n" + + " \"aud\": [\n" + + " \"java.util.Collections$UnmodifiableSet\",\n" + + " [" + aud + "]\n" + + " ],\n" + + " \"azp\": \"" + idToken.getAuthorizedParty() + "\"\n" + + " }\n" + + " }"; + // @formatter:on + } + + private static String asJson(OidcUserInfo userInfo) { + if (userInfo == null) { + return null; + } + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.oidc.OidcUserInfo\",\n" + + " \"claims\": {\n" + + " \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" + + " \"sub\": \"" + userInfo.getSubject() + "\",\n" + + " \"name\": \"" + userInfo.getClaim(NAME) + "\"\n" + + " }\n" + + " }"; + // @formatter:on + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestMixinTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestMixinTests.java new file mode 100644 index 0000000000..f7bec9f1bd --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestMixinTests.java @@ -0,0 +1,197 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Before; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link OAuth2AuthorizationRequestMixin}. + * + * @author Joe Grandja + */ +public class OAuth2AuthorizationRequestMixinTests { + private ObjectMapper mapper; + private OAuth2AuthorizationRequest.Builder authorizationRequestBuilder; + + @Before + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = new ObjectMapper(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + Map additionalParameters = new LinkedHashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + this.authorizationRequestBuilder = TestOAuth2AuthorizationRequests.request() + .scope("read", "write") + .additionalParameters(additionalParameters); + } + + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestBuilder.build(); + String expectedJson = asJson(authorizationRequest); + String json = this.mapper.writeValueAsString(authorizationRequest); + JSONAssert.assertEquals(expectedJson, json, true); + } + + @Test + public void serializeWhenRequiredAttributesOnlyThenSerializes() throws Exception { + OAuth2AuthorizationRequest authorizationRequest = + this.authorizationRequestBuilder + .scopes(null) + .state(null) + .additionalParameters(null) + .attributes(null) + .build(); + String expectedJson = asJson(authorizationRequest); + String json = this.mapper.writeValueAsString(authorizationRequest); + JSONAssert.assertEquals(expectedJson, json, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + String json = asJson(this.authorizationRequestBuilder.build()); + assertThatThrownBy(() -> new ObjectMapper().readValue(json, OAuth2AuthorizationRequest.class)) + .isInstanceOf(JsonProcessingException.class); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + OAuth2AuthorizationRequest expectedAuthorizationRequest = this.authorizationRequestBuilder.build(); + String json = asJson(expectedAuthorizationRequest); + OAuth2AuthorizationRequest authorizationRequest = this.mapper.readValue(json, OAuth2AuthorizationRequest.class); + assertThat(authorizationRequest.getAuthorizationUri()) + .isEqualTo(expectedAuthorizationRequest.getAuthorizationUri()); + assertThat(authorizationRequest.getGrantType()) + .isEqualTo(expectedAuthorizationRequest.getGrantType()); + assertThat(authorizationRequest.getResponseType()) + .isEqualTo(expectedAuthorizationRequest.getResponseType()); + assertThat(authorizationRequest.getClientId()) + .isEqualTo(expectedAuthorizationRequest.getClientId()); + assertThat(authorizationRequest.getRedirectUri()) + .isEqualTo(expectedAuthorizationRequest.getRedirectUri()); + assertThat(authorizationRequest.getScopes()) + .isEqualTo(expectedAuthorizationRequest.getScopes()); + assertThat(authorizationRequest.getState()) + .isEqualTo(expectedAuthorizationRequest.getState()); + assertThat(authorizationRequest.getAdditionalParameters()) + .containsExactlyEntriesOf(expectedAuthorizationRequest.getAdditionalParameters()); + assertThat(authorizationRequest.getAuthorizationRequestUri()) + .isEqualTo(expectedAuthorizationRequest.getAuthorizationRequestUri()); + assertThat(authorizationRequest.getAttributes()) + .containsExactlyEntriesOf(expectedAuthorizationRequest.getAttributes()); + } + + @Test + public void deserializeWhenRequiredAttributesOnlyThenDeserializes() throws Exception { + OAuth2AuthorizationRequest expectedAuthorizationRequest = + this.authorizationRequestBuilder + .scopes(null) + .state(null) + .additionalParameters(null) + .attributes(null) + .build(); + String json = asJson(expectedAuthorizationRequest); + OAuth2AuthorizationRequest authorizationRequest = this.mapper.readValue(json, OAuth2AuthorizationRequest.class); + assertThat(authorizationRequest.getAuthorizationUri()) + .isEqualTo(expectedAuthorizationRequest.getAuthorizationUri()); + assertThat(authorizationRequest.getGrantType()) + .isEqualTo(expectedAuthorizationRequest.getGrantType()); + assertThat(authorizationRequest.getResponseType()) + .isEqualTo(expectedAuthorizationRequest.getResponseType()); + assertThat(authorizationRequest.getClientId()) + .isEqualTo(expectedAuthorizationRequest.getClientId()); + assertThat(authorizationRequest.getRedirectUri()) + .isEqualTo(expectedAuthorizationRequest.getRedirectUri()); + assertThat(authorizationRequest.getScopes()).isEmpty(); + assertThat(authorizationRequest.getState()).isNull(); + assertThat(authorizationRequest.getAdditionalParameters()).isEmpty(); + assertThat(authorizationRequest.getAuthorizationRequestUri()) + .isEqualTo(expectedAuthorizationRequest.getAuthorizationRequestUri()); + assertThat(authorizationRequest.getAttributes()).isEmpty(); + } + + @Test + public void deserializeWhenInvalidAuthorizationGrantTypeThenThrowJsonParseException() { + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestBuilder.build(); + String json = asJson(authorizationRequest).replace("authorization_code", "client_credentials"); + assertThatThrownBy(() -> this.mapper.readValue(json, OAuth2AuthorizationRequest.class)) + .isInstanceOf(JsonParseException.class) + .hasMessageContaining("Invalid authorizationGrantType"); + } + + private static String asJson(OAuth2AuthorizationRequest authorizationRequest) { + String scopes = ""; + if (!CollectionUtils.isEmpty(authorizationRequest.getScopes())) { + scopes = StringUtils.collectionToDelimitedString(authorizationRequest.getScopes(), ",", "\"", "\""); + } + String additionalParameters = "\"@class\": \"java.util.Collections$UnmodifiableMap\""; + if (!CollectionUtils.isEmpty(authorizationRequest.getAdditionalParameters())) { + additionalParameters += "," + authorizationRequest.getAdditionalParameters().keySet().stream() + .map(key -> "\"" + key + "\": \"" + authorizationRequest.getAdditionalParameters().get(key) + "\"") + .collect(Collectors.joining(",")); + } + String attributes = "\"@class\": \"java.util.Collections$UnmodifiableMap\""; + if (!CollectionUtils.isEmpty(authorizationRequest.getAttributes())) { + attributes += "," + authorizationRequest.getAttributes().keySet().stream() + .map(key -> "\"" + key + "\": \"" + authorizationRequest.getAttributes().get(key) + "\"") + .collect(Collectors.joining(",")); + } + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest\",\n" + + " \"authorizationUri\": \"" + authorizationRequest.getAuthorizationUri() + "\",\n" + + " \"authorizationGrantType\": {\n" + + " \"value\": \"" + authorizationRequest.getGrantType().getValue() + "\"\n" + + " },\n" + + " \"responseType\": {\n" + + " \"value\": \"" + authorizationRequest.getResponseType().getValue() + "\"\n" + + " },\n" + + " \"clientId\": \"" + authorizationRequest.getClientId() + "\",\n" + + " \"redirectUri\": \"" + authorizationRequest.getRedirectUri() + "\",\n" + + " \"scopes\": [\n" + + " \"java.util.Collections$UnmodifiableSet\",\n" + + " [" + scopes + "]\n" + + " ],\n" + + " \"state\": " + (authorizationRequest.getState() != null ? "\"" + authorizationRequest.getState() + "\"" : "null") + ",\n" + + " \"additionalParameters\": {\n" + + " " + additionalParameters + "\n" + + " },\n" + + " \"authorizationRequestUri\": \"" + authorizationRequest.getAuthorizationRequestUri() + "\",\n" + + " \"attributes\": {\n" + + " " + attributes + "\n" + + " }\n" + + "}"; + // @formatter:on + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixinTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixinTests.java new file mode 100644 index 0000000000..835760f00d --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixinTests.java @@ -0,0 +1,325 @@ +/* + * Copyright 2002-2020 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.client.jackson2; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Before; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.TestClientRegistrations; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.TestOAuth2AccessTokens; +import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.text.DateFormat; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link OAuth2AuthorizedClientMixin}. + * + * @author Joe Grandja + */ +public class OAuth2AuthorizedClientMixinTests { + private static DateFormat dateFormatter; + private ObjectMapper mapper; + private ClientRegistration.Builder clientRegistrationBuilder; + private OAuth2AccessToken accessToken; + private OAuth2RefreshToken refreshToken; + private String principalName; + + @Before + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = new ObjectMapper(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + dateFormatter = this.mapper.getDateFormat(); + Map providerConfigurationMetadata = new LinkedHashMap<>(); + providerConfigurationMetadata.put("config1", "value1"); + providerConfigurationMetadata.put("config2", "value2"); + this.clientRegistrationBuilder = TestClientRegistrations.clientRegistration() + .scope("read", "write") + .providerConfigurationMetadata(providerConfigurationMetadata); + this.accessToken = TestOAuth2AccessTokens.scopes("read", "write"); + this.refreshToken = TestOAuth2RefreshTokens.refreshToken(); + this.principalName = "principal-name"; + } + + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.clientRegistrationBuilder.build(), this.principalName, this.accessToken, this.refreshToken); + String expectedJson = asJson(authorizedClient); + String json = this.mapper.writeValueAsString(authorizedClient); + JSONAssert.assertEquals(expectedJson, json, true); + } + + @Test + public void serializeWhenRequiredAttributesOnlyThenSerializes() throws Exception { + ClientRegistration clientRegistration = + TestClientRegistrations.clientRegistration() + .clientSecret(null) + .clientName(null) + .userInfoUri(null) + .userNameAttributeName(null) + .jwkSetUri(null) + .build(); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + clientRegistration, this.principalName, TestOAuth2AccessTokens.noScopes()); + String expectedJson = asJson(authorizedClient); + String json = this.mapper.writeValueAsString(authorizedClient); + JSONAssert.assertEquals(expectedJson, json, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.clientRegistrationBuilder.build(), this.principalName, this.accessToken); + String json = asJson(authorizedClient); + assertThatThrownBy(() -> new ObjectMapper().readValue(json, OAuth2AuthorizedClient.class)) + .isInstanceOf(JsonProcessingException.class); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + ClientRegistration expectedClientRegistration = this.clientRegistrationBuilder.build(); + OAuth2AccessToken expectedAccessToken = this.accessToken; + OAuth2RefreshToken expectedRefreshToken = this.refreshToken; + OAuth2AuthorizedClient expectedAuthorizedClient = new OAuth2AuthorizedClient( + expectedClientRegistration, this.principalName, expectedAccessToken, expectedRefreshToken); + String json = asJson(expectedAuthorizedClient); + OAuth2AuthorizedClient authorizedClient = this.mapper.readValue(json, OAuth2AuthorizedClient.class); + ClientRegistration clientRegistration = authorizedClient.getClientRegistration(); + assertThat(clientRegistration.getRegistrationId()) + .isEqualTo(expectedClientRegistration.getRegistrationId()); + assertThat(clientRegistration.getClientId()) + .isEqualTo(expectedClientRegistration.getClientId()); + assertThat(clientRegistration.getClientSecret()) + .isEqualTo(expectedClientRegistration.getClientSecret()); + assertThat(clientRegistration.getClientAuthenticationMethod()) + .isEqualTo(expectedClientRegistration.getClientAuthenticationMethod()); + assertThat(clientRegistration.getAuthorizationGrantType()) + .isEqualTo(expectedClientRegistration.getAuthorizationGrantType()); + assertThat(clientRegistration.getRedirectUriTemplate()) + .isEqualTo(expectedClientRegistration.getRedirectUriTemplate()); + assertThat(clientRegistration.getScopes()) + .isEqualTo(expectedClientRegistration.getScopes()); + assertThat(clientRegistration.getProviderDetails().getAuthorizationUri()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getAuthorizationUri()); + assertThat(clientRegistration.getProviderDetails().getTokenUri()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getTokenUri()); + assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()); + assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()); + assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()); + assertThat(clientRegistration.getProviderDetails().getJwkSetUri()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getJwkSetUri()); + assertThat(clientRegistration.getProviderDetails().getConfigurationMetadata()) + .containsExactlyEntriesOf(clientRegistration.getProviderDetails().getConfigurationMetadata()); + assertThat(clientRegistration.getClientName()) + .isEqualTo(expectedClientRegistration.getClientName()); + assertThat(authorizedClient.getPrincipalName()) + .isEqualTo(expectedAuthorizedClient.getPrincipalName()); + OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); + assertThat(accessToken.getTokenType()) + .isEqualTo(expectedAccessToken.getTokenType()); + assertThat(accessToken.getScopes()) + .isEqualTo(expectedAccessToken.getScopes()); + assertThat(accessToken.getTokenValue()) + .isEqualTo(expectedAccessToken.getTokenValue()); + assertThat(accessToken.getIssuedAt()) + .isEqualTo(expectedAccessToken.getIssuedAt()); + assertThat(accessToken.getExpiresAt()) + .isEqualTo(expectedAccessToken.getExpiresAt()); + OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken(); + assertThat(refreshToken.getTokenValue()) + .isEqualTo(expectedRefreshToken.getTokenValue()); + assertThat(refreshToken.getIssuedAt()) + .isEqualTo(expectedRefreshToken.getIssuedAt()); + assertThat(refreshToken.getExpiresAt()) + .isEqualTo(expectedRefreshToken.getExpiresAt()); + } + + @Test + public void deserializeWhenRequiredAttributesOnlyThenDeserializes() throws Exception { + ClientRegistration expectedClientRegistration = + TestClientRegistrations.clientRegistration() + .clientSecret(null) + .clientName(null) + .userInfoUri(null) + .userNameAttributeName(null) + .jwkSetUri(null) + .build(); + OAuth2AccessToken expectedAccessToken = TestOAuth2AccessTokens.noScopes(); + OAuth2AuthorizedClient expectedAuthorizedClient = new OAuth2AuthorizedClient( + expectedClientRegistration, this.principalName, expectedAccessToken); + String json = asJson(expectedAuthorizedClient); + OAuth2AuthorizedClient authorizedClient = this.mapper.readValue(json, OAuth2AuthorizedClient.class); + ClientRegistration clientRegistration = authorizedClient.getClientRegistration(); + assertThat(clientRegistration.getRegistrationId()) + .isEqualTo(expectedClientRegistration.getRegistrationId()); + assertThat(clientRegistration.getClientId()) + .isEqualTo(expectedClientRegistration.getClientId()); + assertThat(clientRegistration.getClientSecret()).isEmpty(); + assertThat(clientRegistration.getClientAuthenticationMethod()) + .isEqualTo(expectedClientRegistration.getClientAuthenticationMethod()); + assertThat(clientRegistration.getAuthorizationGrantType()) + .isEqualTo(expectedClientRegistration.getAuthorizationGrantType()); + assertThat(clientRegistration.getRedirectUriTemplate()) + .isEqualTo(expectedClientRegistration.getRedirectUriTemplate()); + assertThat(clientRegistration.getScopes()) + .isEqualTo(expectedClientRegistration.getScopes()); + assertThat(clientRegistration.getProviderDetails().getAuthorizationUri()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getAuthorizationUri()); + assertThat(clientRegistration.getProviderDetails().getTokenUri()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getTokenUri()); + assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()).isNull(); + assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()); + assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).isNull(); + assertThat(clientRegistration.getProviderDetails().getJwkSetUri()).isNull(); + assertThat(clientRegistration.getProviderDetails().getConfigurationMetadata()).isEmpty(); + assertThat(clientRegistration.getClientName()) + .isEqualTo(clientRegistration.getRegistrationId()); + assertThat(authorizedClient.getPrincipalName()) + .isEqualTo(expectedAuthorizedClient.getPrincipalName()); + OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); + assertThat(accessToken.getTokenType()) + .isEqualTo(expectedAccessToken.getTokenType()); + assertThat(accessToken.getScopes()).isEmpty(); + assertThat(accessToken.getTokenValue()) + .isEqualTo(expectedAccessToken.getTokenValue()); + assertThat(accessToken.getIssuedAt()) + .isEqualTo(expectedAccessToken.getIssuedAt()); + assertThat(accessToken.getExpiresAt()) + .isEqualTo(expectedAccessToken.getExpiresAt()); + assertThat(authorizedClient.getRefreshToken()).isNull(); + } + + private static String asJson(OAuth2AuthorizedClient authorizedClient) { + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.client.OAuth2AuthorizedClient\",\n" + + " \"clientRegistration\": " + asJson(authorizedClient.getClientRegistration()) + ",\n" + + " \"principalName\": \"" + authorizedClient.getPrincipalName() + "\",\n" + + " \"accessToken\": " + asJson(authorizedClient.getAccessToken()) + ",\n" + + " \"refreshToken\": " + asJson(authorizedClient.getRefreshToken()) + "\n" + + "}"; + // @formatter:on + } + + private static String asJson(ClientRegistration clientRegistration) { + ClientRegistration.ProviderDetails providerDetails = clientRegistration.getProviderDetails(); + ClientRegistration.ProviderDetails.UserInfoEndpoint userInfoEndpoint = providerDetails.getUserInfoEndpoint(); + String scopes = ""; + if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { + scopes = StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), ",", "\"", "\""); + } + String configurationMetadata = "\"@class\": \"java.util.Collections$UnmodifiableMap\""; + if (!CollectionUtils.isEmpty(providerDetails.getConfigurationMetadata())) { + configurationMetadata += "," + providerDetails.getConfigurationMetadata().keySet().stream() + .map(key -> "\"" + key + "\": \"" + providerDetails.getConfigurationMetadata().get(key) + "\"") + .collect(Collectors.joining(",")); + } + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration\",\n" + + " \"registrationId\": \"" + clientRegistration.getRegistrationId() + "\",\n" + + " \"clientId\": \"" + clientRegistration.getClientId() + "\",\n" + + " \"clientSecret\": \"" + clientRegistration.getClientSecret() + "\",\n" + + " \"clientAuthenticationMethod\": {\n" + + " \"value\": \"" + clientRegistration.getClientAuthenticationMethod().getValue() + "\"\n" + + " },\n" + + " \"authorizationGrantType\": {\n" + + " \"value\": \"" + clientRegistration.getAuthorizationGrantType().getValue() + "\"\n" + + " },\n" + + " \"redirectUriTemplate\": \"" + clientRegistration.getRedirectUriTemplate() + "\",\n" + + " \"scopes\": [\n" + + " \"java.util.Collections$UnmodifiableSet\",\n" + + " [" + scopes + "]\n" + + " ],\n" + + " \"providerDetails\": {\n" + + " \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration$ProviderDetails\",\n" + + " \"authorizationUri\": \"" + providerDetails.getAuthorizationUri() + "\",\n" + + " \"tokenUri\": \"" + providerDetails.getTokenUri() + "\",\n" + + " \"userInfoEndpoint\": {\n" + + " \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration$ProviderDetails$UserInfoEndpoint\",\n" + + " \"uri\": " + (userInfoEndpoint.getUri() != null ? "\"" + userInfoEndpoint.getUri() + "\"" : null) + ",\n" + + " \"authenticationMethod\": {\n" + + " \"value\": \"" + userInfoEndpoint.getAuthenticationMethod().getValue() + "\"\n" + + " },\n" + + " \"userNameAttributeName\": " + (userInfoEndpoint.getUserNameAttributeName() != null ? "\"" + userInfoEndpoint.getUserNameAttributeName() + "\"" : null) + "\n" + + " },\n" + + " \"jwkSetUri\": " + (providerDetails.getJwkSetUri() != null ? "\"" + providerDetails.getJwkSetUri() + "\"" : null) + ",\n" + + " \"configurationMetadata\": {\n" + + " " + configurationMetadata + "\n" + + " }\n" + + " },\n" + + " \"clientName\": \"" + clientRegistration.getClientName() + "\"\n" + + "}"; + // @formatter:on + } + + private static String asJson(OAuth2AccessToken accessToken) { + String scopes = ""; + if (!CollectionUtils.isEmpty(accessToken.getScopes())) { + scopes = StringUtils.collectionToDelimitedString(accessToken.getScopes(), ",", "\"", "\""); + } + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.OAuth2AccessToken\",\n" + + " \"tokenType\": {\n" + + " \"value\": \"" + accessToken.getTokenType().getValue() + "\"\n" + + " },\n" + + " \"tokenValue\": \"" + accessToken.getTokenValue() + "\",\n" + + " \"issuedAt\": \"" + dateFormatter.format(Date.from(accessToken.getIssuedAt())) + "\",\n" + + " \"expiresAt\": \"" + dateFormatter.format(Date.from(accessToken.getExpiresAt())) + "\",\n" + + " \"scopes\": [\n" + + " \"java.util.Collections$UnmodifiableSet\",\n" + + " [" + scopes + "]\n" + + " ]\n" + + "}"; + // @formatter:on + } + + private static String asJson(OAuth2RefreshToken refreshToken) { + if (refreshToken == null) { + return null; + } + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.OAuth2RefreshToken\",\n" + + " \"tokenValue\": \"" + refreshToken.getTokenValue() + "\",\n" + + " \"issuedAt\": \"" + dateFormatter.format(Date.from(refreshToken.getIssuedAt())) + "\",\n" + + " \"expiresAt\": " + (refreshToken.getExpiresAt() != null ? "\"" + dateFormatter.format(Date.from(refreshToken.getExpiresAt())) + "\"" : null) + "\n" + + "}"; + // @formatter:on + } +} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java index bdfe6abe60..d53cbf49f4 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -16,15 +16,15 @@ package org.springframework.security.oauth2.core.oidc.user; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import java.time.Instant; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.LinkedHashSet; /** * @author Joe Grandja @@ -32,16 +32,37 @@ import java.util.Map; public class TestOidcUsers { public static DefaultOidcUser create() { - List roles = AuthorityUtils.createAuthorityList("ROLE_USER"); - return new DefaultOidcUser(roles, idToken()); + OidcIdToken idToken = idToken(); + OidcUserInfo userInfo = userInfo(); + return new DefaultOidcUser( + authorities(idToken, userInfo), idToken, userInfo); } private static OidcIdToken idToken() { - Map claims = new HashMap<>(); - claims.put(IdTokenClaimNames.SUB, "subject"); - claims.put(IdTokenClaimNames.ISS, "http://localhost/issuer"); - claims.put(IdTokenClaimNames.AUD, Collections.singletonList("client")); - claims.put(IdTokenClaimNames.AZP, "client"); - return new OidcIdToken("id-token", Instant.now(), Instant.now().plusSeconds(3600), claims); + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plusSeconds(3600); + return OidcIdToken.withTokenValue("id-token") + .issuedAt(issuedAt) + .expiresAt(expiresAt) + .subject("subject") + .issuer("http://localhost/issuer") + .audience(Collections.unmodifiableSet(new LinkedHashSet<>(Collections.singletonList("client")))) + .authorizedParty("client") + .build(); + } + + private static OidcUserInfo userInfo() { + return OidcUserInfo.builder() + .subject("subject") + .name("full name") + .build(); + } + + private static Collection authorities(OidcIdToken idToken, OidcUserInfo userInfo) { + return new LinkedHashSet<>( + Arrays.asList( + new OidcUserAuthority(idToken, userInfo), + new SimpleGrantedAuthority("SCOPE_read"), + new SimpleGrantedAuthority("SCOPE_write"))); } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/user/TestOAuth2Users.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/user/TestOAuth2Users.java index 3e0d7b157d..456f6376aa 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/user/TestOAuth2Users.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/user/TestOAuth2Users.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -17,10 +17,12 @@ package org.springframework.security.oauth2.core.user; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; -import java.util.List; +import java.util.LinkedHashSet; import java.util.Map; /** @@ -29,10 +31,18 @@ import java.util.Map; public class TestOAuth2Users { public static DefaultOAuth2User create() { - List roles = AuthorityUtils.createAuthorityList("ROLE_USER"); - String attrName = "username"; + String nameAttributeKey = "username"; Map attributes = new HashMap<>(); - attributes.put(attrName, "user"); - return new DefaultOAuth2User(roles, attributes, attrName); + attributes.put(nameAttributeKey, "user"); + Collection authorities = authorities(attributes); + return new DefaultOAuth2User(authorities, attributes, nameAttributeKey); + } + + private static Collection authorities(Map attributes) { + return new LinkedHashSet<>( + Arrays.asList( + new OAuth2UserAuthority(attributes), + new SimpleGrantedAuthority("SCOPE_read"), + new SimpleGrantedAuthority("SCOPE_write"))); } }