Prechádzať zdrojové kódy

Mock Jwt Test Support and Jwt.Builder

Fixes: gh-6634
Fixes: gh-6851
Jérôme Wacongne 6 rokov pred
rodič
commit
e59d8a529b

+ 146 - 3
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/Jwt.java

@@ -15,13 +15,19 @@
  */
 package org.springframework.security.oauth2.jwt;
 
-import org.springframework.security.oauth2.core.AbstractOAuth2Token;
-import org.springframework.util.Assert;
-
+import java.net.URL;
 import java.time.Instant;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.security.oauth2.core.AbstractOAuth2Token;
+import org.springframework.util.Assert;
 
 /**
  * An implementation of an {@link AbstractOAuth2Token} representing a JSON Web Token (JWT).
@@ -41,6 +47,8 @@ import java.util.Map;
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516">JSON Web Encryption (JWE)</a>
  */
 public class Jwt extends AbstractOAuth2Token implements JwtClaimAccessor {
+	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+	
 	private final Map<String, Object> headers;
 	private final Map<String, Object> claims;
 
@@ -80,4 +88,139 @@ public class Jwt extends AbstractOAuth2Token implements JwtClaimAccessor {
 	public Map<String, Object> getClaims() {
 		return this.claims;
 	}
+	
+	public static Builder<?> builder() {
+		return new Builder<>();
+	}
+	
+	/**
+	 * Helps configure a {@link Jwt}
+	 *
+	 * @author Jérôme Wacongne &lt;ch4mp&#64;c4-soft.com&gt;
+	 */
+	public static class Builder<T extends Builder<T>> {
+		protected String tokenValue;
+		protected final Map<String, Object> claims = new HashMap<>();
+		protected final Map<String, Object> headers = new HashMap<>();
+		
+		protected Builder() {
+		}
+
+		public T tokenValue(String tokenValue) {
+			this.tokenValue = tokenValue;
+			return downcast();
+		}
+
+		public T claim(String name, Object value) {
+			this.claims.put(name, value);
+			return downcast();
+		}
+
+		public T clearClaims(Map<String, Object> claims) {
+			this.claims.clear();
+			return downcast();
+		}
+
+		/**
+		 * Adds to existing claims (does not replace existing ones)
+		 * @param claims claims to add
+		 * @return this builder to further configure
+		 */
+		public T claims(Map<String, Object> claims) {
+			this.claims.putAll(claims);
+			return downcast();
+		}
+
+		public T header(String name, Object value) {
+			this.headers.put(name, value);
+			return downcast();
+		}
+
+		public T clearHeaders(Map<String, Object> headers) {
+			this.headers.clear();
+			return downcast();
+		}
+
+		/**
+		 * Adds to existing headers (does not replace existing ones)
+		 * @param headers headers to add
+		 * @return this builder to further configure
+		 */
+		public T headers(Map<String, Object> headers) {
+			headers.entrySet().stream().forEach(e -> this.header(e.getKey(), e.getValue()));
+			return downcast();
+		}
+
+		public Jwt build() {
+			final JwtClaimSet claimSet = new JwtClaimSet(claims);
+			return new Jwt(
+					this.tokenValue,
+					claimSet.getClaimAsInstant(JwtClaimNames.IAT),
+					claimSet.getClaimAsInstant(JwtClaimNames.EXP),
+					this.headers,
+					claimSet);
+		}
+
+		public T audience(Stream<String> audience) {
+			this.claim(JwtClaimNames.AUD, audience.collect(Collectors.toList()));
+			return downcast();
+		}
+
+		public T audience(Collection<String> audience) {
+			return audience(audience.stream());
+		}
+
+		public T audience(String... audience) {
+			return audience(Stream.of(audience));
+		}
+
+		public T expiresAt(Instant expiresAt) {
+			this.claim(JwtClaimNames.EXP, expiresAt.getEpochSecond());
+			return downcast();
+		}
+
+		public T jti(String jti) {
+			this.claim(JwtClaimNames.JTI, jti);
+			return downcast();
+		}
+
+		public T issuedAt(Instant issuedAt) {
+			this.claim(JwtClaimNames.IAT, issuedAt.getEpochSecond());
+			return downcast();
+		}
+
+		public T issuer(URL issuer) {
+			this.claim(JwtClaimNames.ISS, issuer.toExternalForm());
+			return downcast();
+		}
+
+		public T notBefore(Instant notBefore) {
+			this.claim(JwtClaimNames.NBF, notBefore.getEpochSecond());
+			return downcast();
+		}
+
+		public T subject(String subject) {
+			this.claim(JwtClaimNames.SUB, subject);
+			return downcast();
+		}
+		
+		@SuppressWarnings("unchecked")
+		protected T downcast() {
+			return (T) this;
+		}
+	}
+
+	private static final class JwtClaimSet extends HashMap<String, Object> implements JwtClaimAccessor {
+		private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+
+		public JwtClaimSet(Map<String, Object> claims) {
+			super(claims);
+		}
+
+		@Override
+		public Map<String, Object> getClaims() {
+			return this;
+		}
+		
+	}
 }

+ 59 - 0
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtBuilderTests.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.jwt;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Test;
+
+/**
+ * Tests for {@link Jwt.Builder}.
+ */
+public class JwtBuilderTests {
+
+	@Test()
+	public void builderCanBeReused() {
+		final Jwt.Builder<?> tokensBuilder = Jwt.builder();
+		
+		final Jwt first = tokensBuilder
+				.tokenValue("V1")
+				.header("TEST_HEADER_1", "H1")
+				.claim("TEST_CLAIM_1", "C1")
+				.build();
+		
+		final Jwt second = tokensBuilder
+				.tokenValue("V2")
+				.header("TEST_HEADER_1", "H2")
+				.header("TEST_HEADER_2", "H3")
+				.claim("TEST_CLAIM_1", "C2")
+				.claim("TEST_CLAIM_2", "C3")
+				.build();
+
+		assertThat(first.getHeaders()).hasSize(1);
+		assertThat(first.getHeaders().get("TEST_HEADER_1")).isEqualTo("H1");
+		assertThat(first.getClaims()).hasSize(1);
+		assertThat(first.getClaims().get("TEST_CLAIM_1")).isEqualTo("C1");
+		assertThat(first.getTokenValue()).isEqualTo("V1");
+
+		assertThat(second.getHeaders()).hasSize(2);
+		assertThat(second.getHeaders().get("TEST_HEADER_1")).isEqualTo("H2");
+		assertThat(second.getHeaders().get("TEST_HEADER_2")).isEqualTo("H3");
+		assertThat(second.getClaims()).hasSize(2);
+		assertThat(second.getClaims().get("TEST_CLAIM_1")).isEqualTo("C2");
+		assertThat(second.getClaims().get("TEST_CLAIM_2")).isEqualTo("C3");
+		assertThat(second.getTokenValue()).isEqualTo("V2");
+	}
+}

+ 73 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java

@@ -17,7 +17,11 @@ package org.springframework.security.oauth2.server.resource.authentication;
 
 import java.util.Collection;
 import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
+import org.springframework.core.convert.converter.Converter;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.SpringSecurityCoreVersion;
 import org.springframework.security.core.Transient;
@@ -71,4 +75,73 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok
 	public String getName() {
 		return this.getToken().getSubject();
 	}
+	
+	public static Builder<?> builder(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
+		return new Builder<>(Jwt.builder(), authoritiesConverter);
+	}
+	
+	public static Builder<?> builder() {
+		return builder(new JwtGrantedAuthoritiesConverter());
+	}
+	
+	/**
+	 * Helps configure a {@link JwtAuthenticationToken}
+	 *
+	 * @author Jérôme Wacongne &lt;ch4mp&#64;c4-soft.com&gt;
+	 * @since 5.2
+	 */
+	public static class Builder<T extends Builder<T>> {
+
+		private Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter;
+
+		private final Jwt.Builder<?> jwt;
+
+		protected Builder(Jwt.Builder<?> principalBuilder, Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
+			this.authoritiesConverter = authoritiesConverter;
+			this.jwt = principalBuilder;
+		}
+
+		public T authoritiesConverter(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
+			this.authoritiesConverter = authoritiesConverter;
+			return downcast();
+		}
+
+		public T token(Consumer<Jwt.Builder<?>> jwtBuilderConsumer) {
+			jwtBuilderConsumer.accept(jwt);
+			return downcast();
+		}
+
+		public T name(String name) {
+			jwt.subject(name);
+			return downcast();
+		}
+
+		/**
+		 * Shortcut to set "scope" claim with a space separated string containing provided scope collection
+		 * @param scopes strings to join with spaces and set as "scope" claim
+		 * @return this builder to further configure
+		 */
+		public T scopes(String... scopes) {
+			jwt.claim("scope", Stream.of(scopes).collect(Collectors.joining(" ")));
+			return downcast();
+		}
+
+		public JwtAuthenticationToken build() {
+			final Jwt token = jwt.build();
+			return new JwtAuthenticationToken(token, getAuthorities(token));
+		}
+		
+		protected Jwt getToken() {
+			return jwt.build();
+		}
+		
+		protected Collection<GrantedAuthority> getAuthorities(Jwt token) {
+			return authoritiesConverter.convert(token);
+		}
+
+		@SuppressWarnings("unchecked")
+		protected T downcast() {
+			return (T) this;
+		}
+	}
 }

