diff --git a/cas/cas.gradle b/cas/cas.gradle index 9149d794839..eb3e27b2784 100644 --- a/cas/cas.gradle +++ b/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" } diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java index 856781ae5e1..e8250195f89 100644 --- a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java +++ b/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 null) + * @param key to identify if this object made by a given + * {@link CasAuthenticationProvider} + * @param principal typically the UserDetails object (cannot be null) * @param credentials the service/proxy ticket ID from CAS (cannot be - * null) + * null) * @param authorities the authorities granted to the user (from the - * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot - * be null) + * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot + * be null) * @param userDetails the user details (from the - * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot - * be null) - * @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 null) + * @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 null was passed */ public CasAuthenticationToken(final String key, final Object principal, - final Object credentials, - final Collection authorities, - final UserDetails userDetails, final Assertion assertion) { + final Object credentials, + final Collection 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 null) + * @param credentials the service/proxy ticket ID from CAS (cannot be + * null) + * @param authorities the authorities granted to the user (from the + * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot + * be null) + * @param userDetails the user details (from the + * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot + * be null) + * @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 null was passed + * @since 4.2 + */ + private CasAuthenticationToken(final Integer keyHash, final Object principal, + final Object credentials, + final Collection 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 CasAuthenticationToken(final String key, final Object principal, "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 CasAuthenticationToken(final String key, final Object principal, // ~ 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; diff --git a/cas/src/main/java/org/springframework/security/cas/jackson2/AssertionImplMixin.java b/cas/src/main/java/org/springframework/security/cas/jackson2/AssertionImplMixin.java new file mode 100644 index 00000000000..0e3dc552bed --- /dev/null +++ b/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. + *

+ *

+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CasJackson2Module());
+ * 
+ * + * + * @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 attributes){ + } +} \ No newline at end of file diff --git a/cas/src/main/java/org/springframework/security/cas/jackson2/AttributePrincipalImplMixin.java b/cas/src/main/java/org/springframework/security/cas/jackson2/AttributePrincipalImplMixin.java new file mode 100644 index 00000000000..f9e71b2a547 --- /dev/null +++ b/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. + *

+ *

+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CasJackson2Module());
+ * 
+ * + * @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 attributes, + @JsonProperty("proxyGrantingTicket") String proxyGrantingTicket, + @JsonProperty("proxyRetriever") ProxyRetriever proxyRetriever) { + } +} \ No newline at end of file diff --git a/cas/src/main/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixin.java b/cas/src/main/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixin.java new file mode 100644 index 00000000000..7e7ae7847ae --- /dev/null +++ b/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. + *
    + *
  1. {@link org.springframework.security.cas.jackson2.AssertionImplMixin}
  2. + *
  3. {@link org.springframework.security.cas.jackson2.AttributePrincipalImplMixin}
  4. + *
+ * + *

+ * + *

