Pārlūkot izejas kodu

Add JSON Serialization

Fixes gh-3812
Jitendra Singh Bisht 9 gadi atpakaļ
vecāks
revīzija
d77ca17e95
47 mainītis faili ar 2791 papildinājumiem un 69 dzēšanām
  1. 4 1
      cas/cas.gradle
  2. 53 16
      cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java
  3. 62 0
      cas/src/main/java/org/springframework/security/cas/jackson2/AssertionImplMixin.java
  4. 58 0
      cas/src/main/java/org/springframework/security/cas/jackson2/AttributePrincipalImplMixin.java
  5. 77 0
      cas/src/main/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixin.java
  6. 56 0
      cas/src/main/java/org/springframework/security/cas/jackson2/CasJackson2Module.java
  7. 123 0
      cas/src/test/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixinTests.java
  8. 4 2
      core/core.gradle
  9. 3 4
      core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java
  10. 31 10
      core/src/main/java/org/springframework/security/authentication/AnonymousAuthenticationToken.java
  11. 19 4
      core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java
  12. 59 0
      core/src/main/java/org/springframework/security/jackson2/AnonymousAuthenticationTokenMixin.java
  13. 63 0
      core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java
  14. 66 0
      core/src/main/java/org/springframework/security/jackson2/RememberMeAuthenticationTokenMixin.java
  15. 106 0
      core/src/main/java/org/springframework/security/jackson2/SecurityJacksonModules.java
  16. 56 0
      core/src/main/java/org/springframework/security/jackson2/SimpleGrantedAuthorityMixin.java
  17. 47 0
      core/src/main/java/org/springframework/security/jackson2/UnmodifiableSetMixin.java
  18. 72 0
      core/src/main/java/org/springframework/security/jackson2/UserDeserializer.java
  19. 49 0
      core/src/main/java/org/springframework/security/jackson2/UserMixin.java
  20. 85 0
      core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java
  21. 48 0
      core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenMixin.java
  22. 26 0
      core/src/main/java/org/springframework/security/jackson2/package-info.java
  23. 52 0
      core/src/test/java/org/springframework/security/jackson2/AbstractMixinTests.java
  24. 95 0
      core/src/test/java/org/springframework/security/jackson2/AnonymousAuthenticationTokenMixinTests.java
  25. 105 0
      core/src/test/java/org/springframework/security/jackson2/RememberMeAuthenticationTokenMixinTests.java
  26. 64 0
      core/src/test/java/org/springframework/security/jackson2/SecurityContextMixinTests.java
  27. 61 0
      core/src/test/java/org/springframework/security/jackson2/SimpleGrantedAuthorityMixinTests.java
  28. 105 0
      core/src/test/java/org/springframework/security/jackson2/UserDeserializerTests.java
  29. 117 0
      core/src/test/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenMixinTests.java
  30. 1 0
      gradle/javaprojects.gradle
  31. 11 0
      web/src/main/java/org/springframework/security/web/authentication/WebAuthenticationDetails.java
  32. 61 0
      web/src/main/java/org/springframework/security/web/jackson2/CookieDeserializer.java
  33. 40 0
      web/src/main/java/org/springframework/security/web/jackson2/CookieMixin.java
  34. 54 0
      web/src/main/java/org/springframework/security/web/jackson2/DefaultCsrfTokenMixin.java
  35. 44 0
      web/src/main/java/org/springframework/security/web/jackson2/DefaultSavedRequestMixin.java
  36. 48 0
      web/src/main/java/org/springframework/security/web/jackson2/SavedCookieMixin.java
  37. 44 0
      web/src/main/java/org/springframework/security/web/jackson2/WebAuthenticationDetailsMixin.java
  38. 61 0
      web/src/main/java/org/springframework/security/web/jackson2/WebJackson2Module.java
  39. 22 0
      web/src/main/java/org/springframework/security/web/jackson2/package-info.java
  40. 174 30
      web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java
  41. 41 0
      web/src/test/java/org/springframework/security/web/jackson2/AbstractMixinTests.java
  42. 61 0
      web/src/test/java/org/springframework/security/web/jackson2/CookieMixinTests.java
  43. 80 0
      web/src/test/java/org/springframework/security/web/jackson2/DefaultCsrfTokenMixinTests.java
  44. 95 0
      web/src/test/java/org/springframework/security/web/jackson2/DefaultSavedRequestMixinTests.java
  45. 96 0
      web/src/test/java/org/springframework/security/web/jackson2/SavedCookieMixinTests.java
  46. 88 0
      web/src/test/java/org/springframework/security/web/jackson2/WebAuthenticationDetailsMixinTests.java
  47. 4 2
      web/web.gradle

+ 4 - 1
cas/cas.gradle

@@ -8,7 +8,10 @@ dependencies {
 			"org.springframework:spring-web:$springVersion",
 			"org.jasig.cas.client:cas-client-core:$casClientVersion"
 
-	optional "net.sf.ehcache:ehcache:$ehcacheVersion"
+	optional "net.sf.ehcache:ehcache:$ehcacheVersion",
+			"com.fasterxml.jackson.core:jackson-databind:$jacksonDatavindVersion"
+
+	testCompile "org.skyscreamer:jsonassert:$jsonassertVersion"
 
 	provided "javax.servlet:javax.servlet-api:$servletApiVersion"
 }

+ 53 - 16
cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java

@@ -50,29 +50,54 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
 	/**
 	 * Constructor.
 	 *
-	 * @param key to identify if this object made by a given
-	 * {@link CasAuthenticationProvider}
-	 * @param principal typically the UserDetails object (cannot be <code>null</code>)
+	 * @param key         to identify if this object made by a given
+	 *                    {@link CasAuthenticationProvider}
+	 * @param principal   typically the UserDetails object (cannot be <code>null</code>)
 	 * @param credentials the service/proxy ticket ID from CAS (cannot be
-	 * <code>null</code>)
+	 *                    <code>null</code>)
 	 * @param authorities the authorities granted to the user (from the
-	 * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot
-	 * be <code>null</code>)
+	 *                    {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot
+	 *                    be <code>null</code>)
 	 * @param userDetails the user details (from the
-	 * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot
-	 * be <code>null</code>)
-	 * @param assertion the assertion returned from the CAS servers. It contains the
-	 * principal and how to obtain a proxy ticket for the user.
-	 *
+	 *                    {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot
+	 *                    be <code>null</code>)
+	 * @param assertion   the assertion returned from the CAS servers. It contains the
+	 *                    principal and how to obtain a proxy ticket for the user.
 	 * @throws IllegalArgumentException if a <code>null</code> was passed
 	 */
 	public CasAuthenticationToken(final String key, final Object principal,
-			final Object credentials,
-			final Collection<? extends GrantedAuthority> authorities,
-			final UserDetails userDetails, final Assertion assertion) {
+								final Object credentials,
+								final Collection<? extends GrantedAuthority> authorities,
+								final UserDetails userDetails, final Assertion assertion) {
+		this(extractKeyHash(key), principal, credentials, authorities, userDetails, assertion);
+	}
+
+	/**
+	 * Private constructor for Jackson Deserialization support
+	 *
+	 * @param keyHash     hashCode of provided key to identify if this object made by a given
+	 *                    {@link CasAuthenticationProvider}
+	 * @param principal   typically the UserDetails object (cannot be <code>null</code>)
+	 * @param credentials the service/proxy ticket ID from CAS (cannot be
+	 *                    <code>null</code>)
+	 * @param authorities the authorities granted to the user (from the
+	 *                    {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot
+	 *                    be <code>null</code>)
+	 * @param userDetails the user details (from the
+	 *                    {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot
+	 *                    be <code>null</code>)
+	 * @param assertion   the assertion returned from the CAS servers. It contains the
+	 *                    principal and how to obtain a proxy ticket for the user.
+	 * @throws IllegalArgumentException if a <code>null</code> was passed
+	 * @since 4.2
+	 */
+	private CasAuthenticationToken(final Integer keyHash, final Object principal,
+									final Object credentials,
+									final Collection<? extends GrantedAuthority> authorities,
+									final UserDetails userDetails, final Assertion assertion) {
 		super(authorities);
 
-		if ((key == null) || ("".equals(key)) || (principal == null)
+		if ((principal == null)
 				|| "".equals(principal) || (credentials == null)
 				|| "".equals(credentials) || (authorities == null)
 				|| (userDetails == null) || (assertion == null)) {
@@ -80,7 +105,7 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
 					"Cannot pass null or empty values to constructor");
 		}
 
-		this.keyHash = key.hashCode();
+		this.keyHash = keyHash;
 		this.principal = principal;
 		this.credentials = credentials;
 		this.userDetails = userDetails;
@@ -91,6 +116,18 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
 	// ~ Methods
 	// ========================================================================================================
 
+	private static Integer extractKeyHash(String key) {
+		Object value = nullSafeValue(key);
+		return value.hashCode();
+	}
+
+	private static Object nullSafeValue(Object value) {
+		if (value == null || "".equals(value)) {
+			throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
+		}
+		return value;
+	}
+
 	public boolean equals(final Object obj) {
 		if (!super.equals(obj)) {
 			return false;

+ 62 - 0
cas/src/main/java/org/springframework/security/cas/jackson2/AssertionImplMixin.java

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.cas.jackson2;
+
+import com.fasterxml.jackson.annotation.*;
+import org.jasig.cas.client.authentication.AttributePrincipal;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * Helps in jackson deserialization of class {@link org.jasig.cas.client.validation.AssertionImpl}, which is
+ * used with {@link org.springframework.security.cas.authentication.CasAuthenticationToken}.
+ * To use this class we need to register with {@link com.fasterxml.jackson.databind.ObjectMapper}. Type information
+ * will be stored in @class property.
+ * <p>
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CasJackson2Module());
+ * </pre>
+ *
+ *
+ * @author Jitendra Singh
+ * @see CasJackson2Module
+ * @see org.springframework.security.jackson2.SecurityJacksonModules
+ * @since 4.2
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY,
+		getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AssertionImplMixin {
+
+	/**
+	 * Mixin Constructor helps in deserialize {@link org.jasig.cas.client.validation.AssertionImpl}
+	 *
+	 * @param principal the Principal to associate with the Assertion.
+	 * @param validFromDate when the assertion is valid from.
+	 * @param validUntilDate when the assertion is valid to.
+	 * @param authenticationDate when the assertion is authenticated.
+	 * @param attributes the key/value pairs for this attribute.
+	 */
+	@JsonCreator
+	public AssertionImplMixin(@JsonProperty("principal") AttributePrincipal principal,
+								@JsonProperty("validFromDate") Date validFromDate, @JsonProperty("validUntilDate") Date validUntilDate,
+								@JsonProperty("authenticationDate") Date authenticationDate, @JsonProperty("attributes") Map<String, Object> attributes){
+	}
+}

+ 58 - 0
cas/src/main/java/org/springframework/security/cas/jackson2/AttributePrincipalImplMixin.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.cas.jackson2;
+
+import com.fasterxml.jackson.annotation.*;
+import org.jasig.cas.client.proxy.ProxyRetriever;
+
+import java.util.Map;
+
+/**
+ * Helps in deserialize {@link org.jasig.cas.client.authentication.AttributePrincipalImpl} which is used with
+ * {@link org.springframework.security.cas.authentication.CasAuthenticationToken}. Type information will be stored
+ * in property named @class.
+ * <p>
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CasJackson2Module());
+ * </pre>
+ *
+ * @author Jitendra Singh
+ * @see CasJackson2Module
+ * @see org.springframework.security.jackson2.SecurityJacksonModules
+ * @since 4.2
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
+		isGetterVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AttributePrincipalImplMixin {
+
+	/**
+	 * Mixin Constructor helps in deserialize {@link org.jasig.cas.client.authentication.AttributePrincipalImpl}
+	 *
+	 * @param name the unique identifier for the principal.
+	 * @param attributes the key/value pairs for this principal.
+	 * @param proxyGrantingTicket the ticket associated with this principal.
+	 * @param proxyRetriever the ProxyRetriever implementation to call back to the CAS server.
+	 */
+	@JsonCreator
+	public AttributePrincipalImplMixin(@JsonProperty("name") String name, @JsonProperty("attributes") Map<String, Object> attributes,
+										@JsonProperty("proxyGrantingTicket") String proxyGrantingTicket,
+										@JsonProperty("proxyRetriever") ProxyRetriever proxyRetriever) {
+	}
+}

+ 77 - 0
cas/src/main/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixin.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.cas.jackson2;
+
+import com.fasterxml.jackson.annotation.*;
+import org.jasig.cas.client.validation.Assertion;
+import org.springframework.security.cas.authentication.CasAuthenticationProvider;
+import org.springframework.security.cas.authentication.CasAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+
+/**
+ * Mixin class which helps in deserialize {@link org.springframework.security.cas.authentication.CasAuthenticationToken}
+ * using jackson. Two more dependent classes needs to register along with this mixin class.
+ * <ol>
+ * 		<li>{@link org.springframework.security.cas.jackson2.AssertionImplMixin}</li>
+ *      <li>{@link org.springframework.security.cas.jackson2.AttributePrincipalImplMixin}</li>
+ * </ol>
+ *
+ * <p>
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CasJackson2Module());
+ * </pre>
+ *
+ * @author Jitendra Singh
+ * @see CasJackson2Module
+ * @see org.springframework.security.jackson2.SecurityJacksonModules
+ * @since 4.2
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, isGetterVisibility = JsonAutoDetect.Visibility.NONE,
+		getterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.ANY)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CasAuthenticationTokenMixin {
+
+	/**
+	 * Mixin Constructor helps in deserialize {@link CasAuthenticationToken}
+	 *
+	 * @param keyHash hashCode of provided key to identify if this object made by a given
+	 * {@link CasAuthenticationProvider}
+	 * @param principal typically the UserDetails object (cannot be <code>null</code>)
+	 * @param credentials the service/proxy ticket ID from CAS (cannot be
+	 * <code>null</code>)
+	 * @param authorities the authorities granted to the user (from the
+	 * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot
+	 * be <code>null</code>)
+	 * @param userDetails the user details (from the
+	 * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot
+	 * be <code>null</code>)
+	 * @param assertion the assertion returned from the CAS servers. It contains the
+	 * principal and how to obtain a proxy ticket for the user.
+	 */
+	@JsonCreator
+	public CasAuthenticationTokenMixin(@JsonProperty("keyHash") Integer keyHash, @JsonProperty("principal") Object principal,
+										@JsonProperty("credentials") Object credentials,
+										@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
+										@JsonProperty("userDetails") UserDetails userDetails, @JsonProperty("assertion") Assertion assertion) {
+	}
+}