+ 71 - 0
samples/boot/oauth2resourceserver/src/test/java/sample/OAuth2ResourceServerControllerTests.java

@@ -0,0 +1,71 @@
+/*
+ * 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 sample;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+
+/**
+ *
+ * @author Jérôme Wacongne &lt;ch4mp@c4-soft.com&gt;
+ * @since 5.2.0
+ *
+ */
+@RunWith(SpringRunner.class)
+@WebMvcTest(OAuth2ResourceServerController.class)
+public class OAuth2ResourceServerControllerTests {
+
+	@Autowired
+	MockMvc mockMvc;
+
+	@MockBean
+	JwtDecoder jwtDecoder;
+
+	@Test
+	public void indexGreetsAuthenticatedUser() throws Exception {
+		mockMvc.perform(get("/").with(jwt().name("ch4mpy")))
+				.andExpect(content().string(is("Hello, ch4mpy!")));
+	}
+	
+	@Test
+	public void messageCanBeReadWithScopeMessageReadAuthority() throws Exception {
+		mockMvc.perform(get("/message").with(jwt().scopes("message:read")))
+				.andExpect(content().string(is("secret message")));
+		
+		mockMvc.perform(get("/message").with(jwt().authorities(new SimpleGrantedAuthority(("SCOPE_message:read")))))
+			.andExpect(content().string(is("secret message")));
+	}
+	
+	@Test
+	public void messageCanNotBeReadWithoutScopeMessageReadAuthority() throws Exception {
+		mockMvc.perform(get("/message").with(jwt()))
+				.andExpect(status().isForbidden());
+	}
+
+}