+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CasJackson2Module());
+ * 
+ * + * @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 null) + * @param credentials the service/proxy ticket ID from CAS (cannot be + * null) + * @param authorities the authorities granted to the user (from the + * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot + * be null) + * @param userDetails the user details (from the + * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot + * be null) + * @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 authorities, + @JsonProperty("userDetails") UserDetails userDetails, @JsonProperty("assertion") Assertion assertion) { + } +} diff --git a/cas/src/main/java/org/springframework/security/cas/jackson2/CasJackson2Module.java b/cas/src/main/java/org/springframework/security/cas/jackson2/CasJackson2Module.java new file mode 100644 index 00000000000..8d7bf6046af --- /dev/null +++ b/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. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CasJackson2Module());
+ * 
+ * Note: use {@link SecurityJacksonModules#getModules()} to get list of all security modules. + * + * @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); + } +} diff --git a/cas/src/test/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixinTests.java b/cas/src/test/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixinTests.java new file mode 100644 index 00000000000..8605a12a230 --- /dev/null +++ b/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 authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); + Assertion assertion = new AssertionImpl(new AttributePrincipalImpl("assertName"), startDate, endDate, startDate, Collections.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.emptyList(), + new User("user", PASSWORD, Collections.emptyList()), null); + } + + @Test(expected = IllegalArgumentException.class) + public void blankKeyTest() { + new CasAuthenticationToken("", "user", PASSWORD, Collections.emptyList(), + new User("user", PASSWORD, Collections.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); + } +} diff --git a/core/core.gradle b/core/core.gradle index c2866496a08..2a4a1a7f277 100644 --- a/core/core.gradle +++ b/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" } diff --git a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java index 4eb602cc6fa..eb5c8e06db7 100644 --- a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java +++ b/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 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 GrantedAuthoritys for the principal - * represented by this authentication object. + * represented by this authentication object. */ public AbstractAuthenticationToken(Collection authorities) { if (authorities == null) { @@ -215,8 +215,7 @@ public String toString() { sb.append(authority); } - } - else { + } else { sb.append("Not granted any authorities"); } diff --git a/core/src/main/java/org/springframework/security/authentication/AnonymousAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/AnonymousAuthenticationToken.java index eb501a8c9cf..04c1f156f68 100644 --- a/core/src/main/java/org/springframework/security/authentication/AnonymousAuthenticationToken.java +++ b/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 UserDetails) + * @param key to identify if this object made by an authorised client + * @param principal the principal (typically a UserDetails) * @param authorities the authorities granted to the principal - * * @throws IllegalArgumentException if a null was passed */ public AnonymousAuthenticationToken(String key, Object principal, - Collection authorities) { + Collection 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 UserDetails) + * @param authorities the authorities granted to the principal + * @since 4.2 + */ + private AnonymousAuthenticationToken(Integer keyHash, Object principal, + Collection 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 AnonymousAuthenticationToken(String key, Object principal, // ~ 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; diff --git a/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java index fa5007fb6c8..3fd5486cbe2 100644 --- a/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java +++ b/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 UserDetails) + * @param key to identify if this object made by an authorised client + * @param principal the principal (typically a UserDetails) * @param authorities the authorities granted to the principal - * * @throws IllegalArgumentException if a null was passed */ public RememberMeAuthenticationToken(String key, Object principal, - Collection authorities) { + Collection authorities) { super(authorities); if ((key == null) || ("".equals(key)) || (principal == null) @@ -67,6 +66,22 @@ public RememberMeAuthenticationToken(String key, Object principal, setAuthenticated(true); } + /** + * Private Constructor to help in Jackson deserialization. + * + * @param keyHash hashCode of above given key. + * @param principal the principal (typically a UserDetails) + * @param authorities the authorities granted to the principal + * @since 4.2 + */ + private RememberMeAuthenticationToken(Integer keyHash, Object principal, Collection authorities) { + super(authorities); + + this.keyHash = keyHash; + this.principal = principal; + setAuthenticated(true); + } + // ~ Methods // ======================================================================================================== diff --git a/core/src/main/java/org/springframework/security/jackson2/AnonymousAuthenticationTokenMixin.java b/core/src/main/java/org/springframework/security/jackson2/AnonymousAuthenticationTokenMixin.java new file mode 100644 index 00000000000..c8207958246 --- /dev/null +++ b/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. + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * 
+ * + * Note: This class will save full class name into a property called @class + * + * @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 UserDetails) + * @param authorities the authorities granted to the principal + */ + @JsonCreator + public AnonymousAuthenticationTokenMixin(@JsonProperty("keyHash") Integer keyHash, @JsonProperty("principal") Object principal, + @JsonProperty("authorities") Collection authorities) { + } +} diff --git a/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java b/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java new file mode 100644 index 00000000000..7db25de05f0 --- /dev/null +++ b/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. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * 
+ * Note: use {@link SecurityJacksonModules#getModules()} to get list of all security modules. + * + * @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); + } +} diff --git a/core/src/main/java/org/springframework/security/jackson2/RememberMeAuthenticationTokenMixin.java b/core/src/main/java/org/springframework/security/jackson2/RememberMeAuthenticationTokenMixin.java new file mode 100644 index 00000000000..5ea4230895f --- /dev/null +++ b/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. + * + *
    + *
  1. {@link SimpleGrantedAuthorityMixin}
  2. + *
  3. {@link UserMixin}
  4. + *
  5. {@link UnmodifiableSetMixin}
  6. + *
+ * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * 
+ * + * Note: This class will save TypeInfo (full class name) into a property called @class + * + * @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 UserDetails) + * @param authorities the authorities granted to the principal + */ + @JsonCreator + public RememberMeAuthenticationTokenMixin(@JsonProperty("keyHash") Integer keyHash, + @JsonProperty("principal") Object principal, + @JsonProperty("authorities") Collection authorities) { + } +} diff --git a/core/src/main/java/org/springframework/security/jackson2/SecurityJacksonModules.java b/core/src/main/java/org/springframework/security/jackson2/SecurityJacksonModules.java new file mode 100644 index 00000000000..27603fe97ac --- /dev/null +++ b/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. + * + *

