+ implements
+ WebTestClientConfigurer, MockServerConfigurer {
+
+ @Override
+ public void beforeServerCreated(WebHttpHandlerBuilder builder) {
+ mockAuthentication(build()).beforeServerCreated(builder);
+ }
+
+ @Override
+ public void afterConfigureAdded(WebTestClient.MockServerSpec> serverSpec) {
+ mockAuthentication(build()).afterConfigureAdded(serverSpec);
+ }
+
+ @Override
+ public void afterConfigurerAdded(
+ WebTestClient.Builder builder,
+ @Nullable WebHttpHandlerBuilder httpHandlerBuilder,
+ @Nullable ClientHttpConnector connector) {
+ mockAuthentication(build()).afterConfigurerAdded(builder, httpHandlerBuilder, connector);
+ }
+ }
}
diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java
index ee13abffa4b..2ff1c704eaa 100644
--- a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java
+++ b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java
@@ -26,6 +26,7 @@
import java.util.Base64;
import java.util.Collection;
import java.util.List;
+import java.util.function.Consumer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -45,7 +46,10 @@
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.test.context.TestSecurityContextHolder;
+import org.springframework.security.test.support.JwtAuthenticationTokenTestingBuilder;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.security.test.web.support.WebTestUtils;
import org.springframework.security.web.context.HttpRequestResponseHolder;
@@ -195,6 +199,37 @@ public static RequestPostProcessor user(UserDetails user) {
return new UserDetailsRequestPostProcessor(user);
}
+ /**
+ * Establish a {@link SecurityContext} that has a
+ * {@link JwtAuthenticationToken} for the
+ * {@link Authentication} and a {@link Jwt} for the
+ * {@link Authentication#getPrincipal()}. All details are
+ * declarative and do not require the JWT to be valid.
+ *
+ *
+ * The support works by associating the authentication to the HttpServletRequest. To associate
+ * the request to the SecurityContextHolder you need to ensure that the
+ * SecurityContextPersistenceFilter is associated with the MockMvc instance. A few
+ * ways to do this are:
+ *
+ *
+ *
+ * - Invoking apply {@link SecurityMockMvcConfigurers#springSecurity()}
+ * - Adding Spring Security's FilterChainProxy to MockMvc
+ * - Manually adding {@link SecurityContextPersistenceFilter} to the MockMvc
+ * instance may make sense when using MockMvcBuilders standaloneSetup
+ *
+ *
+ * @return the {@link JwtRequestPostProcessor} for additional customization
+ */
+ public static JwtRequestPostProcessor jwt() {
+ return new JwtRequestPostProcessor();
+ }
+
+ public static JwtRequestPostProcessor jwt(Consumer> jwt) {
+ return jwt().token(jwt);
+ }
+
/**
* Establish a {@link SecurityContext} that uses the specified {@link Authentication}
* for the {@link Authentication#getPrincipal()} and a custom {@link UserDetails}. All
@@ -555,7 +590,7 @@ private static String md5Hex(String a2) {
* Support class for {@link RequestPostProcessor}'s that establish a Spring Security
* context
*/
- private static abstract class SecurityContextRequestPostProcessorSupport {
+ static class SecurityContextRequestPostProcessorSupport {
/**
* Saves the specified {@link Authentication} into an empty
@@ -564,7 +599,7 @@ private static abstract class SecurityContextRequestPostProcessorSupport {
* @param authentication the {@link Authentication} to save
* @param request the {@link HttpServletRequest} to use
*/
- final void save(Authentication authentication, HttpServletRequest request) {
+ static final void save(Authentication authentication, HttpServletRequest request) {
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
save(securityContext, request);
@@ -576,7 +611,7 @@ final void save(Authentication authentication, HttpServletRequest request) {
* @param securityContext the {@link SecurityContext} to save
* @param request the {@link HttpServletRequest} to use
*/
- final void save(SecurityContext securityContext, HttpServletRequest request) {
+ static final void save(SecurityContext securityContext, HttpServletRequest request) {
SecurityContextRepository securityContextRepository = WebTestUtils
.getSecurityContextRepository(request);
boolean isTestRepository = securityContextRepository instanceof TestSecurityContextRepository;
@@ -604,7 +639,7 @@ final void save(SecurityContext securityContext, HttpServletRequest request) {
* stateless mode
*/
static class TestSecurityContextRepository implements SecurityContextRepository {
- private final static String ATTR_NAME = TestSecurityContextRepository.class
+ final static String ATTR_NAME = TestSecurityContextRepository.class
.getName().concat(".REPO");
private final SecurityContextRepository delegate;
@@ -716,8 +751,6 @@ private AuthenticationRequestPostProcessor(Authentication authentication) {
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
- SecurityContext context = SecurityContextHolder.createEmptyContext();
- context.setAuthentication(this.authentication);
save(this.authentication, request);
return request;
}
@@ -907,4 +940,20 @@ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request)
private SecurityMockMvcRequestPostProcessors() {
}
+
+ /**
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ * @since 5.2
+ */
+ public static class JwtRequestPostProcessor extends JwtAuthenticationTokenTestingBuilder
+ implements
+ RequestPostProcessor {
+
+ @Override
+ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
+ SecurityContextRequestPostProcessorSupport.save(build(), request);
+ return request;
+ }
+
+ }
}
diff --git a/test/src/test/java/org/springframework/security/test/support/JwtAuthenticationTokenTestingBuilderTests.java b/test/src/test/java/org/springframework/security/test/support/JwtAuthenticationTokenTestingBuilderTests.java
new file mode 100644
index 00000000000..e153f91491c
--- /dev/null
+++ b/test/src/test/java/org/springframework/security/test/support/JwtAuthenticationTokenTestingBuilderTests.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.test.support;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Test;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtClaimNames;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
+
+/**
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ * @since 5.2
+ */
+public class JwtAuthenticationTokenTestingBuilderTests {
+
+ @Test
+ public void untouchedBuilderSetsDefaultValues() {
+ final JwtAuthenticationToken actual = new JwtAuthenticationTokenTestingBuilder<>().build();
+
+ assertThat(actual.getName()).isEqualTo("user");
+ assertThat(actual.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SCOPE_USER"));
+ assertThat(actual.getPrincipal()).isInstanceOf(Jwt.class);
+ assertThat(actual.getCredentials()).isInstanceOf(Jwt.class);
+ assertThat(actual.getDetails()).isNull();
+
+ // Token default values are tested in JwtTestingBuilderTests
+ assertThat(actual.getToken()).isEqualTo(new JwtAuthenticationTokenTestingBuilder.JwtTestingBuilder().build());
+ }
+
+ @Test
+ public void nameOverridesDefaultValue() {
+ assertThat(new JwtAuthenticationTokenTestingBuilder<>().name("ch4mpy").build().getName()).isEqualTo("ch4mpy");
+ }
+
+ @Test
+ public void authoritiesAddsToDefaultValue() {
+ assertThat(new JwtAuthenticationTokenTestingBuilder<>().authorities("TEST").build().getAuthorities())
+ .containsExactlyInAnyOrder(new SimpleGrantedAuthority("SCOPE_USER"), new SimpleGrantedAuthority("TEST"));
+ }
+
+ @Test
+ public void scopesOveridesDefaultValue() {
+ assertThat(new JwtAuthenticationTokenTestingBuilder<>().scopes("TEST").build().getAuthorities())
+ .containsExactly(new SimpleGrantedAuthority("SCOPE_TEST"));
+ }
+
+ @Test
+ public void nameSetsAuthenticationNameAndTokenSubjectClaim() {
+ final JwtAuthenticationToken actual = new JwtAuthenticationTokenTestingBuilder<>().name("ch4mpy").build();
+
+ assertThat(actual.getName()).isEqualTo("ch4mpy");
+ assertThat(actual.getTokenAttributes().get(JwtClaimNames.SUB)).isEqualTo("ch4mpy");
+ }
+
+ @Test
+ public void buildMergesConvertedClaimsAndAuthorities() {
+ final JwtAuthenticationToken actual = new JwtAuthenticationTokenTestingBuilder<>().name("ch4mpy")
+ .authorities(new SimpleGrantedAuthority("TEST_AUTHORITY"))
+ .scopes("scope:claim")
+ .build();
+
+ assertThat(actual.getAuthorities()).containsExactlyInAnyOrder(
+ new SimpleGrantedAuthority("TEST_AUTHORITY"),
+ new SimpleGrantedAuthority("SCOPE_scope:claim"));
+ }
+
+}
diff --git a/test/src/test/java/org/springframework/security/test/support/JwtTestingBuilderTests.java b/test/src/test/java/org/springframework/security/test/support/JwtTestingBuilderTests.java
new file mode 100644
index 00000000000..75923fac639
--- /dev/null
+++ b/test/src/test/java/org/springframework/security/test/support/JwtTestingBuilderTests.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.test.support;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Instant;
+
+import org.junit.Test;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtClaimNames;
+import org.springframework.security.test.support.JwtAuthenticationTokenTestingBuilder.JwtTestingBuilder;
+
+/**
+ *
+ *
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ */
+public class JwtTestingBuilderTests {
+
+ @Test
+ public void testDefaultValuesAreSet() {
+ final Jwt actual = new JwtTestingBuilder().build();
+
+ assertThat(actual.getTokenValue()).isEqualTo("test.jwt.value");
+ assertThat(actual.getClaimAsString(JwtClaimNames.SUB)).isEqualTo("user");
+ assertThat(actual.getHeaders()).hasSize(1);
+ }
+
+ @Test
+ public void iatClaimAndExpClaimSetIssuedAtAndExpiresAt() {
+ final Jwt actual = new JwtTestingBuilder()
+ .claim(JwtClaimNames.IAT, Instant.parse("2019-03-21T13:52:25Z"))
+ .claim(JwtClaimNames.EXP, Instant.parse("2019-03-22T13:52:25Z"))
+ .build();
+
+ assertThat(actual.getIssuedAt()).isEqualTo(Instant.parse("2019-03-21T13:52:25Z"));
+ assertThat(actual.getExpiresAt()).isEqualTo(Instant.parse("2019-03-22T13:52:25Z"));
+ assertThat(actual.getClaimAsInstant(JwtClaimNames.IAT)).isEqualTo(Instant.parse("2019-03-21T13:52:25Z"));
+ assertThat(actual.getClaimAsInstant(JwtClaimNames.EXP)).isEqualTo(Instant.parse("2019-03-22T13:52:25Z"));
+ }
+
+}
diff --git a/test/src/test/java/org/springframework/security/test/web/reactive/server/JwtMutatorTests.java b/test/src/test/java/org/springframework/security/test/web/reactive/server/JwtMutatorTests.java
new file mode 100644
index 00000000000..fb1ee4bd669
--- /dev/null
+++ b/test/src/test/java/org/springframework/security/test/web/reactive/server/JwtMutatorTests.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.test.web.reactive.server;
+
+import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockJwt;
+
+import org.junit.Test;
+
+/**
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ * @since 5.2
+ */
+public class JwtMutatorTests {
+// @formatter:off
+ @Test
+ public void defaultJwtConfigurerConfiguresAuthenticationDefaultNameAndAuthorities() {
+ TestController.clientBuilder()
+ .apply(mockJwt()).build()
+ .get().uri("/greet").exchange()
+ .expectStatus().isOk()
+ .expectBody().toString().equals("Hello user!");
+
+ TestController.clientBuilder()
+ .apply(mockJwt()).build()
+ .get().uri("/authorities").exchange()
+ .expectStatus().isOk()
+ .expectBody().toString().equals("[\"ROLE_USER\"]");
+ }
+
+ @Test
+ public void nameAndScopesConfigureAuthenticationNameAndAuthorities() {
+ TestController.clientBuilder()
+ .apply(mockJwt().name("ch4mpy").scopes("message:read")).build()
+ .get().uri("/greet").exchange()
+ .expectStatus().isOk()
+ .expectBody().toString().equals("Hello ch4mpy!");
+
+ TestController.clientBuilder()
+ .apply(mockJwt().name("ch4mpy").scopes("message:read")).build()
+ .get().uri("/authorities").exchange()
+ .expectStatus().isOk()
+ .expectBody().toString().equals("[\"SCOPE_message:read\"]");
+
+ TestController.clientBuilder()
+ .apply(mockJwt().name("ch4mpy").scopes("message:read")).build()
+ .get().uri("/jwt").exchange()
+ .expectStatus().isOk()
+ .expectBody().toString().equals(
+ "Hello,ch4mpy! You are sucessfully authenticated and granted with [message:read] scopes using a JavaWebToken.");
+ }
+// @formatter:on
+}
diff --git a/test/src/test/java/org/springframework/security/test/web/reactive/server/TestController.java b/test/src/test/java/org/springframework/security/test/web/reactive/server/TestController.java
new file mode 100644
index 00000000000..449fd6d37ea
--- /dev/null
+++ b/test/src/test/java/org/springframework/security/test/web/reactive/server/TestController.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.test.web.reactive.server;
+
+import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
+
+import java.security.Principal;
+import java.util.stream.Collectors;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter;
+import org.springframework.security.web.server.csrf.CsrfWebFilter;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ * @since 5.2
+ */
+@RestController
+public class TestController {
+
+ @GetMapping("/greet")
+ public String greet(final Principal authentication) {
+ return String.format("Hello, %s!", authentication.getName());
+ }
+
+ @GetMapping("/authorities")
+ public String authentication(final Authentication authentication) {
+ return authentication.getAuthorities()
+ .stream()
+ .map(GrantedAuthority::getAuthority)
+ .collect(Collectors.toList())
+ .toString();
+ }
+
+ @GetMapping("/jwt")
+ // TODO: investigate why "@AuthenticationPrincipal Jwt token" does not work here
+ public String jwt(final Authentication authentication) {
+ final Jwt token = (Jwt) authentication.getPrincipal();
+ final String scopes = token.getClaimAsString("scope");
+
+ return String.format(
+ "Hello, %s! You are sucessfully authenticated and granted with %s scopes using a Jwt.",
+ token.getSubject(),
+ scopes);
+ }
+
+ public static WebTestClient.Builder clientBuilder() {
+ return WebTestClient.bindToController(new TestController())
+ .webFilter(new CsrfWebFilter(), new SecurityContextServerWebExchangeWebFilter())
+ .apply(springSecurity())
+ .configureClient()
+ .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
+ }
+
+ public static WebTestClient client() {
+ return (WebTestClient) clientBuilder().build();
+ }
+}
diff --git a/test/src/test/java/org/springframework/security/test/web/servlet/request/JwtRequestPostProcessorTests.java b/test/src/test/java/org/springframework/security/test/web/servlet/request/JwtRequestPostProcessorTests.java
new file mode 100644
index 00000000000..fcf40a91224
--- /dev/null
+++ b/test/src/test/java/org/springframework/security/test/web/servlet/request/JwtRequestPostProcessorTests.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.test.web.servlet.request;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.JwtRequestPostProcessor;
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.SecurityContextRequestPostProcessorSupport.TestSecurityContextRepository;
+
+/**
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ * @since 5.2
+ */
+public class JwtRequestPostProcessorTests {
+ @Mock
+ MockHttpServletRequest request;
+
+ final static String TEST_NAME = "ch4mpy";
+ final static String[] TEST_AUTHORITIES = { "TEST_AUTHORITY" };
+
+ @Before
+ public void setup() throws Exception {
+ request = new MockHttpServletRequest();
+ }
+
+ @Test
+ public void nameAndAuthoritiesAndClaimsConfigureSecurityContextAuthentication() {
+ final JwtRequestPostProcessor rpp =
+ jwt().name(TEST_NAME).authorities(TEST_AUTHORITIES).scopes("test:claim");
+
+ final JwtAuthenticationToken actual = (JwtAuthenticationToken) authentication(rpp.postProcessRequest(request));
+
+ assertThat(actual.getName()).isEqualTo(TEST_NAME);
+ assertThat(actual.getAuthorities()).containsExactlyInAnyOrder(
+ new SimpleGrantedAuthority("TEST_AUTHORITY"),
+ new SimpleGrantedAuthority("SCOPE_test:claim"));
+ assertThat(actual.getTokenAttributes().get("scope")).isEqualTo("test:claim");
+ }
+
+ static Authentication authentication(final MockHttpServletRequest req) {
+ final SecurityContext securityContext = (SecurityContext) req.getAttribute(TestSecurityContextRepository.ATTR_NAME);
+ return securityContext == null ? null : securityContext.getAuthentication();
+ }
+
+}