+ 2 - 0
test/spring-security-test.gradle

@@ -7,6 +7,8 @@ dependencies {
 	compile 'org.springframework:spring-test'
 
 	optional project(':spring-security-config')
+	optional project(':spring-security-oauth2-resource-server')
+	optional project(':spring-security-oauth2-jose')
 	optional 'io.projectreactor:reactor-core'
 	optional 'org.springframework:spring-webflux'
 

+ 140 - 0
test/src/main/java/org/springframework/security/test/support/JwtAuthenticationTokenTestingBuilder.java

@@ -0,0 +1,140 @@
+/*
+ * 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 java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.core.GrantedAuthority;
+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;
+import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Jérôme Wacongne &lt;ch4mp&#64;c4-soft.com&gt;
+ * @since 5.2
+ */
+public class JwtAuthenticationTokenTestingBuilder<T extends JwtAuthenticationTokenTestingBuilder<T>>
+		extends
+		JwtAuthenticationToken.Builder<T> {
+
+	private static final String[] DEFAULT_SCOPES = { "USER" };
+
+	private final Set<GrantedAuthority> addedAuthorities;
+
+	public JwtAuthenticationTokenTestingBuilder(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
+		super(new JwtTestingBuilder(), authoritiesConverter);
+		this.addedAuthorities = new HashSet<>();
+		scopes(DEFAULT_SCOPES);
+	}
+
+	public JwtAuthenticationTokenTestingBuilder() {
+		this(new JwtGrantedAuthoritiesConverter());
+	}
+
+	/**
+	 * How to extract authorities from token
+	 * @param authoritiesConverter JWT to granted-authorities converter
+	 * @return this builder to further configure
+	 */
+	public T authorities(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
+		return authoritiesConverter(authoritiesConverter);
+	}
+
+	/**
+	 * Adds authorities to what is extracted from the token.<br>
+	 * Please consider using {@link #authorities(Converter)} instead.
+	 * @param authorities authorities to add to token ones
+	 * @return this builder to further configure
+	 */
+	public T authorities(Stream<GrantedAuthority> authorities) {
+		addedAuthorities.addAll(authorities.collect(Collectors.toSet()));
+		return downcast();
+	}
+
+	/**
+	 * Adds authorities to what is extracted from the token.<br>
+	 * Please consider using {@link #authorities(Converter)} instead.
+	 * @param authorities authorities to add to token ones
+	 * @return this builder to further configure
+	 */
+	public T authorities(GrantedAuthority... authorities) {
+		return authorities(Stream.of(authorities));
+	}
+
+	/**
+	 * Adds authorities to what is extracted from the token.<br>
+	 * Please consider using {@link #authorities(Converter)} instead.
+	 * @param authorities authorities to add to token ones
+	 * @return this builder to further configure
+	 */
+	public T authorities(String... authorities) {
+		return authorities(Stream.of(authorities).map(SimpleGrantedAuthority::new));
+	}
+
+	@Override
+	public JwtAuthenticationToken build() {
+		final Jwt token = getToken();
+
+		return new JwtAuthenticationToken(token, getAuthorities(token));
+	}
+
+	@Override
+	protected Collection<GrantedAuthority> getAuthorities(Jwt token) {
+		final Collection<GrantedAuthority> principalAuthorities = super.getAuthorities(token);
+
+		return addedAuthorities.isEmpty() ? principalAuthorities
+				: Stream.concat(principalAuthorities.stream(), addedAuthorities.stream()).collect(Collectors.toSet());
+	}
+
+	/**
+	 * @author Jérôme Wacongne &lt;ch4mp&#64;c4-soft.com&gt;
+	 * @since 5.2
+	 */
+	static class JwtTestingBuilder extends Jwt.Builder<JwtTestingBuilder> {
+
+		private static final String DEFAULT_SUBJECT = "user";
+
+		private static final String DEFAULT_TOKEN_VALUE = "test.jwt.value";
+
+		private static final String DEFAULT_HEADER_NAME = "test-header";
+
+		private static final String DEFAULT_HEADER_VALUE = "test-header-value";
+
+		public JwtTestingBuilder() {
+			super();
+		}
+
+		@Override
+		public Jwt build() {
+			final Object subjectClaim = claims.get(JwtClaimNames.SUB);
+			if (!StringUtils.hasLength(tokenValue)) {
+				tokenValue(DEFAULT_TOKEN_VALUE);
+			}
+			if (!StringUtils.hasLength((String) subjectClaim)) {
+				claim(JwtClaimNames.SUB, DEFAULT_SUBJECT);
+			}
+			if (headers.size() == 0) {
+				header(DEFAULT_HEADER_NAME, DEFAULT_HEADER_VALUE);
+			}
+			return super.build();
+		}
+	}
+}

+ 53 - 5
test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java

@@ -16,6 +16,11 @@
 
 package org.springframework.security.test.web.reactive.server;
 
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
 import org.springframework.http.client.reactive.ClientHttpConnector;
 import org.springframework.lang.Nullable;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -26,6 +31,9 @@ import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextImpl;
 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.support.JwtAuthenticationTokenTestingBuilder;
 import org.springframework.security.web.server.csrf.CsrfWebFilter;
 import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
 import org.springframework.test.web.reactive.server.MockServerConfigurer;
@@ -35,12 +43,8 @@ import org.springframework.web.server.ServerWebExchange;
 import org.springframework.web.server.WebFilter;
 import org.springframework.web.server.WebFilterChain;
 import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
-import reactor.core.publisher.Mono;
 
-import java.util.Collection;
-import java.util.List;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
+import reactor.core.publisher.Mono;
 
 /**
  * Test utilities for working with Spring Security and
@@ -109,6 +113,23 @@ public class SecurityMockServerConfigurers {
 		return new UserExchangeMutator(username);
 	}
 
+	/**
+	 * Updates the ServerWebExchange to 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.
+	 *
+	 * @return the {@link JwtMutator} to further configure or use
+	 */
+	public static JwtMutator mockJwt() {
+		return new JwtMutator();
+	}
+	
+	public static JwtMutator mockJwt(Consumer<Jwt.Builder<?>> jwt) {
+		return new JwtMutator().token(jwt);
+	}
+
 	public static CsrfMutator csrf() {
 		return new CsrfMutator();
 	}
@@ -294,4 +315,31 @@ public class SecurityMockServerConfigurers {
 			return webFilterChain.filter(exchange);
 		}
 	}