+ *

+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModules(SecurityJacksonModules.getModules());
+ * 
+ * Above code is equivalent to + *

+ *

+ *     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());
+ * 
+ * + * @author Jitendra Singh. + * @since 4.2 + */ +public final class SecurityJacksonModules { + + private static final Log logger = LogFactory.getLog(SecurityJacksonModules.class); + private static final List 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 securityModule = (Class) 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 getModules() { + List modules = new ArrayList(); + for (String className : securityJackson2ModuleClasses) { + Module module = loadAndGetInstance(className); + if (!ObjectUtils.isEmpty(module)) { + modules.add(module); + } + } + return modules; + } +} diff --git a/core/src/main/java/org/springframework/security/jackson2/SimpleGrantedAuthorityMixin.java b/core/src/main/java/org/springframework/security/jackson2/SimpleGrantedAuthorityMixin.java new file mode 100644 index 00000000000..fd11cf14e18 --- /dev/null +++ b/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}. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * 
+ * @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 authority key, it will be serialized + * as role key. Because above mixin constructor will look for role key to properly deserialize. + * + * @return + */ + @JsonProperty("role") + public abstract String getAuthority(); +} diff --git a/core/src/main/java/org/springframework/security/jackson2/UnmodifiableSetMixin.java b/core/src/main/java/org/springframework/security/jackson2/UnmodifiableSetMixin.java new file mode 100644 index 00000000000..3fd3875efde --- /dev/null +++ b/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. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * 
+ * + * @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) {} +} diff --git a/core/src/main/java/org/springframework/security/jackson2/UserDeserializer.java b/core/src/main/java/org/springframework/security/jackson2/UserDeserializer.java new file mode 100644 index 00000000000..5367fd481e8 --- /dev/null +++ b/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 { + + /** + * 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 authorities = mapper.convertValue(jsonNode.get("authorities"), new TypeReference>() { + }); + 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(); + } +} diff --git a/core/src/main/java/org/springframework/security/jackson2/UserMixin.java b/core/src/main/java/org/springframework/security/jackson2/UserMixin.java new file mode 100644 index 00000000000..3f86ac92a20 --- /dev/null +++ b/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. + *
    + *
  1. {@link SimpleGrantedAuthorityMixin}
  2. + *
  3. {@link UnmodifiableSetMixin}
  4. + *
+ *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * 
+ * + * @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 { +} diff --git a/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java b/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java new file mode 100644 index 00000000000..985f605e4f3 --- /dev/null +++ b/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 authenticated property. + * It will ensure that the token's state must not change. + *

+ * 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 { + + /** + * 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() {}); + } else { + principal = principalNode.asText(); + } + Object credentials = readJsonNode(jsonNode, "credentials").asText(); + List authorities = mapper.readValue( + readJsonNode(jsonNode, "authorities").toString(), new TypeReference>() { + }); + 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(); + } +} diff --git a/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenMixin.java b/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenMixin.java new file mode 100644 index 00000000000..9815b687372 --- /dev/null +++ b/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. + *

    + *
  1. {@link UnmodifiableSetMixin}
  2. + *
  3. {@link SimpleGrantedAuthorityMixin}
  4. + *
  5. {@link UserMixin}
  6. + *
+ * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * 
+ * @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 { +} diff --git a/core/src/main/java/org/springframework/security/jackson2/package-info.java b/core/src/main/java/org/springframework/security/jackson2/package-info.java new file mode 100644 index 00000000000..fadd25a8cf5 --- /dev/null +++ b/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. + */ \ No newline at end of file diff --git a/core/src/test/java/org/springframework/security/jackson2/AbstractMixinTests.java b/core/src/test/java/org/springframework/security/jackson2/AbstractMixinTests.java new file mode 100644 index 00000000000..8b78118d3e9 --- /dev/null +++ b/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))); + } +} diff --git a/core/src/test/java/org/springframework/security/jackson2/AnonymousAuthenticationTokenMixinTests.java b/core/src/test/java/org/springframework/security/jackson2/AnonymousAuthenticationTokenMixinTests.java new file mode 100644 index 00000000000..542148e8f8f --- /dev/null +++ b/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.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); + } +} diff --git a/core/src/test/java/org/springframework/security/jackson2/RememberMeAuthenticationTokenMixinTests.java b/core/src/test/java/org/springframework/security/jackson2/RememberMeAuthenticationTokenMixinTests.java new file mode 100644 index 00000000000..b55d8f96648 --- /dev/null +++ b/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.emptyList()); + } + + @Test(expected = IllegalArgumentException.class) + public void testWithNullKey() throws JsonProcessingException, JSONException { + new RememberMeAuthenticationToken(null, "principal", Collections.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); + } +} diff --git a/core/src/test/java/org/springframework/security/jackson2/SecurityContextMixinTests.java b/core/src/test/java/org/springframework/security/jackson2/SecurityContextMixinTests.java new file mode 100644 index 00000000000..4a3c16b161f --- /dev/null +++ b/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")); + } +} diff --git a/core/src/test/java/org/springframework/security/jackson2/SimpleGrantedAuthorityMixinTests.java b/core/src/test/java/org/springframework/security/jackson2/SimpleGrantedAuthorityMixinTests.java new file mode 100644 index 00000000000..ed2563f48d6 --- /dev/null +++ b/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); + } +} diff --git a/core/src/test/java/org/springframework/security/jackson2/UserDeserializerTests.java b/core/src/test/java/org/springframework/security/jackson2/UserDeserializerTests.java new file mode 100644 index 00000000000..295a563baae --- /dev/null +++ b/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.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")); + } +} diff --git a/core/src/test/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenMixinTests.java b/core/src/test/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenMixinTests.java new file mode 100644 index 00000000000..b5bdd58f07f --- /dev/null +++ b/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); + } +} diff --git a/gradle/javaprojects.gradle b/gradle/javaprojects.gradle index 10c9548e2d3..eff3a10b83d 100644 --- a/gradle/javaprojects.gradle +++ b/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") { diff --git a/web/src/main/java/org/springframework/security/web/authentication/WebAuthenticationDetails.java b/web/src/main/java/org/springframework/security/web/authentication/WebAuthenticationDetails.java index 2d0f1d6c838..6957fa9753d 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/WebAuthenticationDetails.java +++ b/web/src/main/java/org/springframework/security/web/authentication/WebAuthenticationDetails.java @@ -54,6 +54,17 @@ public WebAuthenticationDetails(HttpServletRequest request) { 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 // ======================================================================================================== diff --git a/web/src/main/java/org/springframework/security/web/jackson2/CookieDeserializer.java b/web/src/main/java/org/springframework/security/web/jackson2/CookieDeserializer.java new file mode 100644 index 00000000000..ff6876a61b1 --- /dev/null +++ b/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 { + + @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(); + } +} diff --git a/web/src/main/java/org/springframework/security/web/jackson2/CookieMixin.java b/web/src/main/java/org/springframework/security/web/jackson2/CookieMixin.java new file mode 100644 index 00000000000..9b5816a6460 --- /dev/null +++ b/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} + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new WebJackson2Module());
+ * 
+ * + * @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 { +} diff --git a/web/src/main/java/org/springframework/security/web/jackson2/DefaultCsrfTokenMixin.java b/web/src/main/java/org/springframework/security/web/jackson2/DefaultCsrfTokenMixin.java new file mode 100644 index 00000000000..46be5b66701 --- /dev/null +++ b/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. + * + *
+ * 		ObjectMapper mapper = new ObjectMapper();
+ *		mapper.registerModule(new WebJackson2Module());
+ * 
+ * + * @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) { + } +} diff --git a/web/src/main/java/org/springframework/security/web/jackson2/DefaultSavedRequestMixin.java b/web/src/main/java/org/springframework/security/web/jackson2/DefaultSavedRequestMixin.java new file mode 100644 index 00000000000..b896882e202 --- /dev/null +++ b/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}. + *

