浏览代码

Add Jackson support for oauth2-client session related classes

Fixes gh-4886
Joe Grandja 5 年之前
父节点
当前提交
04f3fe8af9
共有 28 个文件被更改,包括 2044 次插入33 次删除
  1. 15 1
      core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java
  2. 1 0
      gradle/dependency-management.gradle
  3. 3 1
      oauth2/oauth2-client/spring-security-oauth2-client.gradle
  4. 84 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationDeserializer.java
  5. 40 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationMixin.java
  6. 49 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserMixin.java
  7. 51 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOidcUserMixin.java
  8. 68 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/JsonNodeUtils.java
  9. 53 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AccessTokenMixin.java
  10. 49 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixin.java
  11. 76 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestDeserializer.java
  12. 40 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestMixin.java
  13. 49 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixin.java
  14. 105 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2ClientJackson2Module.java
  15. 48 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2RefreshTokenMixin.java
  16. 46 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2UserAuthorityMixin.java
  17. 51 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcIdTokenMixin.java
  18. 47 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcUserAuthorityMixin.java
  19. 44 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OidcUserInfoMixin.java
  20. 92 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/StdConverters.java
  21. 52 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapDeserializer.java
  22. 42 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapMixin.java
  23. 14 10
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/TestOAuth2AuthenticationTokens.java
  24. 351 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixinTests.java
  25. 197 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestMixinTests.java
  26. 325 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixinTests.java
  27. 35 14
      oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java
  28. 17 7
      oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/user/TestOAuth2Users.java

+ 15 - 1
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());
  * </pre>
  *
  * @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"
 		)));
 

+ 1 - 0
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"

+ 3 - 1
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'
 }

+ 84 - 0
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<ClientRegistration> {
+	private static final StdConverter<JsonNode, ClientAuthenticationMethod> CLIENT_AUTHENTICATION_METHOD_CONVERTER =
+			new StdConverters.ClientAuthenticationMethodConverter();
+	private static final StdConverter<JsonNode, AuthorizationGrantType> AUTHORIZATION_GRANT_TYPE_CONVERTER =
+			new StdConverters.AuthorizationGrantTypeConverter();
+	private static final StdConverter<JsonNode, AuthenticationMethod> 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();
+	}
+}

+ 40 - 0
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 {
+}

+ 49 - 0
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<? extends GrantedAuthority> authorities,
+			@JsonProperty("attributes") Map<String, Object> attributes,
+			@JsonProperty("nameAttributeKey") String nameAttributeKey) {
+	}
+}

+ 51 - 0
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<? extends GrantedAuthority> authorities,
+			@JsonProperty("idToken") OidcIdToken idToken,
+			@JsonProperty("userInfo") OidcUserInfo userInfo,
+			@JsonProperty("nameAttributeKey") String nameAttributeKey) {
+	}
+}

+ 68 - 0
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<String>> SET_TYPE_REFERENCE = new TypeReference<Set<String>>() {};
+	static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<Map<String, Object>>() {};
+
+
+	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> T findValue(JsonNode jsonNode, String fieldName, TypeReference<T> 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;
+	}
+}

+ 53 - 0
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<String> scopes) {
+	}
+}

+ 49 - 0
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<? extends GrantedAuthority> authorities,
+			@JsonProperty("authorizedClientRegistrationId") String authorizedClientRegistrationId) {
+	}
+}

+ 76 - 0
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<OAuth2AuthorizationRequest> {
+	private static final StdConverter<JsonNode, AuthorizationGrantType> 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();
+	}
+}

+ 40 - 0
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 {
+}

+ 49 - 0
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) {
+	}
+}

+ 105 - 0
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:
+ *
+ * <ul>
+ *     <li>{@link OAuth2AuthorizationRequestMixin}</li>
+ *     <li>{@link ClientRegistrationMixin}</li>
+ *     <li>{@link OAuth2AccessTokenMixin}</li>
+ *     <li>{@link OAuth2RefreshTokenMixin}</li>
+ *     <li>{@link OAuth2AuthorizedClientMixin}</li>
+ *     <li>{@link OAuth2UserAuthorityMixin}</li>
+ *     <li>{@link DefaultOAuth2UserMixin}</li>
+ *     <li>{@link OidcIdTokenMixin}</li>
+ *     <li>{@link OidcUserInfoMixin}</li>
+ *     <li>{@link OidcUserAuthorityMixin}</li>
+ *     <li>{@link DefaultOidcUserMixin}</li>
+ *     <li>{@link OAuth2AuthenticationTokenMixin}</li>
+ * </ul>
+ *
+ * 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.
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new OAuth2ClientJackson2Module());
+ * </pre>
+ *
+ * <b>NOTE:</b> 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);
+	}
+}

