diff --git a/web/src/main/java/org/springframework/security/web/jackson2/PreAuthenticatedAuthenticationTokenDeserializer.java b/web/src/main/java/org/springframework/security/web/jackson2/PreAuthenticatedAuthenticationTokenDeserializer.java new file mode 100644 index 0000000000..48f1914436 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/jackson2/PreAuthenticatedAuthenticationTokenDeserializer.java @@ -0,0 +1,86 @@ +/* + * 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 java.util.List; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +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; + +/** + * Custom deserializer for {@link PreAuthenticatedAuthenticationToken}. 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 PreAuthenticatedAuthenticationTokenMixin} but + * you can also registered it with your own mixin class. + * + * @author Jitendra Singh + * @see PreAuthenticatedAuthenticationTokenMixin + * @since 4.2 + */ +class PreAuthenticatedAuthenticationTokenDeserializer extends JsonDeserializer { + + /** + * This method construct {@link PreAuthenticatedAuthenticationToken} object from serialized json. + * @param jp the JsonParser + * @param ctxt the DeserializationContext + * @return the user + * @throws IOException if a exception during IO occurs + * @throws JsonProcessingException if an error during JSON processing occurs + */ + @Override + public PreAuthenticatedAuthenticationToken deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + PreAuthenticatedAuthenticationToken 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 PreAuthenticatedAuthenticationToken(principal, credentials, authorities); + } else { + token = new PreAuthenticatedAuthenticationToken(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/web/src/main/java/org/springframework/security/web/jackson2/PreAuthenticatedAuthenticationTokenMixin.java b/web/src/main/java/org/springframework/security/web/jackson2/PreAuthenticatedAuthenticationTokenMixin.java new file mode 100644 index 0000000000..6a339f39db --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/jackson2/PreAuthenticatedAuthenticationTokenMixin.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.web.jackson2; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.jackson2.SimpleGrantedAuthorityMixin; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +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 PreAuthenticatedAuthenticationTokenDeserializer}. + * + * 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 Webackson2Module + * @see SecurityJackson2Modules + * @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 = PreAuthenticatedAuthenticationTokenDeserializer.class) +abstract class PreAuthenticatedAuthenticationTokenMixin { +} 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 index d9408be734..de9adaf207 100644 --- a/web/src/main/java/org/springframework/security/web/jackson2/WebJackson2Module.java +++ b/web/src/main/java/org/springframework/security/web/jackson2/WebJackson2Module.java @@ -20,6 +20,7 @@ import javax.servlet.http.Cookie; import org.springframework.security.jackson2.SecurityJackson2Modules; import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; import org.springframework.security.web.csrf.DefaultCsrfToken; import org.springframework.security.web.savedrequest.DefaultSavedRequest; import org.springframework.security.web.savedrequest.SavedCookie; @@ -58,5 +59,6 @@ public class WebJackson2Module extends SimpleModule { context.setMixInAnnotations(DefaultCsrfToken.class, DefaultCsrfTokenMixin.class); context.setMixInAnnotations(DefaultSavedRequest.class, DefaultSavedRequestMixin.class); context.setMixInAnnotations(WebAuthenticationDetails.class, WebAuthenticationDetailsMixin.class); + context.setMixInAnnotations(PreAuthenticatedAuthenticationToken.class, PreAuthenticatedAuthenticationTokenMixin.class); } } diff --git a/web/src/test/java/org/springframework/security/web/jackson2/PreAuthenticatedAuthenticationTokenMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson2/PreAuthenticatedAuthenticationTokenMixinTests.java new file mode 100644 index 0000000000..a82b7d2025 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/jackson2/PreAuthenticatedAuthenticationTokenMixinTests.java @@ -0,0 +1,68 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; + +import org.json.JSONException; +import org.junit.Before; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.jackson2.SimpleGrantedAuthorityMixinTests; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +import com.fasterxml.jackson.core.JsonProcessingException; + +/** + * @author Rob Winch + * @since 4.2 + */ +public class PreAuthenticatedAuthenticationTokenMixinTests extends AbstractMixinTests { + // @formatter:off + private static final String PREAUTH_JSON = "{" + + "\"@class\": \"org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken\"," + + "\"principal\": \"principal\", " + + "\"credentials\": \"credentials\", " + + "\"authenticated\": true, " + + "\"details\": null, " + + "\"authorities\": "+ SimpleGrantedAuthorityMixinTests.AUTHORITIES_ARRAYLIST_JSON + + "}"; + // @formatter:on + + PreAuthenticatedAuthenticationToken expected; + + @Before + public void setupExpected() { + expected = new PreAuthenticatedAuthenticationToken("principal", "credentials", AuthorityUtils.createAuthorityList("ROLE_USER")); + } + + @Test + public void serializeWhenPrincipalCredentialsAuthoritiesThenSuccess() throws JsonProcessingException, JSONException { + String serializedJson = mapper.writeValueAsString(expected); + JSONAssert.assertEquals(PREAUTH_JSON, serializedJson, true); + } + + @Test + public void deserializeAuthenticatedUsernamePasswordAuthenticationTokenMixinTest() throws Exception{ + PreAuthenticatedAuthenticationToken deserialized = mapper + .readValue(PREAUTH_JSON, PreAuthenticatedAuthenticationToken.class); + assertThat(deserialized).isNotNull(); + assertThat(deserialized.isAuthenticated()).isTrue(); + assertThat(deserialized.getAuthorities()).isEqualTo(expected.getAuthorities()); + } +}