+ 56 - 0
cas/src/main/java/org/springframework/security/cas/jackson2/CasJackson2Module.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.cas.jackson2;
+
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import org.jasig.cas.client.authentication.AttributePrincipalImpl;
+import org.jasig.cas.client.validation.AssertionImpl;
+import org.springframework.security.cas.authentication.CasAuthenticationToken;
+import org.springframework.security.jackson2.SecurityJacksonModules;
+
+/**
+ * Jackson module for spring-security-cas. This module register {@link AssertionImplMixin},
+ * {@link AttributePrincipalImplMixin} and {@link CasAuthenticationTokenMixin}. If no default typing enabled by default then
+ * it'll enable it because typing info is needed to properly serialize/deserialize objects. In order to use this module just
+ * add this module into your ObjectMapper configuration.
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CasJackson2Module());
+ * </pre>
+ *  <b>Note: use {@link SecurityJacksonModules#getModules()} to get list of all security modules.</b>
+ *
+ * @author Jitendra Singh.
+ * @see org.springframework.security.jackson2.SecurityJacksonModules
+ * @since 4.2
+ */
+public class CasJackson2Module extends SimpleModule {
+
+	public CasJackson2Module() {
+		super(CasJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null));
+	}
+
+	@Override
+	public void setupModule(SetupContext context) {
+		SecurityJacksonModules.enableDefaultTyping((ObjectMapper) context.getOwner());
+		context.setMixInAnnotations(AssertionImpl.class, AssertionImplMixin.class);
+		context.setMixInAnnotations(AttributePrincipalImpl.class, AttributePrincipalImplMixin.class);
+		context.setMixInAnnotations(CasAuthenticationToken.class, CasAuthenticationTokenMixin.class);
+	}
+}

+ 123 - 0
cas/src/test/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixinTests.java