+ 48 - 0
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) {
+	}
+}

+ 46 - 0
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<String, Object> attributes) {
+	}
+}

+ 51 - 0
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<String, Object> claims) {
+	}
+}

+ 47 - 0
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) {
+	}
+}

+ 44 - 0
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<String, Object> claims) {
+	}
+}

+ 92 - 0
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<JsonNode, OAuth2AccessToken.TokenType> {
+		@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<JsonNode, ClientAuthenticationMethod> {
+		@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<JsonNode, AuthorizationGrantType> {
+		@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<JsonNode, AuthenticationMethod> {
+		@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;
+		}
+	}
+}

+ 52 - 0
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<Map<?, ?>> {
+
+	@Override
+	public Map<?, ?> deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+		ObjectMapper mapper = (ObjectMapper) parser.getCodec();
+		JsonNode mapNode = mapper.readTree(parser);
+		Map<String, Object> result = new LinkedHashMap<>();
+		if (mapNode != null && mapNode.isObject()) {
+			Iterable<Map.Entry<String, JsonNode>> fields = mapNode::fields;
+			for (Map.Entry<String, JsonNode> field : fields) {
+				result.put(field.getKey(), mapper.readValue(field.getValue().traverse(mapper), Object.class));
+			}
+		}
+		return Collections.unmodifiableMap(result);
+	}
+}

+ 42 - 0
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) {
+	}
+}

+ 14 - 10
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<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(roles);
+
+	public static OAuth2AuthenticationToken authenticated() {
+		DefaultOAuth2User principal = TestOAuth2Users.create();
+		String registrationId = "registration-id";
+		return new OAuth2AuthenticationToken(principal, principal.getAuthorities(), registrationId);
+	}
+
+	public static OAuth2AuthenticationToken oidcAuthenticated() {
+		DefaultOidcUser principal = TestOidcUsers.create();
 		String registrationId = "registration-id";
-		return new OAuth2AuthenticationToken(principal, authorities, registrationId);
+		return new OAuth2AuthenticationToken(principal, principal.getAuthorities(), registrationId);
 	}
 }

+ 351 - 0
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<? extends GrantedAuthority> authorities, String classTypeInfo) {
+		OAuth2UserAuthority oauth2UserAuthority = null;
+		OidcUserAuthority oidcUserAuthority = null;
+		List<SimpleGrantedAuthority> 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<SimpleGrantedAuthority> 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
+	}
+}

+ 197 - 0
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<String, Object> 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
+	}
+}

+ 325 - 0
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<String, Object> 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
+	}
+}

+ 35 - 14
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<GrantedAuthority> 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<String, Object> 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<? extends GrantedAuthority> authorities(OidcIdToken idToken, OidcUserInfo userInfo) {
+		return new LinkedHashSet<>(
+				Arrays.asList(
+						new OidcUserAuthority(idToken, userInfo),
+						new SimpleGrantedAuthority("SCOPE_read"),
+						new SimpleGrantedAuthority("SCOPE_write")));
 	}
 }

+ 17 - 7
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<GrantedAuthority> roles = AuthorityUtils.createAuthorityList("ROLE_USER");
-		String attrName = "username";
+		String nameAttributeKey = "username";
 		Map<String, Object> attributes = new HashMap<>();
-		attributes.put(attrName, "user");
-		return new DefaultOAuth2User(roles, attributes, attrName);
+		attributes.put(nameAttributeKey, "user");
+		Collection<GrantedAuthority> authorities = authorities(attributes);
+		return new DefaultOAuth2User(authorities, attributes, nameAttributeKey);
+	}
+
+	private static Collection<GrantedAuthority> authorities(Map<String, Object> attributes) {
+		return new LinkedHashSet<>(
+				Arrays.asList(
+						new OAuth2UserAuthority(attributes),
+						new SimpleGrantedAuthority("SCOPE_read"),
+						new SimpleGrantedAuthority("SCOPE_write")));
 	}
 }