+	
+	/**
+	 * @author Jérôme Wacongne &lt;ch4mp&#64;c4-soft.com&gt;
+	 * @since 5.2
+	 */
+	public static class JwtMutator extends JwtAuthenticationTokenTestingBuilder<JwtMutator>
+			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);
+		}
+	}
 }

+ 55 - 6
test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java

@@ -26,6 +26,7 @@ import java.util.Arrays;
 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.SecurityContext;
 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 final class SecurityMockMvcRequestPostProcessors {
 		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.
+	 *
+	 * <p>
+	 * 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:
+	 * </p>
+	 *
+	 * <ul>
+	 * <li>Invoking apply {@link SecurityMockMvcConfigurers#springSecurity()}</li>
+	 * <li>Adding Spring Security's FilterChainProxy to MockMvc</li>
+	 * <li>Manually adding {@link SecurityContextPersistenceFilter} to the MockMvc
+	 * instance may make sense when using MockMvcBuilders standaloneSetup</li>
+	 * </ul>
+	 *
+	 * @return the {@link JwtRequestPostProcessor} for additional customization
+	 */
+	public static JwtRequestPostProcessor jwt() {
+		return new JwtRequestPostProcessor();
+	}
+	
+	public static JwtRequestPostProcessor jwt(Consumer<Jwt.Builder<?>> 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 @@ public final class SecurityMockMvcRequestPostProcessors {
 	 * 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 @@ public final class SecurityMockMvcRequestPostProcessors {
 		 * @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 @@ public final class SecurityMockMvcRequestPostProcessors {
 		 * @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 @@ public final class SecurityMockMvcRequestPostProcessors {
 		 * 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 @@ public final class SecurityMockMvcRequestPostProcessors {
 
 		@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 final class SecurityMockMvcRequestPostProcessors {
 
 	private SecurityMockMvcRequestPostProcessors() {
 	}
+	
+	/**
+	 * @author Jérôme Wacongne &lt;ch4mp&#64;c4-soft.com&gt;
+	 * @since 5.2
+	 */
+	public static class JwtRequestPostProcessor extends JwtAuthenticationTokenTestingBuilder<JwtRequestPostProcessor>
+			implements
+			RequestPostProcessor {
+
+		@Override
+		public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
+			SecurityContextRequestPostProcessorSupport.save(build(), request);
+			return request;
+		}
+
+	}
 }

+ 83 - 0
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 &lt;ch4mp&#64;c4-soft.com&gt;
+ * @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"));
+	}
+
+}

+ 56 - 0
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 &lt;ch4mp&#64;c4-soft.com&gt;
+ */
+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"));
+	}
+
+}

+ 65 - 0
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 &lt;ch4mp&#64;c4-soft.com&gt;
+ * @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
+}

+ 78 - 0
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 &lt;ch4mp&#64;c4-soft.com&gt;
+ * @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();
+	}
+}

+ 67 - 0
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 &lt;ch4mp&#64;c4-soft.com&gt;
+ * @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();
+	}
+
+}