@@ -0,0 +1,123 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.cas.jackson2;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.jasig.cas.client.authentication.AttributePrincipalImpl;
+import org.jasig.cas.client.validation.Assertion;
+import org.jasig.cas.client.validation.AssertionImpl;
+import org.json.JSONException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.security.cas.authentication.CasAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.jackson2.SecurityJacksonModules;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jitendra Singh
+ * @since 4.2
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class CasAuthenticationTokenMixinTests {
+
+	private final String KEY = "casKey";
+	private final String PASSWORD = "pass";
+	Date startDate = new Date();
+	Date endDate = new Date();
+	String expectedJson = "{\"@class\": \"org.springframework.security.cas.authentication.CasAuthenticationToken\", \"keyHash\": " + KEY.hashCode() + "," +
+			"\"principal\": {\"@class\": \"org.springframework.security.core.userdetails.User\", \"username\": \"username\", \"password\": %s, \"accountNonExpired\": true, \"enabled\": true," +
+			"\"accountNonLocked\": true, \"credentialsNonExpired\": true, \"authorities\": [\"java.util.Collections$UnmodifiableSet\"," +
+			"[{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"role\": \"USER\"}]]}, \"credentials\": \"" + PASSWORD + "\", \"authorities\": [\"java.util.ArrayList\", [{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"role\": \"ROLE_USER\"}]]," +
+			"\"userDetails\": {\"@class\": \"org.springframework.security.core.userdetails.User\",\"username\": \"user\", \"password\": \"" + PASSWORD + "\", \"enabled\": true, \"accountNonExpired\": true, \"accountNonLocked\": true, \"credentialsNonExpired\": true, \"authorities\": [\"java.util.Collections$UnmodifiableSet\", [{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"role\": \"ROLE_USER\"}]]}," +
+			"\"authenticated\": true, \"details\": null," +
+			"\"assertion\": {" +
+			"\"@class\": \"org.jasig.cas.client.validation.AssertionImpl\", \"principal\": {\"@class\": \"org.jasig.cas.client.authentication.AttributePrincipalImpl\", \"name\": \"assertName\", \"attributes\": {\"@class\": \"java.util.Collections$EmptyMap\"}, \"proxyGrantingTicket\": null, \"proxyRetriever\": null}, " +
+			"\"validFromDate\": [\"java.util.Date\", " + startDate.getTime() + "], \"validUntilDate\": [\"java.util.Date\", " + endDate.getTime() + "]," +
+			"\"authenticationDate\": [\"java.util.Date\", " + startDate.getTime() + "], \"attributes\": {\"@class\": \"java.util.Collections$EmptyMap\"}" +
+			"}}";
+
+	private CasAuthenticationToken createCasAuthenticationToken() {
+		User principal = new User("username", PASSWORD, Collections.singletonList(new SimpleGrantedAuthority("USER")));
+		Collection<? extends GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
+		Assertion assertion = new AssertionImpl(new AttributePrincipalImpl("assertName"), startDate, endDate, startDate, Collections.<String, Object>emptyMap());
+		return new CasAuthenticationToken(KEY, principal, principal.getPassword(), authorities,
+				new User("user", PASSWORD, authorities), assertion);
+	}
+
+	ObjectMapper buildObjectMapper() {
+		ObjectMapper mapper = new ObjectMapper();
+		mapper.registerModules(SecurityJacksonModules.getModules());
+		return mapper;
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void nullKeyTest() {
+		new CasAuthenticationToken(null, "user", PASSWORD, Collections.<GrantedAuthority>emptyList(),
+				new User("user", PASSWORD, Collections.<GrantedAuthority>emptyList()), null);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void blankKeyTest() {
+		new CasAuthenticationToken("", "user", PASSWORD, Collections.<GrantedAuthority>emptyList(),
+				new User("user", PASSWORD, Collections.<GrantedAuthority>emptyList()), null);
+	}
+
+	@Test
+	public void serializeCasAuthenticationTest() throws JsonProcessingException, JSONException {
+		CasAuthenticationToken token = createCasAuthenticationToken();
+		String actualJson = buildObjectMapper().writeValueAsString(token);
+		JSONAssert.assertEquals(String.format(expectedJson, "\"" + PASSWORD + "\""), actualJson, true);
+	}
+
+	@Test
+	public void serializeCasAuthenticationTestAfterEraseCredentialInvoked() throws JsonProcessingException, JSONException {
+		CasAuthenticationToken token = createCasAuthenticationToken();
+		token.eraseCredentials();
+		String actualJson = buildObjectMapper().writeValueAsString(token);
+		JSONAssert.assertEquals(String.format(expectedJson, "null"), actualJson, true);
+	}
+
+	@Test
+	public void deserializeCasAuthenticationTest() throws IOException, JSONException {
+		CasAuthenticationToken token = buildObjectMapper().readValue(String.format(expectedJson, "\"" + PASSWORD + "\""), CasAuthenticationToken.class);
+		assertThat(token).isNotNull();
+		assertThat(token.getPrincipal()).isNotNull().isInstanceOf(User.class);
+		assertThat(((User) token.getPrincipal()).getUsername()).isEqualTo("username");
+		assertThat(((User) token.getPrincipal()).getPassword()).isEqualTo(PASSWORD);
+		assertThat(token.getUserDetails()).isNotNull().isInstanceOf(User.class);
+		assertThat(token.getAssertion()).isNotNull().isInstanceOf(AssertionImpl.class);
+		assertThat(token.getKeyHash()).isEqualTo(KEY.hashCode());
+		assertThat(token.getUserDetails().getAuthorities()).hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER"));
+		assertThat(token.getAssertion().getAuthenticationDate()).isEqualTo(startDate);
+		assertThat(token.getAssertion().getValidFromDate()).isEqualTo(startDate);
+		assertThat(token.getAssertion().getValidUntilDate()).isEqualTo(endDate);
+		assertThat(token.getAssertion().getPrincipal().getName()).isEqualTo("assertName");
+		assertThat(token.getAssertion().getAttributes()).hasSize(0);
+	}
+}

+ 4 - 2
core/core.gradle

@@ -24,14 +24,16 @@ dependencies {
 			 'javax.annotation:jsr250-api:1.0',
 			 "org.aspectj:aspectjrt:$aspectjVersion",
 			 "org.springframework:spring-jdbc:$springVersion",
-			 "org.springframework:spring-tx:$springVersion"
+			 "org.springframework:spring-tx:$springVersion",
+			 "com.fasterxml.jackson.core:jackson-databind:$jacksonDatavindVersion"
 
 	included cryptoProject
 
 	testCompile "commons-collections:commons-collections:$commonsCollectionsVersion",
 				"org.springframework:spring-test:$springVersion",
 				"org.slf4j:jcl-over-slf4j:$slf4jVersion",
-				powerMockDependencies
+				powerMockDependencies,
+				"org.skyscreamer:jsonassert:$jsonassertVersion"
 
 	testRuntime "org.hsqldb:hsqldb:$hsqlVersion"
 }

+ 3 - 4
core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java

@@ -40,8 +40,8 @@ public abstract class AbstractAuthenticationToken implements Authentication,
 	// ~ Instance fields
 	// ================================================================================================
 
-	private Object details;
 	private final Collection<GrantedAuthority> authorities;
+	private Object details;
 	private boolean authenticated = false;
 
 	// ~ Constructors
@@ -51,7 +51,7 @@ public abstract class AbstractAuthenticationToken implements Authentication,
 	 * Creates a token with the supplied array of authorities.
 	 *
 	 * @param authorities the collection of <tt>GrantedAuthority</tt>s for the principal
-	 * represented by this authentication object.
+	 *                    represented by this authentication object.
 	 */
 	public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
 		if (authorities == null) {
@@ -215,8 +215,7 @@ public abstract class AbstractAuthenticationToken implements Authentication,
 
 				sb.append(authority);
 			}
-		}
-		else {
+		} else {
 			sb.append("Not granted any authorities");
 		}
 

+ 31 - 10
core/src/main/java/org/springframework/security/authentication/AnonymousAuthenticationToken.java

@@ -41,24 +41,33 @@ public class AnonymousAuthenticationToken extends AbstractAuthenticationToken im
 	/**
 	 * Constructor.
 	 *
-	 * @param key to identify if this object made by an authorised client
-	 * @param principal the principal (typically a <code>UserDetails</code>)
+	 * @param key         to identify if this object made by an authorised client
+	 * @param principal   the principal (typically a <code>UserDetails</code>)
 	 * @param authorities the authorities granted to the principal
-	 *
 	 * @throws IllegalArgumentException if a <code>null</code> was passed
 	 */
 	public AnonymousAuthenticationToken(String key, Object principal,
-			Collection<? extends GrantedAuthority> authorities) {
+										Collection<? extends GrantedAuthority> authorities) {
+		this(extractKeyHash(key), nullSafeValue(principal), authorities);
+	}
+
+	/**
+	 * Constructor helps in Jackson Deserialization
+	 *
+	 * @param keyHash     hashCode of provided Key, constructed by above constructor
+	 * @param principal   the principal (typically a <code>UserDetails</code>)
+	 * @param authorities the authorities granted to the principal
+	 * @since 4.2
+	 */
+	private AnonymousAuthenticationToken(Integer keyHash, Object principal,
+										Collection<? extends GrantedAuthority> authorities) {
 		super(authorities);
 
-		if ((key == null) || ("".equals(key)) || (principal == null)
-				|| "".equals(principal) || (authorities == null)
-				|| (authorities.isEmpty())) {
-			throw new IllegalArgumentException(
-					"Cannot pass null or empty values to constructor");
+		if (authorities == null || authorities.isEmpty()) {
+			throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
 		}
 
-		this.keyHash = key.hashCode();
+		this.keyHash = keyHash;
 		this.principal = principal;
 		setAuthenticated(true);
 	}
@@ -66,6 +75,18 @@ public class AnonymousAuthenticationToken extends AbstractAuthenticationToken im
 	// ~ Methods
 	// ========================================================================================================
 
+	private static Integer extractKeyHash(String key) {
+		Object value = nullSafeValue(key);
+		return value.hashCode();
+	}
+
+	private static Object nullSafeValue(Object value) {
+		if (value == null || "".equals(value)) {
+			throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
+		}
+		return value;
+	}
+
 	public boolean equals(Object obj) {
 		if (!super.equals(obj)) {
 			return false;

+ 19 - 4
core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java

@@ -46,14 +46,13 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
 	/**
 	 * Constructor.
 	 *
-	 * @param key to identify if this object made by an authorised client
-	 * @param principal the principal (typically a <code>UserDetails</code>)
+	 * @param key         to identify if this object made by an authorised client
+	 * @param principal   the principal (typically a <code>UserDetails</code>)
 	 * @param authorities the authorities granted to the principal
-	 *
 	 * @throws IllegalArgumentException if a <code>null</code> was passed
 	 */
 	public RememberMeAuthenticationToken(String key, Object principal,
-			Collection<? extends GrantedAuthority> authorities) {
+										Collection<? extends GrantedAuthority> authorities) {
 		super(authorities);
 
 		if ((key == null) || ("".equals(key)) || (principal == null)
@@ -67,6 +66,22 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
 		setAuthenticated(true);
 	}
 
+	/**
+	 * Private Constructor to help in Jackson deserialization.
+	 *
+	 * @param keyHash     hashCode of above given key.
+	 * @param principal   the principal (typically a <code>UserDetails</code>)
+	 * @param authorities the authorities granted to the principal
+	 * @since 4.2
+	 */
+	private RememberMeAuthenticationToken(Integer keyHash, Object principal, Collection<? extends GrantedAuthority> authorities) {
+		super(authorities);
+
+		this.keyHash = keyHash;
+		this.principal = principal;
+		setAuthenticated(true);
+	}
+
 	// ~ Methods
 	// ========================================================================================================
 

+ 59 - 0
core/src/main/java/org/springframework/security/jackson2/AnonymousAuthenticationTokenMixin.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.annotation.*;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.util.Collection;
+
+/**
+ * This is a Jackson mixin class helps in serialize/deserialize
+ * {@link org.springframework.security.authentication.AnonymousAuthenticationToken} class. To use this class you need to register it
+ * with {@link com.fasterxml.jackson.databind.ObjectMapper} and {@link SimpleGrantedAuthorityMixin} because
+ * AnonymousAuthenticationToken contains SimpleGrantedAuthority.
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * </pre>
+ *
+ * <i>Note: This class will save full class name into a property called @class</i>
+ *
+ * @author Jitendra Singh
+ * @see CoreJackson2Module
+ * @see SecurityJacksonModules
+ * @since 4.2
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, isGetterVisibility = JsonAutoDetect.Visibility.NONE,
+		getterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.ANY)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AnonymousAuthenticationTokenMixin {
+
+	/**
+	 * Constructor used by Jackson to create object of {@link org.springframework.security.authentication.AnonymousAuthenticationToken}.
+	 *
+	 * @param keyHash hashCode of key provided at the time of token creation by using
+	 *                {@link org.springframework.security.authentication.AnonymousAuthenticationToken#AnonymousAuthenticationToken(String, Object, Collection)}
+	 * @param principal the principal (typically a <code>UserDetails</code>)
+	 * @param authorities the authorities granted to the principal
+	 */
+	@JsonCreator
+	public AnonymousAuthenticationTokenMixin(@JsonProperty("keyHash") Integer keyHash, @JsonProperty("principal") Object principal,
+												@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities) {
+	}
+}

+ 63 - 0
core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java

@@ -0,0 +1,63 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.authentication.RememberMeAuthenticationToken;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+
+import java.util.Collections;
+
+/**
+ * Jackson module for spring-security-core. This module register {@link AnonymousAuthenticationTokenMixin},
+ * {@link RememberMeAuthenticationTokenMixin}, {@link SimpleGrantedAuthorityMixin}, {@link UnmodifiableSetMixin},
+ * {@link UserMixin} and {@link UsernamePasswordAuthenticationTokenMixin}. If no default typing enabled by default then
+ * it'll enable it because typing info is needed to properly serialize/deserialize objects. In order to use this module just
+ * add this module into your ObjectMapper configuration.
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * </pre>
+ * <b>Note: use {@link SecurityJacksonModules#getModules()} to get list of all security modules.</b>
+ *
+ * @author Jitendra Singh.
+ * @see SecurityJacksonModules
+ * @since 4.2
+ */
+public class CoreJackson2Module extends SimpleModule {
+
+	public CoreJackson2Module() {
+		super(CoreJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null));
+	}
+
+	@Override
+	public void setupModule(SetupContext context) {
+		SecurityJacksonModules.enableDefaultTyping((ObjectMapper) context.getOwner());
+		context.setMixInAnnotations(AnonymousAuthenticationToken.class, AnonymousAuthenticationTokenMixin.class);
+		context.setMixInAnnotations(RememberMeAuthenticationToken.class, RememberMeAuthenticationTokenMixin.class);
+		context.setMixInAnnotations(SimpleGrantedAuthority.class, SimpleGrantedAuthorityMixin.class);
+		context.setMixInAnnotations(Collections.unmodifiableSet(Collections.EMPTY_SET).getClass(), UnmodifiableSetMixin.class);
+		context.setMixInAnnotations(User.class, UserMixin.class);
+		context.setMixInAnnotations(UsernamePasswordAuthenticationToken.class, UsernamePasswordAuthenticationTokenMixin.class);
+	}
+}

+ 66 - 0
core/src/main/java/org/springframework/security/jackson2/RememberMeAuthenticationTokenMixin.java

@@ -0,0 +1,66 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.annotation.*;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.util.Collection;
+
+/**
+ * This mixin class helps in serialize/deserialize
+ * {@link org.springframework.security.authentication.RememberMeAuthenticationToken} class. To use this class you need to register it
+ * with {@link com.fasterxml.jackson.databind.ObjectMapper} and 2 more mixin classes.
+ *
+ * <ol>
+ *     <li>{@link SimpleGrantedAuthorityMixin}</li>
+ *     <li>{@link UserMixin}</li>
+ *     <li>{@link UnmodifiableSetMixin}</li>
+ * </ol>
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * </pre>
+ *
+ * <i>Note: This class will save TypeInfo (full class name) into a property called @class</i>
+ *
+ * @author Jitendra Singh
+ * @see CoreJackson2Module
+ * @see SecurityJacksonModules
+ * @since 4.2
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
+		isGetterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.ANY)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class RememberMeAuthenticationTokenMixin {
+
+	/**
+	 * Constructor used by Jackson to create
+	 * {@link org.springframework.security.authentication.RememberMeAuthenticationToken} object.
+	 *
+	 * @param keyHash hashCode of above given key.
+	 * @param principal the principal (typically a <code>UserDetails</code>)
+	 * @param authorities the authorities granted to the principal
+	 */
+	@JsonCreator
+	public RememberMeAuthenticationTokenMixin(@JsonProperty("keyHash") Integer keyHash,
+												@JsonProperty("principal") Object principal,
+												@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities) {
+	}
+}

+ 106 - 0
core/src/main/java/org/springframework/security/jackson2/SecurityJacksonModules.java

@@ -0,0 +1,106 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ObjectUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This utility class will find all the SecurityModules in classpath.
+ *
+ * <p>
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModules(SecurityJacksonModules.getModules());
+ * </pre>
+ * Above code is equivalent to
+ * <p>
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+ *     mapper.registerModule(new CoreJackson2Module());
+ *     mapper.registerModule(new CasJackson2Module());
+ *     mapper.registerModule(new WebJackson2Module());
+ * </pre>
+ *
+ * @author Jitendra Singh.
+ * @since 4.2
+ */
+public final class SecurityJacksonModules {
+
+	private static final Log logger = LogFactory.getLog(SecurityJacksonModules.class);
+	private static final List<String> securityJackson2ModuleClasses = Arrays.asList(
+			"org.springframework.security.jackson2.CoreJackson2Module",
+			"org.springframework.security.cas.jackson2.CasJackson2Module",
+			"org.springframework.security.web.jackson2.WebJackson2Module"
+	);
+
+	private SecurityJacksonModules() {
+	}
+
+	public static void enableDefaultTyping(ObjectMapper mapper) {
+		if(!ObjectUtils.isEmpty(mapper)) {
+			TypeResolverBuilder<?> typeBuilder = mapper.getDeserializationConfig().getDefaultTyper(null);
+			if (ObjectUtils.isEmpty(typeBuilder)) {
+				mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+			}
+		}
+	}
+
+	private static Module loadAndGetInstance(String className) {
+		Module instance = null;
+		try {
+			logger.debug("Loading module " + className);
+			Class<? extends Module> securityModule = (Class<? extends Module>) ClassUtils.forName(className, ClassUtils.getDefaultClassLoader());
+			if (!ObjectUtils.isEmpty(securityModule)) {
+				logger.debug("Loaded module " + className + ", now registering");
+				instance = securityModule.newInstance();
+			}
+		} catch (ClassNotFoundException e) {
+			logger.warn("Module class not found : " + e.getMessage());
+		} catch (InstantiationException e) {
+			logger.error(e.getMessage());
+		} catch (IllegalAccessException e) {
+			logger.error(e.getMessage());
+		}
+		return instance;
+	}
+
+	/**
+	 * @return List of available security modules in classpath.
+	 */
+	public static List<Module> getModules() {
+		List<Module> modules = new ArrayList<Module>();
+		for (String className : securityJackson2ModuleClasses) {
+			Module module = loadAndGetInstance(className);
+			if (!ObjectUtils.isEmpty(module)) {
+				modules.add(module);
+			}
+		}
+		return modules;
+	}
+}

+ 56 - 0
core/src/main/java/org/springframework/security/jackson2/SimpleGrantedAuthorityMixin.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.annotation.*;
+
+/**
+ * Jackson Mixin class helps in serialize/deserialize
+ * {@link org.springframework.security.core.authority.SimpleGrantedAuthority}.
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * </pre>
+ * @author Jitendra Singh
+ * @see CoreJackson2Module
+ * @see SecurityJacksonModules
+ * @since 4.2
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
+		isGetterVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public abstract class SimpleGrantedAuthorityMixin {
+
+	/**
+	 * Mixin Constructor.
+	 * @param role
+	 */
+	@JsonCreator
+	public SimpleGrantedAuthorityMixin(@JsonProperty("role") String role) {
+	}
+
+	/**
+	 * This method will ensure that getAuthority() doesn't serialized to <b>authority</b> key, it will be serialized
+	 * as <b>role</b> key. Because above mixin constructor will look for role key to properly deserialize.
+	 *
+	 * @return
+	 */
+	@JsonProperty("role")
+	public abstract String getAuthority();
+}

+ 47 - 0
core/src/main/java/org/springframework/security/jackson2/UnmodifiableSetMixin.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import java.util.Set;
+
+/**
+ * This mixin class used to deserialize java.util.Collections$UnmodifiableSet and used with various AuthenticationToken
+ * implementation's mixin classes.
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * </pre>
+ *
+ * @author Jitendra Singh
+ * @see CoreJackson2Module
+ * @see SecurityJacksonModules
+ * @since 4.2
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+public class UnmodifiableSetMixin {
+
+	/**
+	 * Mixin Constructor
+	 * @param s
+	 */
+	@JsonCreator
+	UnmodifiableSetMixin(Set s) {}
+}