+ *

+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new WebJackson2Module());
+ * 
+ * + * @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 { +} diff --git a/web/src/main/java/org/springframework/security/web/jackson2/SavedCookieMixin.java b/web/src/main/java/org/springframework/security/web/jackson2/SavedCookieMixin.java new file mode 100644 index 00000000000..a048cb82f2e --- /dev/null +++ b/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. + * + *
+ * 		ObjectMapper mapper = new ObjectMapper();
+ *		mapper.registerModule(new WebJackson2Module());
+ * 
+ * + * @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) { + + } +} diff --git a/web/src/main/java/org/springframework/security/web/jackson2/WebAuthenticationDetailsMixin.java b/web/src/main/java/org/springframework/security/web/jackson2/WebAuthenticationDetailsMixin.java new file mode 100644 index 00000000000..c07918aeab7 --- /dev/null +++ b/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}. + * + *
+ * 	ObjectMapper mapper = new ObjectMapper();
+ *	mapper.registerModule(new WebJackson2Module());
+ * 
+ * + * @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) { + } +} diff --git a/web/src/main/java/org/springframework/security/web/jackson2/WebJackson2Module.java b/web/src/main/java/org/springframework/security/web/jackson2/WebJackson2Module.java new file mode 100644 index 00000000000..197537e865f --- /dev/null +++ b/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. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new WebJackson2Module());
+ * 
+ * Note: use {@link SecurityJacksonModules#getModules()} to get list of all security modules. + * + * @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); + } +} diff --git a/web/src/main/java/org/springframework/security/web/jackson2/package-info.java b/web/src/main/java/org/springframework/security/web/jackson2/package-info.java new file mode 100644 index 00000000000..9bc066de22c --- /dev/null +++ b/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; \ No newline at end of file diff --git a/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java b/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java index 1ac42d7a455..5a5811c77af 100644 --- a/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java +++ b/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 DefaultSavedRequest(HttpServletRequest request, PortResolver portResolver 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 names = request.getHeaderNames(); @@ -110,27 +107,10 @@ public DefaultSavedRequest(HttpServletRequest request, PortResolver portResolver } // Locales - Enumeration locales = request.getLocales(); - - while (locales.hasMoreElements()) { - Locale locale = (Locale) locales.nextElement(); - this.addLocale(locale); - } + addLocales(request.getLocales()); // Parameters - Map 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 DefaultSavedRequest(HttpServletRequest request, PortResolver portResolver 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 @@ private void addHeader(String name, String value) { values.add(value); } + /** + * @since 4.2 + */ + private void addLocales(Enumeration 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 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 @@ private void addParameter(String name, String[] values) { *

* 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 @@ private boolean propertyEquals(String log, Object arg1, Object arg2) { } return true; - } - else { + } else { if (logger.isDebugEnabled()) { logger.debug(log + ": arg1=" + arg1 + "; arg2=" + arg2 + " (property not equals)"); @@ -355,4 +388,115 @@ private boolean propertyEquals(String log, Object arg1, Object arg2) { public String toString() { return "DefaultSavedRequest[" + getRedirectUrl() + "]"; } + + /** + * @since 4.2 + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonPOJOBuilder(withPrefix = "set") + public static class Builder { + + private List cookies = null; + private List locales = null; + private Map> headers = new TreeMap>(String.CASE_INSENSITIVE_ORDER); + private Map parameters = new TreeMap(); + 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 cookies) { + this.cookies = cookies; + return this; + } + + public Builder setLocales(List locales) { + this.locales = locales; + return this; + } + + public Builder setHeaders(Map> header) { + this.headers.putAll(header); + return this; + } + + public Builder setParameters(Map 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; + } + } } diff --git a/web/src/test/java/org/springframework/security/web/jackson2/AbstractMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson2/AbstractMixinTests.java new file mode 100644 index 00000000000..7523385a3c1 --- /dev/null +++ b/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; + } +} diff --git a/web/src/test/java/org/springframework/security/web/jackson2/CookieMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson2/CookieMixinTests.java new file mode 100644 index 00000000000..251f26e3f3d --- /dev/null +++ b/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); + } +} diff --git a/web/src/test/java/org/springframework/security/web/jackson2/DefaultCsrfTokenMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson2/DefaultCsrfTokenMixinTests.java new file mode 100644 index 00000000000..4944173bc52 --- /dev/null +++ b/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); + } +} diff --git a/web/src/test/java/org/springframework/security/web/jackson2/DefaultSavedRequestMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson2/DefaultSavedRequestMixinTests.java new file mode 100644 index 00000000000..724750f8d2b --- /dev/null +++ b/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"); + } +} diff --git a/web/src/test/java/org/springframework/security/web/jackson2/SavedCookieMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson2/SavedCookieMixinTests.java new file mode 100644 index 00000000000..f86c6e92676 --- /dev/null +++ b/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 savedCookies = new ArrayList(); + 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 savedCookies = (List)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(); + } +} diff --git a/web/src/test/java/org/springframework/security/web/jackson2/WebAuthenticationDetailsMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson2/WebAuthenticationDetailsMixinTests.java new file mode 100644 index 00000000000..9cc8d54b44b --- /dev/null +++ b/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"); + } +} diff --git a/web/web.gradle b/web/web.gradle index a525bfef8c7..4bd07fa22b7 100644 --- a/web/web.gradle +++ b/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" } \ No newline at end of file