+ 72 - 0
core/src/main/java/org/springframework/security/jackson2/UserDeserializer.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+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.node.MissingNode;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * Custom Deserializer for {@link User} class. This is already registered with {@link UserMixin}.
+ * You can also use it directly with your mixin class.
+ *
+ * @author Jitendra Singh
+ * @see UserMixin
+ * @since 4.2
+ */
+public class UserDeserializer extends JsonDeserializer<User> {
+
+	/**
+	 * This method will create {@link User} object. It will ensure successful object creation even if password key is null in
+	 * serialized json, because credentials may be removed from the {@link User} by invoking {@link User#eraseCredentials()}.
+	 * In that case there won't be any password key in serialized json.
+	 *
+	 * @param jp
+	 * @param ctxt
+	 * @return
+	 * @throws IOException
+	 * @throws JsonProcessingException
+	 */
+	@Override
+	public User deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
+		ObjectMapper mapper = (ObjectMapper) jp.getCodec();
+		JsonNode jsonNode = mapper.readTree(jp);
+		Set<GrantedAuthority> authorities = mapper.convertValue(jsonNode.get("authorities"), new TypeReference<Set<SimpleGrantedAuthority>>() {
+		});
+		return new User(
+				readJsonNode(jsonNode, "username").asText(), readJsonNode(jsonNode, "password").asText(""),
+				readJsonNode(jsonNode, "enabled").asBoolean(), readJsonNode(jsonNode, "accountNonExpired").asBoolean(),
+				readJsonNode(jsonNode, "credentialsNonExpired").asBoolean(),
+				readJsonNode(jsonNode, "accountNonLocked").asBoolean(), authorities
+		);
+	}
+
+	private JsonNode readJsonNode(JsonNode jsonNode, String field) {
+		return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
+	}
+}

+ 49 - 0
core/src/main/java/org/springframework/security/jackson2/UserMixin.java

@@ -0,0 +1,49 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.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;
+
+/**
+ * This mixin class helps in serialize/deserialize {@link org.springframework.security.core.userdetails.User}.
+ * This class also register a custom deserializer {@link UserDeserializer} to deserialize User object successfully.
+ * In order to use this mixin you need to register two more mixin classes in your ObjectMapper configuration.
+ * <ol>
+ *     <li>{@link SimpleGrantedAuthorityMixin}</li>
+ *     <li>{@link UnmodifiableSetMixin}</li>
+ * </ol>
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * </pre>
+ *
+ * @author Jitendra Singh
+ * @see UserDeserializer
+ * @see CoreJackson2Module
+ * @see SecurityJacksonModules
+ * @since 4.2
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonDeserialize(using = UserDeserializer.class)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
+		isGetterVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public abstract class UserMixin {
+}

+ 85 - 0
core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java

@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+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.node.MissingNode;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Custom deserializer for {@link UsernamePasswordAuthenticationToken}. At the time of deserialization
+ * it will invoke suitable constructor depending on the value of <b>authenticated</b> property.
+ * It will ensure that the token's state must not change.
+ * <p>
+ * This deserializer is already registered with {@link UsernamePasswordAuthenticationTokenMixin} but
+ * you can also registered it with your own mixin class.
+ *
+ * @author Jitendra Singh
+ * @see UsernamePasswordAuthenticationTokenMixin
+ * @since 4.2
+ */
+public class UsernamePasswordAuthenticationTokenDeserializer extends JsonDeserializer<UsernamePasswordAuthenticationToken> {
+
+	/**
+	 * This method construct {@link UsernamePasswordAuthenticationToken} object from serialized json.
+	 * @param jp
+	 * @param ctxt
+	 * @return
+	 * @throws IOException
+	 * @throws JsonProcessingException
+	 */
+	@Override
+	public UsernamePasswordAuthenticationToken deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
+		UsernamePasswordAuthenticationToken token = null;
+		ObjectMapper mapper = (ObjectMapper) jp.getCodec();
+		JsonNode jsonNode = mapper.readTree(jp);
+		Boolean authenticated = readJsonNode(jsonNode, "authenticated").asBoolean();
+		JsonNode principalNode = readJsonNode(jsonNode, "principal");
+		Object principal = null;
+		if(principalNode.isObject()) {
+			principal = mapper.readValue(principalNode.toString(), new TypeReference<User>() {});
+		} else {
+			principal = principalNode.asText();
+		}
+		Object credentials = readJsonNode(jsonNode, "credentials").asText();
+		List<GrantedAuthority> authorities = mapper.readValue(
+				readJsonNode(jsonNode, "authorities").toString(), new TypeReference<List<GrantedAuthority>>() {
+		});
+		if (authenticated) {
+			token = new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
+		} else {
+			token = new UsernamePasswordAuthenticationToken(principal, credentials);
+		}
+		token.setDetails(readJsonNode(jsonNode, "details"));
+		return token;
+	}
+
+	private JsonNode readJsonNode(JsonNode jsonNode, String field) {
+		return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
+	}
+}

+ 48 - 0
core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenMixin.java

@@ -0,0 +1,48 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.annotation.*;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+/**
+ * This mixin class is used to serialize / deserialize
+ * {@link org.springframework.security.authentication.UsernamePasswordAuthenticationToken}. This class register
+ * a custom deserializer {@link UsernamePasswordAuthenticationTokenDeserializer}.
+ *
+ * In order to use this mixin you'll need to add 3 more mixin classes.
+ * <ol>
+ *     <li>{@link UnmodifiableSetMixin}</li>
+ *     <li>{@link SimpleGrantedAuthorityMixin}</li>
+ *     <li>{@link UserMixin}</li>
+ * </ol>
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * </pre>
+ * @author Jitendra Singh
+ * @see CoreJackson2Module
+ * @see SecurityJacksonModules
+ * @since 4.2
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
+		isGetterVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonDeserialize(using = UsernamePasswordAuthenticationTokenDeserializer.class)
+public abstract class UsernamePasswordAuthenticationTokenMixin {
+}

+ 26 - 0
core/src/main/java/org/springframework/security/jackson2/package-info.java

@@ -0,0 +1,26 @@
+/*
+ * Copyright 2002-2016 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
+ *
+ *      http://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.
+ */
+/**
+ * Mix-in classes to add Jackson serialization support.
+ *
+ * @author Jitendra Singh
+ * @since 4.2
+ */
+package org.springframework.security.jackson2;
+
+/**
+ * Package contains Jackson mixin classes.
+ */

+ 52 - 0
core/src/test/java/org/springframework/security/jackson2/AbstractMixinTests.java

@@ -0,0 +1,52 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.util.ObjectUtils;
+
+import java.util.Collections;
+
+/**
+ * @author Jitenra Singh
+ * @since 4.2
+ */
+@RunWith(MockitoJUnitRunner.class)
+public abstract class AbstractMixinTests {
+
+	ObjectMapper mapper;
+
+	protected ObjectMapper buildObjectMapper() {
+		if (ObjectUtils.isEmpty(mapper)) {
+			mapper = new ObjectMapper();
+			mapper.registerModules(SecurityJacksonModules.getModules());
+		}
+		return mapper;
+	}
+
+	User createDefaultUser() {
+		return createUser("dummy", "password", "ROLE_USER");
+	}
+
+	User createUser(String username, String password, String authority) {
+		return new User(username, password, Collections.singletonList(new SimpleGrantedAuthority(authority)));
+	}
+}

+ 95 - 0
core/src/test/java/org/springframework/security/jackson2/AnonymousAuthenticationTokenMixinTests.java

@@ -0,0 +1,95 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import org.json.JSONException;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jitendra Singh
+ * @since 4.2
+ */
+public class AnonymousAuthenticationTokenMixinTests extends AbstractMixinTests {
+
+	String hashKey = "key";
+	String anonymousAuthTokenJson = "{\"@class\": \"org.springframework.security.authentication.AnonymousAuthenticationToken\", \"details\": null," +
+			"\"principal\": {\"@class\": \"org.springframework.security.core.userdetails.User\", \"username\": \"dummy\", \"password\": %s," +
+			" \"accountNonExpired\": true, \"enabled\": true, " +
+			"\"accountNonLocked\": true, \"credentialsNonExpired\": true, \"authorities\": [\"java.util.Collections$UnmodifiableSet\"," +
+			"[{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"role\": \"ROLE_USER\"}]]}, \"authenticated\": true, \"keyHash\": " + hashKey.hashCode() + "," +
+			"\"authorities\": [\"java.util.ArrayList\", [{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"role\": \"ROLE_USER\"}]]}";
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testWithNullAuthorities() throws JsonProcessingException, JSONException {
+		new AnonymousAuthenticationToken("key", "principal", null);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testWithEmptyAuthorities() throws JsonProcessingException, JSONException {
+		new AnonymousAuthenticationToken("key", "principal", Collections.<GrantedAuthority>emptyList());
+	}
+
+	@Test
+	public void serializeAnonymousAuthenticationTokenTest() throws JsonProcessingException, JSONException {
+		User user = createDefaultUser();
+		AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(
+				hashKey, user, user.getAuthorities()
+		);
+		String actualJson = buildObjectMapper().writeValueAsString(token);
+		JSONAssert.assertEquals(String.format(anonymousAuthTokenJson, "\"password\""), actualJson, true);
+	}
+
+	@Test
+	public void deserializeAnonymousAuthenticationTokenTest() throws IOException {
+		AnonymousAuthenticationToken token = buildObjectMapper()
+				.readValue(String.format(anonymousAuthTokenJson,"\"password\""), AnonymousAuthenticationToken.class);
+		assertThat(token).isNotNull();
+		assertThat(token.getKeyHash()).isEqualTo(hashKey.hashCode());
+		assertThat(token.getAuthorities()).isNotNull().hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER"));
+	}
+
+	@Test(expected = JsonMappingException.class)
+	public void deserializeAnonymousAuthenticationTokenWithoutAuthoritiesTest() throws IOException {
+		String jsonString = "{\"@class\": \"org.springframework.security.authentication.AnonymousAuthenticationToken\", \"details\": null," +
+				"\"principal\": \"user\", \"authenticated\": true, \"keyHash\": " + hashKey.hashCode() + "," +
+				"\"authorities\": [\"java.util.ArrayList\", []]}";
+		buildObjectMapper().readValue(jsonString, AnonymousAuthenticationToken.class);
+	}
+
+	@Test
+	public void serializeAnonymousAuthenticationTokenMixinAfterEraseCredentialTest() throws JsonProcessingException, JSONException {
+		User user = createDefaultUser();
+		AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(
+				hashKey, user, user.getAuthorities()
+		);
+		token.eraseCredentials();
+		String actualJson = buildObjectMapper().writeValueAsString(token);
+		JSONAssert.assertEquals(String.format(anonymousAuthTokenJson, "null"), actualJson, true);
+	}
+}

+ 105 - 0
core/src/test/java/org/springframework/security/jackson2/RememberMeAuthenticationTokenMixinTests.java

@@ -0,0 +1,105 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.json.JSONException;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.security.authentication.RememberMeAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jitendra Singh
+ * @since 4.2
+ */
+public class RememberMeAuthenticationTokenMixinTests extends AbstractMixinTests {
+
+	String rememberMeKey = "rememberMe";
+	String rememberMeAuthTokenJson = "{\"@class\": \"org.springframework.security.authentication.RememberMeAuthenticationToken\"," +
+			"\"keyHash\": " + rememberMeKey.hashCode() + ", \"authenticated\": true, \"details\": null," +
+			"\"principal\": {\"@class\": \"org.springframework.security.core.userdetails.User\", \"username\": \"dummy\", \"password\": %s," +
+			" \"enabled\": true, \"accountNonExpired\": true, \"accountNonLocked\": true, \"credentialsNonExpired\": true, " +
+			"\"authorities\": [\"java.util.Collections$UnmodifiableSet\", [{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"role\": \"ROLE_USER\"}]]}," +
+			"\"authorities\": [\"java.util.ArrayList\", [{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"role\": \"ROLE_USER\"}]]}";
+
+	String rememberMeAuthTokenWithoutUserJson = "{\"@class\": \"org.springframework.security.authentication.RememberMeAuthenticationToken\"," +
+			"\"keyHash\": " + rememberMeKey.hashCode() + ", \"authenticated\": true, \"details\": null," +
+			"\"principal\": \"dummy\", \"authorities\": [\"java.util.ArrayList\", [{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"role\": \"ROLE_USER\"}]]}";
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testWithNullPrincipal() throws JsonProcessingException, JSONException {
+		new RememberMeAuthenticationToken("key", null, Collections.<GrantedAuthority>emptyList());
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testWithNullKey() throws JsonProcessingException, JSONException {
+		new RememberMeAuthenticationToken(null, "principal", Collections.<GrantedAuthority>emptyList());
+	}
+
+	@Test
+	public void serializeRememberMeAuthenticationToken() throws JsonProcessingException, JSONException {
+		RememberMeAuthenticationToken token = new RememberMeAuthenticationToken(rememberMeKey, "dummy", Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")));
+		String actualJson = buildObjectMapper().writeValueAsString(token);
+		JSONAssert.assertEquals(rememberMeAuthTokenWithoutUserJson, actualJson, true);
+	}
+
+	@Test
+	public void serializeRememberMeAuthenticationWithUserToken() throws JsonProcessingException, JSONException {
+		User user = createDefaultUser();
+		RememberMeAuthenticationToken token = new RememberMeAuthenticationToken(rememberMeKey, user, user.getAuthorities());
+		String actualJson = buildObjectMapper().writeValueAsString(token);
+		JSONAssert.assertEquals(String.format(rememberMeAuthTokenJson, "\"password\""), actualJson, true);
+	}
+
+	@Test
+	public void serializeRememberMeAuthenticationWithUserTokenAfterEraseCredential() throws JsonProcessingException, JSONException {
+		User user = createDefaultUser();
+		RememberMeAuthenticationToken token = new RememberMeAuthenticationToken(rememberMeKey, user, user.getAuthorities());
+		token.eraseCredentials();
+		String actualJson = buildObjectMapper().writeValueAsString(token);
+		JSONAssert.assertEquals(String.format(rememberMeAuthTokenJson, "null"), actualJson, true);
+	}
+
+	@Test
+	public void deserializeRememberMeAuthenticationToken() throws IOException {
+		RememberMeAuthenticationToken token = buildObjectMapper().readValue(rememberMeAuthTokenWithoutUserJson, RememberMeAuthenticationToken.class);
+		assertThat(token).isNotNull();
+		assertThat(token.getPrincipal()).isNotNull().isEqualTo("dummy").isEqualTo(token.getName());
+		assertThat(token.getAuthorities()).hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER"));
+	}
+
+	@Test
+	public void deserializeRememberMeAuthenticationTokenWithUserTest() throws IOException {
+		RememberMeAuthenticationToken token = buildObjectMapper()
+				.readValue(String.format(rememberMeAuthTokenJson, "\"password\""), RememberMeAuthenticationToken.class);
+		assertThat(token).isNotNull();
+		assertThat(token.getPrincipal()).isNotNull().isInstanceOf(User.class);
+		assertThat(((User)token.getPrincipal()).getUsername()).isEqualTo("dummy");
+		assertThat(((User)token.getPrincipal()).getPassword()).isEqualTo("password");
+		assertThat(((User) token.getPrincipal()).getAuthorities()).hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER"));
+		assertThat(token.getAuthorities()).hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER"));
+		assertThat(((User) token.getPrincipal()).isEnabled()).isEqualTo(true);
+	}
+}

+ 64 - 0
core/src/test/java/org/springframework/security/jackson2/SecurityContextMixinTests.java

@@ -0,0 +1,64 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.json.JSONException;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextImpl;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jitendra Singh
+ * @since 4.2
+ */
+public class SecurityContextMixinTests extends AbstractMixinTests {
+
+	String securityContextJson = "{\"@class\": \"org.springframework.security.core.context.SecurityContextImpl\", \"authentication\": " +
+				"{\"@class\": \"org.springframework.security.authentication.UsernamePasswordAuthenticationToken\"," +
+					"\"principal\": \"dummy\", \"credentials\": \"password\", \"authenticated\": true, \"details\": null," +
+					"\"authorities\": [\"java.util.ArrayList\", [{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"role\": \"ROLE_USER\"}]]" +
+				"}" +
+			"}";
+
+	@Test
+	public void securityContextSerializeTest() throws JsonProcessingException, JSONException {
+		SecurityContext context = new SecurityContextImpl();
+		context.setAuthentication(new UsernamePasswordAuthenticationToken("dummy", "password", Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"))));
+		String actualJson = buildObjectMapper().writeValueAsString(context);
+		JSONAssert.assertEquals(securityContextJson, actualJson, true);
+	}
+
+	@Test
+	public void securityContextDeserializeTest() throws IOException {
+		SecurityContext context = buildObjectMapper().readValue(securityContextJson, SecurityContextImpl.class);
+		assertThat(context).isNotNull();
+		assertThat(context.getAuthentication()).isNotNull().isInstanceOf(UsernamePasswordAuthenticationToken.class);
+		assertThat(context.getAuthentication().getPrincipal()).isEqualTo("dummy");
+		assertThat(context.getAuthentication().getCredentials()).isEqualTo("password");
+		assertThat(context.getAuthentication().isAuthenticated()).isEqualTo(true);
+		assertThat(context.getAuthentication().getAuthorities()).hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER"));
+	}
+}

+ 61 - 0
core/src/test/java/org/springframework/security/jackson2/SimpleGrantedAuthorityMixinTests.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.json.JSONException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.*;
+
+/**
+ * @author Jitendra Singh
+ * @since 4.2
+ */
+public class SimpleGrantedAuthorityMixinTests extends AbstractMixinTests {
+
+	String simpleGrantedAuthorityJson = "{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"role\": \"ROLE_USER\"}";
+
+	@Test
+	public void serializeSimpleGrantedAuthorityTest() throws JsonProcessingException, JSONException {
+		SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER");
+		String serializeJson = buildObjectMapper().writeValueAsString(authority);
+		JSONAssert.assertEquals(simpleGrantedAuthorityJson, serializeJson, true);
+	}
+
+	@Test
+	public void deserializeGrantedAuthorityTest() throws IOException {
+		SimpleGrantedAuthority authority = buildObjectMapper().readValue(simpleGrantedAuthorityJson, SimpleGrantedAuthority.class);
+		assertThat(authority).isNotNull();
+		assertThat(authority.getAuthority()).isNotNull().isEqualTo("ROLE_USER");
+	}
+
+	@Test(expected = JsonMappingException.class)
+	public void deserializeGrantedAuthorityWithoutRoleTest() throws IOException {
+		String json = "{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\"}";
+		buildObjectMapper().readValue(json, SimpleGrantedAuthority.class);
+	}
+}

+ 105 - 0
core/src/test/java/org/springframework/security/jackson2/UserDeserializerTests.java

@@ -0,0 +1,105 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.json.JSONException;
+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.core.userdetails.User;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jitendra Singh
+ * @since 4.2
+ */
+public class UserDeserializerTests extends AbstractMixinTests {
+
+	String userWithAuthoritiesJson = "{\"@class\": \"org.springframework.security.core.userdetails.User\", \"username\": \"admin\"," +
+			" \"password\": %s, \"accountNonExpired\": true, \"accountNonLocked\": true, \"credentialsNonExpired\": true, " +
+			"\"enabled\": true, \"authorities\": [\"java.util.Collections$UnmodifiableSet\", [{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"role\": \"ROLE_USER\"}]]}";
+
+	String userWithoutAuthoritiesJson = "{\"@class\": \"org.springframework.security.core.userdetails.User\", \"username\": \"admin\"," +
+			" \"password\": \"1234\", \"accountNonExpired\": true, \"accountNonLocked\": true, \"credentialsNonExpired\": true," +
+			" \"enabled\": true, \"authorities\": [\"java.util.Collections$UnmodifiableSet\", []]}";
+
+	@Test
+	public void serializeUserTest() throws JsonProcessingException, JSONException {
+		ObjectMapper mapper = buildObjectMapper();
+		User user = new User("admin", "1234", Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
+		String userJson = mapper.writeValueAsString(user);
+		JSONAssert.assertEquals(String.format(userWithAuthoritiesJson, "\"1234\""), userJson, true);
+	}
+
+	@Test
+	public void serializeUserWithoutAuthority() throws JsonProcessingException, JSONException {
+		ObjectMapper mapper = buildObjectMapper();
+		User user = new User("admin", "1234", Collections.<GrantedAuthority>emptyList());
+		String userJson = mapper.writeValueAsString(user);
+		JSONAssert.assertEquals(userWithoutAuthoritiesJson, userJson, true);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void deserializeUserWithNullPasswordEmptyAuthorityTest() throws IOException {
+		String userJsonWithoutPasswordString = "{\"@class\": \"org.springframework.security.core.userdetails.User\", " +
+				"\"username\": \"user\", \"accountNonExpired\": true, " +
+				"\"accountNonLocked\": true, \"credentialsNonExpired\": true, \"enabled\": true, " +
+				"\"authorities\": []}";
+		ObjectMapper mapper = buildObjectMapper();
+		mapper.readValue(userJsonWithoutPasswordString, User.class);
+	}
+
+	@Test
+	public void deserializeUserWithNullPasswordNoAuthorityTest() throws IOException {
+		String userJsonWithoutPasswordString = "{\"@class\": \"org.springframework.security.core.userdetails.User\", " +
+				"\"username\": \"admin\", \"accountNonExpired\": true, " +
+				"\"accountNonLocked\": true, \"credentialsNonExpired\": true, \"enabled\": true, " +
+				"\"authorities\": [\"java.util.HashSet\", []]}";
+		ObjectMapper mapper = buildObjectMapper();
+		User user = mapper.readValue(userJsonWithoutPasswordString, User.class);
+		assertThat(user).isNotNull();
+		assertThat(user.getUsername()).isEqualTo("admin");
+		assertThat(user.getPassword()).isEqualTo("");
+		assertThat(user.getAuthorities()).hasSize(0);
+		assertThat(user.isEnabled()).isEqualTo(true);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void deserializeUserWithNoClassIdInAuthoritiesTest() throws IOException {
+		String userJson = "{\"@class\": \"org.springframework.security.core.userdetails.User\", " +
+				"\"username\": \"user\", \"password\": \"pass\", \"accountNonExpired\": false, " +
+				"\"accountNonLocked\": false, \"credentialsNonExpired\": false, \"enabled\": false, " +
+				"\"authorities\": [{\"role\": \"ROLE_USER\"}]}";
+		buildObjectMapper().readValue(userJson, User.class);
+	}
+
+	@Test
+	public void deserializeUserWithClassIdInAuthoritiesTest() throws IOException {
+		User user = buildObjectMapper().readValue(String.format(userWithAuthoritiesJson, "\"1234\""), User.class);
+		assertThat(user).isNotNull();
+		assertThat(user.getUsername()).isEqualTo("admin");
+		assertThat(user.getPassword()).isEqualTo("1234");
+		assertThat(user.getAuthorities()).hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER"));
+	}
+}

+ 117 - 0
core/src/test/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenMixinTests.java

@@ -0,0 +1,117 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.jackson2;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.json.JSONException;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jitendra Singh
+ * @since 4.2
+ */
+public class UsernamePasswordAuthenticationTokenMixinTests extends AbstractMixinTests {
+
+	String unauthenticatedTokenWithoutUserPrincipal = "{\"@class\": \"org.springframework.security.authentication.UsernamePasswordAuthenticationToken\"," +
+			" \"principal\": \"user1\", \"credentials\": \"password\", \"authenticated\": false, \"details\": null, " +
+			"\"authorities\": [\"java.util.ArrayList\", []]}";
+
+	String authenticatedTokenWithoutUserPrincipal = "{\"@class\": \"org.springframework.security.authentication.UsernamePasswordAuthenticationToken\"," +
+			" \"principal\": \"user1\", \"credentials\": \"password\", \"authenticated\": true, \"details\": null, " +
+			"\"authorities\": [\"java.util.ArrayList\", [{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"role\": \"ROLE_USER\"}]]}";
+
+	String authenticatedTokenWithUserPrincipal = "{\"@class\": \"org.springframework.security.authentication.UsernamePasswordAuthenticationToken\"," +
+			"\"principal\": {\"@class\": \"org.springframework.security.core.userdetails.User\", \"username\": \"user\", \"password\": %s, \"accountNonExpired\": true, \"enabled\": true, " +
+			"\"accountNonLocked\": true, \"credentialsNonExpired\": true, \"authorities\": [\"java.util.Collections$UnmodifiableSet\"," +
+			"[{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"role\": \"ROLE_USER\"}]]}, \"credentials\": %s," +
+			"\"details\": null, \"authenticated\": true," +
+			"\"authorities\": [\"java.util.ArrayList\", [{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"role\": \"ROLE_USER\"}]]}";
+
+	@Test
+	public void serializeUnauthenticatedUsernamePasswordAuthenticationTokenMixinTest() throws JsonProcessingException, JSONException {
+		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user1", "password");
+		String serializedJson = buildObjectMapper().writeValueAsString(token);
+		JSONAssert.assertEquals(unauthenticatedTokenWithoutUserPrincipal, serializedJson, true);
+	}
+
+	@Test
+	public void serializeAuthenticatedUsernamePasswordAuthenticationTokenMixinTest() throws JsonProcessingException, JSONException {
+		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user1", "password", Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
+		String serializedJson = buildObjectMapper().writeValueAsString(token);
+		JSONAssert.assertEquals(authenticatedTokenWithoutUserPrincipal, serializedJson, true);
+	}
+
+	@Test
+	public void deserializeUnauthenticatedUsernamePasswordAuthenticationTokenMixinTest() throws IOException, JSONException {
+		UsernamePasswordAuthenticationToken token = buildObjectMapper()
+				.readValue(unauthenticatedTokenWithoutUserPrincipal, UsernamePasswordAuthenticationToken.class);
+		assertThat(token).isNotNull();
+		assertThat(token.isAuthenticated()).isEqualTo(false);
+		assertThat(token.getAuthorities()).isNotNull().hasSize(0);
+	}
+
+	@Test
+	public void deserializeAuthenticatedUsernamePasswordAuthenticationTokenMixinTest() throws IOException {
+		UsernamePasswordAuthenticationToken token = buildObjectMapper()
+				.readValue(authenticatedTokenWithoutUserPrincipal, UsernamePasswordAuthenticationToken.class);
+		assertThat(token).isNotNull();
+		assertThat(token.isAuthenticated()).isEqualTo(true);
+		assertThat(token.getAuthorities()).isNotNull().hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER"));
+	}
+
+	@Test
+	public void serializeAuthenticatedUsernamePasswordAuthenticationTokenMixinWithUserTest() throws JsonProcessingException, JSONException {
+		GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER");
+		User user = new User("user", "password", Collections.singleton(authority));
+		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, "password", Collections.singleton(authority));
+		String actualJson = buildObjectMapper().writeValueAsString(token);
+		JSONAssert.assertEquals(String.format(authenticatedTokenWithUserPrincipal, "password", "password"), actualJson, true);
+	}
+
+	@Test
+	public void deserializeAuthenticatedUsernamePasswordAuthenticationTokenWithUserTest() throws IOException {
+		ObjectMapper mapper = buildObjectMapper();
+		UsernamePasswordAuthenticationToken token = mapper
+				.readValue(String.format(authenticatedTokenWithUserPrincipal, "\"password\"", "\"password\""), UsernamePasswordAuthenticationToken.class);
+		assertThat(token).isNotNull();
+		assertThat(token.getPrincipal()).isNotNull().isInstanceOf(User.class);
+		assertThat(((User)token.getPrincipal()).getAuthorities()).isNotNull().hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER"));
+		assertThat(token.isAuthenticated()).isEqualTo(true);
+		assertThat(token.getAuthorities()).hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER"));
+	}
+
+	@Test
+	public void serializeAuthenticatedUsernamePasswordAuthenticationTokenMixinAfterEraseCredentialInvoked() throws JsonProcessingException, JSONException {
+		GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER");
+		User user = new User("user", "password", Collections.singleton(authority));
+		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, "password", Collections.singleton(authority));
+		token.eraseCredentials();
+		String actualJson = buildObjectMapper().writeValueAsString(token);
+		JSONAssert.assertEquals(String.format(authenticatedTokenWithUserPrincipal, "null", "null"), actualJson, true);
+	}
+}

+ 1 - 0
gradle/javaprojects.gradle

@@ -35,6 +35,7 @@ ext.springDataRedisVersion = '1.7.2.RELEASE'
 ext.springSessionVersion = '1.2.1.RELEASE'
 ext.springBootVersion = '1.4.0.RELEASE'
 ext.thymeleafVersion = '2.1.5.RELEASE'
+ext.jsonassertVersion = '1.3.0'
 
 ext.spockDependencies = [
 	dependencies.create("org.spockframework:spock-spring:$spockVersion") {

+ 11 - 0
web/src/main/java/org/springframework/security/web/authentication/WebAuthenticationDetails.java

@@ -54,6 +54,17 @@ public class WebAuthenticationDetails implements Serializable {
 		this.sessionId = (session != null) ? session.getId() : null;
 	}
 
+	/**
+	 * Constructor to add Jackson2 serialize/deserialize support
+	 *
+	 * @param remoteAddress remote address of current request
+	 * @param sessionId session id
+	 */
+	private WebAuthenticationDetails(final String remoteAddress, final String sessionId) {
+		this.remoteAddress = remoteAddress;
+		this.sessionId = sessionId;
+	}
+
 	// ~ Methods
 	// ========================================================================================================
 

+ 61 - 0
web/src/main/java/org/springframework/security/web/jackson2/CookieDeserializer.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.web.jackson2;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+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.node.MissingNode;
+import com.fasterxml.jackson.databind.node.NullNode;
+
+import javax.servlet.http.Cookie;
+import java.io.IOException;
+
+/**
+ * Jackson deserializer for {@link Cookie}. This is needed because in most cases we don't
+ * set {@link Cookie#domain} property. So when jackson deserialize that json {@link Cookie#setDomain(String)}
+ * throws {@link NullPointerException}. This is registered with {@link CookieMixin} but you can also use it with
+ * your own mixin.
+ *
+ * @author Jitendra Singh
+ * @see CookieMixin
+ * @since 4.2
+ */
+public class CookieDeserializer extends JsonDeserializer<Cookie> {
+
+	@Override
+	public Cookie deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
+		ObjectMapper mapper = (ObjectMapper) jp.getCodec();
+		JsonNode jsonNode = mapper.readTree(jp);
+		Cookie cookie = new Cookie(readJsonNode(jsonNode, "name").asText(), readJsonNode(jsonNode, "value").asText());
+		cookie.setComment(readJsonNode(jsonNode, "comment").asText());
+		cookie.setDomain(readJsonNode(jsonNode, "domain").asText());
+		cookie.setMaxAge(readJsonNode(jsonNode, "maxAge").asInt(-1));
+		cookie.setSecure(readJsonNode(jsonNode, "secure").asBoolean());
+		cookie.setVersion(readJsonNode(jsonNode, "version").asInt());
+		cookie.setPath(readJsonNode(jsonNode, "path").asText());
+		cookie.setHttpOnly(readJsonNode(jsonNode, "httpOnly").asBoolean());
+		return cookie;
+	}
+
+	private JsonNode readJsonNode(JsonNode jsonNode, String field) {
+		return jsonNode.has(field) && !(jsonNode.get(field) instanceof NullNode) ? jsonNode.get(field) : MissingNode.getInstance();
+	}
+}

+ 40 - 0
web/src/main/java/org/springframework/security/web/jackson2/CookieMixin.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.web.jackson2;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+/**
+ * Mixin class to serialize/deserialize {@link javax.servlet.http.Cookie}
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new WebJackson2Module());
+ * </pre>
+ *
+ * @author Jitendra Singh
+ * @see WebJackson2Module
+ * @see org.springframework.security.jackson2.SecurityJacksonModules
+ * @since 4.2
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonDeserialize(using = CookieDeserializer.class)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, isGetterVisibility = JsonAutoDetect.Visibility.NONE)
+public abstract class CookieMixin {
+}

+ 54 - 0
web/src/main/java/org/springframework/security/web/jackson2/DefaultCsrfTokenMixin.java

@@ -0,0 +1,54 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.web.jackson2;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+/**
+ * Jackson mixin class to serialize/deserialize {@link org.springframework.security.web.csrf.DefaultCsrfToken}
+ * serialization support.
+ *
+ * <pre>
+ * 		ObjectMapper mapper = new ObjectMapper();
+ *		mapper.registerModule(new WebJackson2Module());
+ * </pre>
+ *
+ * @author Jitendra Singh
+ * @see WebJackson2Module
+ * @see org.springframework.security.jackson2.SecurityJacksonModules
+ * @since 4.2
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class DefaultCsrfTokenMixin {
+
+	/**
+	 * JsonCreator constructor needed by Jackson to create {@link org.springframework.security.web.csrf.DefaultCsrfToken}
+	 * object.
+	 *
+	 * @param headerName
+	 * @param parameterName
+	 * @param token
+	 */
+	@JsonCreator
+	public DefaultCsrfTokenMixin(@JsonProperty("headerName") String headerName,
+								@JsonProperty("parameterName") String parameterName, @JsonProperty("token") String token) {
+	}
+}

+ 44 - 0
web/src/main/java/org/springframework/security/web/jackson2/DefaultSavedRequestMixin.java

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.web.jackson2;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.springframework.security.web.savedrequest.DefaultSavedRequest;
+
+/**
+ * Jackson mixin class to serialize/deserialize {@link DefaultSavedRequest}. This mixin use
+ * {@link org.springframework.security.web.savedrequest.DefaultSavedRequest.Builder} to
+ * deserialized json.In order to use this mixin class you also need to register
+ * {@link CookieMixin}.
+ * <p>
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new WebJackson2Module());
+ * </pre>
+ *
+ * @author Jitendra Singh
+ * @see WebJackson2Module
+ * @see org.springframework.security.jackson2.SecurityJacksonModules
+ * @since 4.2
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonDeserialize(builder = DefaultSavedRequest.Builder.class)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
+public abstract class DefaultSavedRequestMixin {
+}

+ 48 - 0
web/src/main/java/org/springframework/security/web/jackson2/SavedCookieMixin.java

@@ -0,0 +1,48 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.web.jackson2;
+
+import com.fasterxml.jackson.annotation.*;
+
+/**
+ * Jackson mixin class to serialize/deserialize {@link org.springframework.security.web.savedrequest.SavedCookie}
+ * serialization support.
+ *
+ * <pre>
+ * 		ObjectMapper mapper = new ObjectMapper();
+ *		mapper.registerModule(new WebJackson2Module());
+ * </pre>
+ *
+ * @author Jitendra Singh.
+ * @see WebJackson2Module
+ * @see org.springframework.security.jackson2.SecurityJacksonModules
+ * @since 4.2
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY,
+		getterVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public abstract class SavedCookieMixin {
+
+	@JsonCreator
+	public SavedCookieMixin(@JsonProperty("name") String name, @JsonProperty("value") String value,
+							@JsonProperty("comment") String comment, @JsonProperty("domain") String domain,
+							@JsonProperty("maxAge") int maxAge, @JsonProperty("path") String path,
+							@JsonProperty("secure") boolean secure, @JsonProperty("version") int version) {
+
+	}
+}

+ 44 - 0
web/src/main/java/org/springframework/security/web/jackson2/WebAuthenticationDetailsMixin.java

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.web.jackson2;
+
+import com.fasterxml.jackson.annotation.*;
+
+/**
+ * Jackson mixin class to serialize/deserialize {@link org.springframework.security.web.authentication.WebAuthenticationDetails}.
+ *
+ * <pre>
+ * 	ObjectMapper mapper = new ObjectMapper();
+ *	mapper.registerModule(new WebJackson2Module());
+ * </pre>
+ *
+ * @author Jitendra Singh
+ * @see WebJackson2Module
+ * @see org.springframework.security.jackson2.SecurityJacksonModules
+ * @since 4.2
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
+		isGetterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.ANY)
+public class WebAuthenticationDetailsMixin {
+
+	@JsonCreator
+	WebAuthenticationDetailsMixin(@JsonProperty("remoteAddress") String remoteAddress,
+									@JsonProperty("sessionId") String sessionId) {
+	}
+}

+ 61 - 0
web/src/main/java/org/springframework/security/web/jackson2/WebJackson2Module.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.web.jackson2;
+
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import org.springframework.security.jackson2.SecurityJacksonModules;
+import org.springframework.security.web.authentication.WebAuthenticationDetails;
+import org.springframework.security.web.csrf.DefaultCsrfToken;
+import org.springframework.security.web.savedrequest.DefaultSavedRequest;
+import org.springframework.security.web.savedrequest.SavedCookie;
+
+import javax.servlet.http.Cookie;
+
+/**
+ * Jackson module for spring-security-web. This module register {@link CookieMixin},
+ * {@link DefaultCsrfTokenMixin}, {@link DefaultSavedRequestMixin} and {@link WebAuthenticationDetailsMixin}. If no
+ * default typing enabled by default then it'll enable it because typing info is needed to properly serialize/deserialize objects.
+ * In order to use this module just add this module into your ObjectMapper configuration.
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new WebJackson2Module());
+ * </pre>
+ * <b>Note: use {@link SecurityJacksonModules#getModules()} to get list of all security modules.</b>
+ *
+ * @author Jitendra Singh
+ * @see SecurityJacksonModules
+ * @since 4.2
+ */
+public class WebJackson2Module extends SimpleModule {
+
+	public WebJackson2Module() {
+		super(WebJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null));
+	}
+
+	@Override
+	public void setupModule(SetupContext context) {
+		SecurityJacksonModules.enableDefaultTyping((ObjectMapper) context.getOwner());
+		context.setMixInAnnotations(Cookie.class, CookieMixin.class);
+		context.setMixInAnnotations(SavedCookie.class, SavedCookieMixin.class);
+		context.setMixInAnnotations(DefaultCsrfToken.class, DefaultCsrfTokenMixin.class);
+		context.setMixInAnnotations(DefaultSavedRequest.class, DefaultSavedRequestMixin.class);
+		context.setMixInAnnotations(WebAuthenticationDetails.class, WebAuthenticationDetailsMixin.class);
+	}
+}

+ 22 - 0
web/src/main/java/org/springframework/security/web/jackson2/package-info.java

@@ -0,0 +1,22 @@
+/*
+ * Copyright 2002-2016 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
+ *
+ *      http://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.
+ */
+/**
+ * Mix-in classes to provide Jackson serialization support.
+ *
+ * @author Jitendra Singh
+ * @since 4.2
+ */
+package org.springframework.security.web.jackson2;

+ 174 - 30
web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java

@@ -16,11 +16,14 @@
 
 package org.springframework.security.web.savedrequest;
 
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.springframework.security.web.PortResolver;
 import org.springframework.security.web.util.UrlUtils;
 import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
 
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
@@ -84,13 +87,7 @@ public class DefaultSavedRequest implements SavedRequest {
 		Assert.notNull(portResolver, "PortResolver required");
 
 		// Cookies
-		Cookie[] cookies = request.getCookies();
-
-		if (cookies != null) {
-			for (Cookie cookie : cookies) {
-				this.addCookie(cookie);
-			}
-		}
+		addCookies(request.getCookies());
 
 		// Headers
 		Enumeration<String> names = request.getHeaderNames();
@@ -110,27 +107,10 @@ public class DefaultSavedRequest implements SavedRequest {
 		}
 
 		// Locales
-		Enumeration<Locale> locales = request.getLocales();
-
-		while (locales.hasMoreElements()) {
-			Locale locale = (Locale) locales.nextElement();
-			this.addLocale(locale);
-		}
+		addLocales(request.getLocales());
 
 		// Parameters
-		Map<String, String[]> parameters = request.getParameterMap();
-
-		for (String paramName : parameters.keySet()) {
-			Object paramValues = parameters.get(paramName);
-			if (paramValues instanceof String[]) {
-				this.addParameter(paramName, (String[]) paramValues);
-			}
-			else {
-				if (logger.isWarnEnabled()) {
-					logger.warn("ServletRequest.getParameterMap() returned non-String array");
-				}
-			}
-		}
+		addParameters(request.getParameterMap());
 
 		// Primitives
 		this.method = request.getMethod();
@@ -145,9 +125,36 @@ public class DefaultSavedRequest implements SavedRequest {
 		this.servletPath = request.getServletPath();
 	}
 
+	/**
+	 * Private constructor invoked through Builder
+	 */
+	private DefaultSavedRequest(Builder builder) {
+		this.contextPath = builder.contextPath;
+		this.method = builder.method;
+		this.pathInfo = builder.pathInfo;
+		this.queryString = builder.queryString;
+		this.requestURI = builder.requestURI;
+		this.requestURL = builder.requestURL;
+		this.scheme = builder.scheme;
+		this.serverName = builder.serverName;
+		this.servletPath = builder.servletPath;
+		this.serverPort = builder.serverPort;
+	}
+
 	// ~ Methods
 	// ========================================================================================================
 
+	/**
+	 * @since 4.2
+	 */
+	private void addCookies(Cookie[] cookies) {
+		if (cookies != null) {
+			for (Cookie cookie : cookies) {
+				this.addCookie(cookie);
+			}
+		}
+	}
+
 	private void addCookie(Cookie cookie) {
 		cookies.add(new SavedCookie(cookie));
 	}
@@ -163,10 +170,38 @@ public class DefaultSavedRequest implements SavedRequest {
 		values.add(value);
 	}
 
+	/**
+	 * @since 4.2
+	 */
+	private void addLocales(Enumeration<Locale> locales) {
+		while (locales.hasMoreElements()) {
+			Locale locale = locales.nextElement();
+			this.addLocale(locale);
+		}
+	}
+
 	private void addLocale(Locale locale) {
 		locales.add(locale);
 	}
 
+	/**
+	 * @since 4.2
+	 */
+	private void addParameters(Map<String, String[]> parameters) {
+		if (!ObjectUtils.isEmpty(parameters)) {
+			for (String paramName : parameters.keySet()) {
+				Object paramValues = parameters.get(paramName);
+				if (paramValues instanceof String[]) {
+					this.addParameter(paramName, (String[]) paramValues);
+				} else {
+					if (logger.isWarnEnabled()) {
+						logger.warn("ServletRequest.getParameterMap() returned non-String array");
+					}
+				}
+			}
+		}
+	}
+
 	private void addParameter(String name, String[] values) {
 		parameters.put(name, values);
 	}
@@ -176,10 +211,9 @@ public class DefaultSavedRequest implements SavedRequest {
 	 * <p>
 	 * All URL arguments are considered but not cookies, locales, headers or parameters.
 	 *
-	 * @param request the actual request to be matched against this one
+	 * @param request      the actual request to be matched against this one
 	 * @param portResolver used to obtain the server port of the request
 	 * @return true if the request is deemed to match this one.
-	 *
 	 */
 	public boolean doesRequestMatch(HttpServletRequest request, PortResolver portResolver) {
 
@@ -341,8 +375,7 @@ public class DefaultSavedRequest implements SavedRequest {
 			}
 
 			return true;
-		}
-		else {
+		} else {
 			if (logger.isDebugEnabled()) {
 				logger.debug(log + ": arg1=" + arg1 + "; arg2=" + arg2
 						+ " (property not equals)");
@@ -355,4 +388,115 @@ public class DefaultSavedRequest implements SavedRequest {
 	public String toString() {
 		return "DefaultSavedRequest[" + getRedirectUrl() + "]";
 	}
+
+	/**
+	 * @since 4.2
+	 */
+	@JsonIgnoreProperties(ignoreUnknown = true)
+	@JsonPOJOBuilder(withPrefix = "set")
+	public static class Builder {
+
+		private List<SavedCookie> cookies = null;
+		private List<Locale> locales = null;
+		private Map<String, List<String>> headers = new TreeMap<String, List<String>>(String.CASE_INSENSITIVE_ORDER);
+		private Map<String, String[]> parameters = new TreeMap<String, String[]>();
+		private String contextPath;
+		private String method;
+		private String pathInfo;
+		private String queryString;
+		private String requestURI;
+		private String requestURL;
+		private String scheme;
+		private String serverName;
+		private String servletPath;
+		private int serverPort = 80;
+
+		public Builder setCookies(List<SavedCookie> cookies) {
+			this.cookies = cookies;
+			return this;
+		}
+
+		public Builder setLocales(List<Locale> locales) {
+			this.locales = locales;
+			return this;
+		}
+
+		public Builder setHeaders(Map<String, List<String>> header) {
+			this.headers.putAll(header);
+			return this;
+		}
+
+		public Builder setParameters(Map<String, String[]> parameters) {
+			this.parameters = parameters;
+			return this;
+		}
+
+		public Builder setContextPath(String contextPath) {
+			this.contextPath = contextPath;
+			return this;
+		}
+
+		public Builder setMethod(String method) {
+			this.method = method;
+			return this;
+		}
+
+		public Builder setPathInfo(String pathInfo) {
+			this.pathInfo = pathInfo;
+			return this;
+		}
+
+		public Builder setQueryString(String queryString) {
+			this.queryString = queryString;
+			return this;
+		}
+
+		public Builder setRequestURI(String requestURI) {
+			this.requestURI = requestURI;
+			return this;
+		}
+
+		public Builder setRequestURL(String requestURL) {
+			this.requestURL = requestURL;
+			return this;
+		}
+
+		public Builder setScheme(String scheme) {
+			this.scheme = scheme;
+			return this;
+		}
+
+		public Builder setServerName(String serverName) {
+			this.serverName = serverName;
+			return this;
+		}
+
+		public Builder setServletPath(String servletPath) {
+			this.servletPath = servletPath;
+			return this;
+		}
+
+		public Builder setServerPort(int serverPort) {
+			this.serverPort = serverPort;
+			return this;
+		}
+
+		public DefaultSavedRequest build() {
+			DefaultSavedRequest savedRequest = new DefaultSavedRequest(this);
+			if(!ObjectUtils.isEmpty(this.cookies)) {
+				for (SavedCookie cookie : this.cookies) {
+					savedRequest.addCookie(cookie.getCookie());
+				}
+			}
+			if (!ObjectUtils.isEmpty(this.locales))
+				savedRequest.locales.addAll(this.locales);
+			savedRequest.addParameters(this.parameters);
+
+			this.headers.remove(HEADER_IF_MODIFIED_SINCE);
+			this.headers.remove(HEADER_IF_NONE_MATCH);
+			if (!ObjectUtils.isEmpty(this.headers))
+				savedRequest.headers.putAll(this.headers);
+			return savedRequest;
+		}
+	}
 }

+ 41 - 0
web/src/test/java/org/springframework/security/web/jackson2/AbstractMixinTests.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.web.jackson2;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.security.jackson2.SecurityJacksonModules;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * @author Jitenra Singh
+ * @since 4.2
+ */
+@RunWith(MockitoJUnitRunner.class)
+public abstract class AbstractMixinTests {
+
+	ObjectMapper mapper;
+
+	protected ObjectMapper buildObjectMapper() {
+		if (ObjectUtils.isEmpty(mapper)) {
+			mapper = new ObjectMapper();
+			mapper.registerModules(SecurityJacksonModules.getModules());
+		}
+		return mapper;
+	}
+}

+ 61 - 0
web/src/test/java/org/springframework/security/web/jackson2/CookieMixinTests.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.web.jackson2;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.json.JSONException;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.security.jackson2.SecurityJacksonModules;
+
+import javax.servlet.http.Cookie;
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jitendra Singh
+ * @since 4.2
+ */
+public class CookieMixinTests {
+
+	String cookieJson = "{\"@class\": \"javax.servlet.http.Cookie\", \"name\": \"demo\", \"value\": \"cookie1\"," +
+			"\"comment\": null, \"maxAge\": -1, \"path\": null, \"secure\": false, \"version\": 0, \"isHttpOnly\": false, \"domain\": null}";
+
+	ObjectMapper buildObjectMapper() {
+		ObjectMapper mapper = new ObjectMapper();
+		mapper.registerModules(SecurityJacksonModules.getModules());
+		return mapper;
+	}
+
+	@Test
+	public void serializeCookie() throws JsonProcessingException, JSONException {
+		Cookie cookie = new Cookie("demo", "cookie1");
+		String actualString = buildObjectMapper().writeValueAsString(cookie);
+		JSONAssert.assertEquals(cookieJson, actualString, true);
+	}
+
+	@Test
+	public void deserializeCookie() throws IOException {
+		Cookie cookie = buildObjectMapper().readValue(cookieJson, Cookie.class);
+		assertThat(cookie).isNotNull();
+		assertThat(cookie.getName()).isEqualTo("demo");
+		assertThat(cookie.getDomain()).isEqualTo("");
+		assertThat(cookie.isHttpOnly()).isEqualTo(false);
+	}
+}

+ 80 - 0
web/src/test/java/org/springframework/security/web/jackson2/DefaultCsrfTokenMixinTests.java

@@ -0,0 +1,80 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.web.jackson2;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.json.JSONException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.security.jackson2.SecurityJacksonModules;
+import org.springframework.security.web.csrf.DefaultCsrfToken;
+
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jitendra Singh
+ * @since 4.2
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class DefaultCsrfTokenMixinTests {
+
+	ObjectMapper objectMapper;
+	String defaultCsrfTokenJson;
+
+	@Before
+	public void setup() {
+		objectMapper = new ObjectMapper();
+		objectMapper.registerModules(SecurityJacksonModules.getModules());
+		defaultCsrfTokenJson = "{\"@class\": \"org.springframework.security.web.csrf.DefaultCsrfToken\", " +
+				"\"headerName\": \"csrf-header\", \"parameterName\": \"_csrf\", \"token\": \"1\"}";
+	}
+
+	@Test
+	public void defaultCsrfTokenSerializedTest() throws JsonProcessingException, JSONException {
+		DefaultCsrfToken token = new DefaultCsrfToken("csrf-header", "_csrf", "1");
+		String serializedJson = objectMapper.writeValueAsString(token);
+		JSONAssert.assertEquals(defaultCsrfTokenJson, serializedJson, true);
+	}
+
+	@Test
+	public void defaultCsrfTokenDeserializeTest() throws IOException {
+		DefaultCsrfToken token = objectMapper.readValue(defaultCsrfTokenJson, DefaultCsrfToken.class);
+		assertThat(token).isNotNull();
+		assertThat(token.getHeaderName()).isEqualTo("csrf-header");
+		assertThat(token.getParameterName()).isEqualTo("_csrf");
+		assertThat(token.getToken()).isEqualTo("1");
+	}
+
+	@Test(expected = JsonMappingException.class)
+	public void defaultCsrfTokenDeserializeWithoutClassTest() throws IOException {
+		String tokenJson = "{\"headerName\": \"csrf-header\", \"parameterName\": \"_csrf\", \"token\": \"1\"}";
+		objectMapper.readValue(tokenJson, DefaultCsrfToken.class);
+	}
+
+	@Test(expected = JsonMappingException.class)
+	public void defaultCsrfTokenDeserializeNullValuesTest() throws IOException {
+		String tokenJson = "{\"@class\": \"org.springframework.security.web.csrf.DefaultCsrfToken\", \"headerName\": \"\", \"parameterName\": null, \"token\": \"1\"}";
+		objectMapper.readValue(tokenJson, DefaultCsrfToken.class);
+	}
+}

+ 95 - 0
web/src/test/java/org/springframework/security/web/jackson2/DefaultSavedRequestMixinTests.java

@@ -0,0 +1,95 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.web.jackson2;
+
+import org.json.JSONException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.web.PortResolverImpl;
+import org.springframework.security.web.savedrequest.DefaultSavedRequest;
+import org.springframework.security.web.savedrequest.SavedCookie;
+
+import javax.servlet.http.Cookie;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Locale;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jitendra Singh
+ * @since 4.2
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class DefaultSavedRequestMixinTests extends AbstractMixinTests {
+
+	String defaultSavedRequestJson = "{" +
+			"\"@class\": \"org.springframework.security.web.savedrequest.DefaultSavedRequest\", \"cookies\": [\"java.util.ArrayList\", [{\"@class\": \"org.springframework.security.web.savedrequest.SavedCookie\", \"name\": \"SESSION\", \"value\": \"123456789\", \"comment\": null, \"maxAge\": -1, \"path\": null, \"secure\":false, \"version\": 0, \"domain\": null}]]," +
+			"\"locales\": [\"java.util.ArrayList\", [\"en\"]], \"headers\": {\"@class\": \"java.util.TreeMap\", \"x-auth-token\": [\"java.util.ArrayList\", [\"12\"]]}, \"parameters\": {\"@class\": \"java.util.TreeMap\"}," +
+			"\"contextPath\": \"\", \"method\": \"\", \"pathInfo\": null, \"queryString\": null, \"requestURI\": \"\", \"requestURL\": \"http://localhost\", \"scheme\": \"http\", " +
+			"\"serverName\": \"localhost\", \"servletPath\": \"\", \"serverPort\": 80"+
+			"}";
+
+	@Test
+	public void matchRequestBuildWithConstructorAndBuilder() {
+		DefaultSavedRequest request = new DefaultSavedRequest.Builder()
+				.setCookies(Collections.singletonList(new SavedCookie(new Cookie("SESSION", "123456789"))))
+				.setHeaders(Collections.singletonMap("x-auth-token", Collections.singletonList("12")))
+				.setScheme("http").setRequestURL("http://localhost").setServerName("localhost").setRequestURI("")
+				.setLocales(Collections.singletonList(new Locale("en"))).setContextPath("").setMethod("")
+				.setServletPath("").build();
+		MockHttpServletRequest mockRequest = new MockHttpServletRequest();
+		mockRequest.setCookies(new Cookie("SESSION", "123456789"));
+		mockRequest.addHeader("x-auth-token", "12");
+
+		assert request.doesRequestMatch(mockRequest, new PortResolverImpl());
+	}
+
+	@Test
+	public void serializeDefaultRequestBuildWithConstructorTest() throws IOException, JSONException {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setCookies(new Cookie("SESSION", "123456789"));
+		request.addHeader("x-auth-token", "12");
+		String actualString = buildObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(new DefaultSavedRequest(request, new PortResolverImpl()));
+		JSONAssert.assertEquals(defaultSavedRequestJson, actualString, true);
+	}
+
+	@Test
+	public void serializeDefaultRequestBuildWithBuilderTest() throws IOException, JSONException {
+		DefaultSavedRequest request = new DefaultSavedRequest.Builder()
+				.setCookies(Collections.singletonList(new SavedCookie(new Cookie("SESSION", "123456789"))))
+				.setHeaders(Collections.singletonMap("x-auth-token", Collections.singletonList("12")))
+				.setScheme("http").setRequestURL("http://localhost").setServerName("localhost").setRequestURI("")
+				.setLocales(Collections.singletonList(new Locale("en"))).setContextPath("").setMethod("")
+				.setServletPath("").build();
+		String actualString = buildObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(request);
+		JSONAssert.assertEquals(defaultSavedRequestJson, actualString, true);
+	}
+
+	@Test
+	public void deserializeDefaultSavedRequest() throws IOException {
+		DefaultSavedRequest request = (DefaultSavedRequest) buildObjectMapper().readValue(defaultSavedRequestJson, Object.class);
+		assertThat(request).isNotNull();
+		assertThat(request.getCookies()).hasSize(1);
+		assertThat(request.getLocales()).hasSize(1).contains(new Locale("en"));
+		assertThat(request.getHeaderNames()).hasSize(1).contains("x-auth-token");
+		assertThat(request.getHeaderValues("x-auth-token")).hasSize(1).contains("12");
+	}
+}

+ 96 - 0
web/src/test/java/org/springframework/security/web/jackson2/SavedCookieMixinTests.java

@@ -0,0 +1,96 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.web.jackson2;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.json.JSONException;
+import org.junit.Before;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.security.web.savedrequest.SavedCookie;
+
+import javax.servlet.http.Cookie;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jitendra Singh.
+ */
+public class SavedCookieMixinTests extends AbstractMixinTests {
+
+	private String expectedSavedCookieJson;
+
+	@Before
+	public void setup() {
+		expectedSavedCookieJson = "{\"@class\": \"org.springframework.security.web.savedrequest.SavedCookie\", " +
+				"\"name\": \"session\", \"value\": \"123456\", \"comment\": null, \"domain\": null, \"maxAge\": -1, " +
+				"\"path\": null, \"secure\": false, \"version\": 0}";
+	}
+
+
+	@Test
+	public void serializeWithDefaultConfigurationTest() throws JsonProcessingException, JSONException {
+		SavedCookie savedCookie = new SavedCookie(new Cookie("session", "123456"));
+		String actualJson = buildObjectMapper().writeValueAsString(savedCookie);
+		JSONAssert.assertEquals(expectedSavedCookieJson, actualJson, true);
+	}
+
+	@Test
+	public void serializeWithOverrideConfigurationTest() throws JsonProcessingException, JSONException {
+		SavedCookie savedCookie = new SavedCookie(new Cookie("session", "123456"));
+		ObjectMapper mapper = buildObjectMapper();
+		mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.PUBLIC_ONLY)
+				.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.ANY);
+		String actualJson = mapper.writeValueAsString(savedCookie);
+		JSONAssert.assertEquals(expectedSavedCookieJson, actualJson, true);
+	}
+
+	@Test
+	public void serializeSavedCookieWithList() throws JsonProcessingException, JSONException {
+		List<SavedCookie> savedCookies = new ArrayList<SavedCookie>();
+		savedCookies.add(new SavedCookie(new Cookie("session", "123456")));
+		String expectedJson = String.format("[\"java.util.ArrayList\", [%s]]", expectedSavedCookieJson);
+		String actualJson = buildObjectMapper().writeValueAsString(savedCookies);
+		JSONAssert.assertEquals(expectedJson, actualJson, true);
+	}
+
+	@Test
+	public void deserializeSavedCookieWithList() throws IOException, JSONException {
+		String expectedJson = String.format("[\"java.util.ArrayList\", [%s]]", expectedSavedCookieJson);
+		List<SavedCookie> savedCookies = (List<SavedCookie>)buildObjectMapper().readValue(expectedJson, Object.class);
+		assertThat(savedCookies).isNotNull().hasSize(1);
+		assertThat(savedCookies.get(0).getName()).isEqualTo("session");
+		assertThat(savedCookies.get(0).getValue()).isEqualTo("123456");
+	}
+
+	@Test
+	public void deserializeSavedCookieJsonTest() throws IOException {
+		SavedCookie savedCookie = (SavedCookie) buildObjectMapper().readValue(expectedSavedCookieJson, Object.class);
+		assertThat(savedCookie).isNotNull();
+		assertThat(savedCookie.getName()).isEqualTo("session");
+		assertThat(savedCookie.getValue()).isEqualTo("123456");
+		assertThat(savedCookie.isSecure()).isEqualTo(false);
+		assertThat(savedCookie.getVersion()).isEqualTo(0);
+		assertThat(savedCookie.getComment()).isNull();
+	}
+}

+ 88 - 0
web/src/test/java/org/springframework/security/web/jackson2/WebAuthenticationDetailsMixinTests.java

@@ -0,0 +1,88 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.web.jackson2;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.json.JSONException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpSession;
+import org.springframework.security.jackson2.SecurityJacksonModules;
+import org.springframework.security.web.authentication.WebAuthenticationDetails;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jitendra Singh
+ * @since 4.2
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class WebAuthenticationDetailsMixinTests {
+
+	ObjectMapper mapper;
+	String webAuthenticationDetailsJson = "{\"@class\": \"org.springframework.security.web.authentication.WebAuthenticationDetails\","
+			+ "\"sessionId\": \"1\", \"remoteAddress\": \"/localhost\"}";
+
+	@Before
+	public void setup() {
+		this.mapper = new ObjectMapper();
+		this.mapper.registerModules(SecurityJacksonModules.getModules());
+	}
+
+	@Test
+	public void buildWebAuthenticationDetailsUsingDifferentConstructors()
+			throws IOException {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setRemoteAddr("localhost");
+		request.setSession(new MockHttpSession(null, "1"));
+
+		WebAuthenticationDetails details = new WebAuthenticationDetails(request);
+
+		WebAuthenticationDetails authenticationDetails = this.mapper.readValue(webAuthenticationDetailsJson,
+				WebAuthenticationDetails.class);
+		assertThat(details.equals(authenticationDetails));
+	}
+
+	@Test
+	public void webAuthenticationDetailsSerializeTest()
+			throws JsonProcessingException, JSONException {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setRemoteAddr("/localhost");
+		request.setSession(new MockHttpSession(null, "1"));
+		WebAuthenticationDetails details = new WebAuthenticationDetails(request);
+		String actualJson = this.mapper.writeValueAsString(details);
+		JSONAssert.assertEquals(webAuthenticationDetailsJson, actualJson, true);
+	}
+
+	@Test
+	public void webAuthenticationDetailsDeserializeTest()
+			throws IOException, JSONException {
+		WebAuthenticationDetails details = this.mapper.readValue(webAuthenticationDetailsJson,
+				WebAuthenticationDetails.class);
+		assertThat(details).isNotNull();
+		assertThat(details.getRemoteAddress()).isEqualTo("/localhost");
+		assertThat(details.getSessionId()).isEqualTo("1");
+	}
+}

+ 4 - 2
web/web.gradle

@@ -11,7 +11,8 @@ dependencies {
 
 	optional "org.springframework:spring-webmvc:$springVersion",
 			"org.springframework:spring-jdbc:$springVersion",
-			"org.springframework:spring-tx:$springVersion"
+			"org.springframework:spring-tx:$springVersion",
+			"com.fasterxml.jackson.core:jackson-databind:$jacksonDatavindVersion"
 
 	provided "javax.servlet:javax.servlet-api:$servletApiVersion"
 
@@ -20,7 +21,8 @@ dependencies {
 				"org.slf4j:jcl-over-slf4j:$slf4jVersion",
 				"org.codehaus.groovy:groovy-all:$groovyVersion",
 				powerMockDependencies,
-				spockDependencies
+				spockDependencies,
+				"org.skyscreamer:jsonassert:$jsonassertVersion"
 
 	testRuntime "org.hsqldb:hsqldb:$hsqlVersion"
 }