From d0f5b428847c1050d38da7d2af98eb2cda6a9b9e Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 21 May 2019 17:59:55 -0600 Subject: [PATCH] Mock Jwt Test Support and Jwt.Builder Polish Simplified the initial support to introduce fewer classes and only the features described in the ticket. Changed tests to align with existing patterns in the repository. Added JavaDoc to remaining public methods introduced for this feature. Issue: gh-6634 Issue: gh-6851 --- .../security/oauth2/jwt/Jwt.java | 300 ++++++++++-------- .../security/oauth2/jwt/JwtBuilderTests.java | 143 ++++++++- .../JwtAuthenticationToken.java | 73 ----- .../OAuth2ResourceServerControllerTests.java | 25 +- test/spring-security-test.gradle | 2 +- .../JwtAuthenticationTokenTestingBuilder.java | 140 -------- .../server/SecurityMockServerConfigurers.java | 99 +++++- .../SecurityMockMvcRequestPostProcessors.java | 114 +++++-- ...uthenticationTokenTestingBuilderTests.java | 83 ----- .../test/support/JwtTestingBuilderTests.java | 56 ---- .../AbstractMockServerConfigurersTests.java | 25 +- .../web/reactive/server/JwtMutatorTests.java | 65 ---- ...SecurityMockServerConfigurersJwtTests.java | 139 ++++++++ .../web/reactive/server/TestController.java | 78 ----- .../request/JwtRequestPostProcessorTests.java | 67 ---- ...yMockMvcRequestPostProcessorsJwtTests.java | 157 +++++++++ 16 files changed, 819 insertions(+), 747 deletions(-) delete mode 100644 test/src/main/java/org/springframework/security/test/support/JwtAuthenticationTokenTestingBuilder.java delete mode 100644 test/src/test/java/org/springframework/security/test/support/JwtAuthenticationTokenTestingBuilderTests.java delete mode 100644 test/src/test/java/org/springframework/security/test/support/JwtTestingBuilderTests.java delete mode 100644 test/src/test/java/org/springframework/security/test/web/reactive/server/JwtMutatorTests.java create mode 100644 test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurersJwtTests.java delete mode 100644 test/src/test/java/org/springframework/security/test/web/reactive/server/TestController.java delete mode 100644 test/src/test/java/org/springframework/security/test/web/servlet/request/JwtRequestPostProcessorTests.java create mode 100644 test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessorsJwtTests.java diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/Jwt.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/Jwt.java index 9cccb7b086..8a6a5454a8 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/Jwt.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/Jwt.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * 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. @@ -15,20 +15,24 @@ */ package org.springframework.security.oauth2.jwt; -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 java.util.function.Consumer; -import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.security.oauth2.core.AbstractOAuth2Token; import org.springframework.util.Assert; +import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD; +import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP; +import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT; +import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS; +import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI; +import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF; +import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB; + /** * An implementation of an {@link AbstractOAuth2Token} representing a JSON Web Token (JWT). * @@ -47,8 +51,6 @@ import org.springframework.util.Assert; * @see JSON Web Encryption (JWE) */ public class Jwt extends AbstractOAuth2Token implements JwtClaimAccessor { - private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; - private final Map headers; private final Map claims; @@ -88,139 +90,181 @@ public class Jwt extends AbstractOAuth2Token implements JwtClaimAccessor { public Map getClaims() { return this.claims; } - - public static Builder builder() { - return new Builder<>(); + + /** + * Return a {@link Jwt.Builder} + * + * @return A {@link Jwt.Builder} + */ + public static Builder withTokenValue(String tokenValue) { + return new Builder(tokenValue); } - + /** * Helps configure a {@link Jwt} * * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @author Josh Cummings + * @since 5.2 */ - public static class Builder> { - protected String tokenValue; - protected final Map claims = new HashMap<>(); - protected final Map headers = new HashMap<>(); - - protected Builder() { - } + public final static class Builder { + private String tokenValue; + private final Map claims = new LinkedHashMap<>(); + private final Map headers = new LinkedHashMap<>(); - public T tokenValue(String tokenValue) { + private Builder(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 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 + * Use this token value in the resulting {@link Jwt} + * + * @param tokenValue The token value to use + * @return the {@link Builder} for further configurations */ - public T claims(Map 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 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 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 audience) { - this.claim(JwtClaimNames.AUD, audience.collect(Collectors.toList())); - return downcast(); - } - - public T audience(Collection 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 implements JwtClaimAccessor { - private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; - - public JwtClaimSet(Map claims) { - super(claims); - } - - @Override - public Map getClaims() { + public Builder tokenValue(String tokenValue) { + this.tokenValue = tokenValue; return this; } - + + /** + * Use this claim in the resulting {@link Jwt} + * + * @param name The claim name + * @param value The claim value + * @return the {@link Builder} for further configurations + */ + public Builder claim(String name, Object value) { + this.claims.put(name, value); + return this; + } + + /** + * Provides access to every {@link #claim(String, Object)} + * declared so far with the possibility to add, replace, or remove. + * @param claimsConsumer the consumer + * @return the {@link Builder} for further configurations + */ + public Builder claims(Consumer> claimsConsumer) { + claimsConsumer.accept(this.claims); + return this; + } + + /** + * Use this header in the resulting {@link Jwt} + * + * @param name The header name + * @param value The header value + * @return the {@link Builder} for further configurations + */ + public Builder header(String name, Object value) { + this.headers.put(name, value); + return this; + } + + /** + * Provides access to every {@link #header(String, Object)} + * declared so far with the possibility to add, replace, or remove. + * @param headersConsumer the consumer + * @return the {@link Builder} for further configurations + */ + public Builder headers(Consumer> headersConsumer) { + headersConsumer.accept(this.headers); + return this; + } + + /** + * Use this audience in the resulting {@link Jwt} + * + * @param audience The audience(s) to use + * @return the {@link Builder} for further configurations + */ + public Builder audience(Collection audience) { + return claim(AUD, audience); + } + + /** + * Use this expiration in the resulting {@link Jwt} + * + * @param expiresAt The expiration to use + * @return the {@link Builder} for further configurations + */ + public Builder expiresAt(Instant expiresAt) { + this.claim(EXP, expiresAt); + return this; + } + + /** + * Use this identifier in the resulting {@link Jwt} + * + * @param jti The identifier to use + * @return the {@link Builder} for further configurations + */ + public Builder jti(String jti) { + this.claim(JTI, jti); + return this; + } + + /** + * Use this issued-at timestamp in the resulting {@link Jwt} + * + * @param issuedAt The issued-at timestamp to use + * @return the {@link Builder} for further configurations + */ + public Builder issuedAt(Instant issuedAt) { + this.claim(IAT, issuedAt); + return this; + } + + /** + * Use this issuer in the resulting {@link Jwt} + * + * @param issuer The issuer to use + * @return the {@link Builder} for further configurations + */ + public Builder issuer(String issuer) { + this.claim(ISS, issuer); + return this; + } + + /** + * Use this not-before timestamp in the resulting {@link Jwt} + * + * @param notBefore The not-before timestamp to use + * @return the {@link Builder} for further configurations + */ + public Builder notBefore(Instant notBefore) { + this.claim(NBF, notBefore.getEpochSecond()); + return this; + } + + /** + * Use this subject in the resulting {@link Jwt} + * + * @param subject The subject to use + * @return the {@link Builder} for further configurations + */ + public Builder subject(String subject) { + this.claim(SUB, subject); + return this; + } + + /** + * Build the {@link Jwt} + * + * @return The constructed {@link Jwt} + */ + public Jwt build() { + Instant iat = toInstant(this.claims.get(IAT)); + Instant exp = toInstant(this.claims.get(EXP)); + return new Jwt(this.tokenValue, iat, exp, this.headers, this.claims); + } + + private Instant toInstant(Object timestamp) { + if (timestamp != null) { + Assert.isInstanceOf(Instant.class, timestamp, "timestamps must be of type Instant"); + } + return (Instant) timestamp; + } } } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtBuilderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtBuilderTests.java index 47ba3b22e0..4004ef9400 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtBuilderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * 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. @@ -15,26 +15,35 @@ */ package org.springframework.security.oauth2.jwt; -import static org.assertj.core.api.Assertions.assertThat; +import java.time.Instant; import org.junit.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP; +import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT; +import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB; + /** * Tests for {@link Jwt.Builder}. + * + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @author Josh Cummings */ public class JwtBuilderTests { - @Test() - public void builderCanBeReused() { - final Jwt.Builder tokensBuilder = Jwt.builder(); - - final Jwt first = tokensBuilder + @Test + public void buildWhenCalledTwiceThenGeneratesTwoJwts() { + Jwt.Builder jwtBuilder = Jwt.withTokenValue("token"); + + Jwt first = jwtBuilder .tokenValue("V1") .header("TEST_HEADER_1", "H1") .claim("TEST_CLAIM_1", "C1") .build(); - - final Jwt second = tokensBuilder + + Jwt second = jwtBuilder .tokenValue("V2") .header("TEST_HEADER_1", "H2") .header("TEST_HEADER_2", "H3") @@ -56,4 +65,120 @@ public class JwtBuilderTests { assertThat(second.getClaims().get("TEST_CLAIM_2")).isEqualTo("C3"); assertThat(second.getTokenValue()).isEqualTo("V2"); } + + @Test + public void expiresAtWhenUsingGenericOrNamedClaimMethodRequiresInstant() { + Jwt.Builder jwtBuilder = Jwt.withTokenValue("token") + .header("needs", "a header"); + + Instant now = Instant.now(); + + Jwt jwt = jwtBuilder + .expiresAt(now).build(); + assertThat(jwt.getExpiresAt()).isSameAs(now); + + jwt = jwtBuilder + .expiresAt(now).build(); + assertThat(jwt.getExpiresAt()).isSameAs(now); + + assertThatCode(() -> jwtBuilder + .claim(EXP, "not an instant").build()) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void issuedAtWhenUsingGenericOrNamedClaimMethodRequiresInstant() { + Jwt.Builder jwtBuilder = Jwt.withTokenValue("token") + .header("needs", "a header"); + + Instant now = Instant.now(); + + Jwt jwt = jwtBuilder + .issuedAt(now).build(); + assertThat(jwt.getIssuedAt()).isSameAs(now); + + jwt = jwtBuilder + .issuedAt(now).build(); + assertThat(jwt.getIssuedAt()).isSameAs(now); + + assertThatCode(() -> jwtBuilder + .claim(IAT, "not an instant").build()) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void subjectWhenUsingGenericOrNamedClaimMethodThenLastOneWins() { + Jwt.Builder jwtBuilder = Jwt.withTokenValue("token") + .header("needs", "a header"); + + String generic = new String("sub"); + String named = new String("sub"); + + Jwt jwt = jwtBuilder + .subject(named) + .claim(SUB, generic).build(); + assertThat(jwt.getSubject()).isSameAs(generic); + + jwt = jwtBuilder + .claim(SUB, generic) + .subject(named).build(); + assertThat(jwt.getSubject()).isSameAs(named); + } + + @Test + public void claimsWhenRemovingAClaimThenIsNotPresent() { + Jwt.Builder jwtBuilder = Jwt.withTokenValue("token") + .claim("needs", "a claim") + .header("needs", "a header"); + + Jwt jwt = jwtBuilder + .subject("sub") + .claims(claims -> claims.remove(SUB)) + .build(); + assertThat(jwt.getSubject()).isNull(); + } + + @Test + public void claimsWhenAddingAClaimThenIsPresent() { + Jwt.Builder jwtBuilder = Jwt.withTokenValue("token") + .header("needs", "a header"); + + String name = new String("name"); + String value = new String("value"); + Jwt jwt = jwtBuilder + .claims(claims -> claims.put(name, value)) + .build(); + + assertThat(jwt.getClaims()).hasSize(1); + assertThat(jwt.getClaims().get(name)).isSameAs(value); + } + + @Test + public void headersWhenRemovingAClaimThenIsNotPresent() { + Jwt.Builder jwtBuilder = Jwt.withTokenValue("token") + .claim("needs", "a claim") + .header("needs", "a header"); + + Jwt jwt = jwtBuilder + .header("alg", "none") + .headers(headers -> headers.remove("alg")) + .build(); + assertThat(jwt.getHeaders().get("alg")).isNull(); + } + + @Test + public void headersWhenAddingAClaimThenIsPresent() { + Jwt.Builder jwtBuilder = Jwt.withTokenValue("token") + .claim("needs", "a claim"); + + String name = new String("name"); + String value = new String("value"); + Jwt jwt = jwtBuilder + .headers(headers -> headers.put(name, value)) + .build(); + + assertThat(jwt.getHeaders()).hasSize(1); + assertThat(jwt.getHeaders().get(name)).isSameAs(value); + } + } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java index 0c7b997ddc..e9ff686ea2 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java @@ -17,11 +17,7 @@ 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; @@ -75,73 +71,4 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok public String getName() { return this.getToken().getSubject(); } - - public static Builder builder(Converter> 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 <ch4mp@c4-soft.com> - * @since 5.2 - */ - public static class Builder> { - - private Converter> authoritiesConverter; - - private final Jwt.Builder jwt; - - protected Builder(Jwt.Builder principalBuilder, Converter> authoritiesConverter) { - this.authoritiesConverter = authoritiesConverter; - this.jwt = principalBuilder; - } - - public T authoritiesConverter(Converter> authoritiesConverter) { - this.authoritiesConverter = authoritiesConverter; - return downcast(); - } - - public T token(Consumer> 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 getAuthorities(Jwt token) { - return authoritiesConverter.convert(token); - } - - @SuppressWarnings("unchecked") - protected T downcast() { - return (T) this; - } - } } diff --git a/samples/boot/oauth2resourceserver/src/test/java/sample/OAuth2ResourceServerControllerTests.java b/samples/boot/oauth2resourceserver/src/test/java/sample/OAuth2ResourceServerControllerTests.java index e9fb7b88a8..9443ffd895 100644 --- a/samples/boot/oauth2resourceserver/src/test/java/sample/OAuth2ResourceServerControllerTests.java +++ b/samples/boot/oauth2resourceserver/src/test/java/sample/OAuth2ResourceServerControllerTests.java @@ -15,14 +15,9 @@ */ 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; @@ -31,9 +26,16 @@ import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; +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; + /** * * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @author Josh Cummings * @since 5.2.0 * */ @@ -49,23 +51,22 @@ public class OAuth2ResourceServerControllerTests { @Test public void indexGreetsAuthenticatedUser() throws Exception { - mockMvc.perform(get("/").with(jwt().name("ch4mpy"))) + mockMvc.perform(get("/").with(jwt(jwt -> jwt.subject("ch4mpy")))) .andExpect(content().string(is("Hello, ch4mpy!"))); } - + @Test public void messageCanBeReadWithScopeMessageReadAuthority() throws Exception { - mockMvc.perform(get("/message").with(jwt().scopes("message:read"))) + mockMvc.perform(get("/message").with(jwt(jwt -> jwt.claim("scope", "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()); } - } diff --git a/test/spring-security-test.gradle b/test/spring-security-test.gradle index 16fe5886d3..5f77d4ab63 100644 --- a/test/spring-security-test.gradle +++ b/test/spring-security-test.gradle @@ -7,8 +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 project(':spring-security-oauth2-resource-server') optional 'io.projectreactor:reactor-core' optional 'org.springframework:spring-webflux' diff --git a/test/src/main/java/org/springframework/security/test/support/JwtAuthenticationTokenTestingBuilder.java b/test/src/main/java/org/springframework/security/test/support/JwtAuthenticationTokenTestingBuilder.java deleted file mode 100644 index 655d8ae5e5..0000000000 --- a/test/src/main/java/org/springframework/security/test/support/JwtAuthenticationTokenTestingBuilder.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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 <ch4mp@c4-soft.com> - * @since 5.2 - */ -public class JwtAuthenticationTokenTestingBuilder> - extends - JwtAuthenticationToken.Builder { - - private static final String[] DEFAULT_SCOPES = { "USER" }; - - private final Set addedAuthorities; - - public JwtAuthenticationTokenTestingBuilder(Converter> 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> authoritiesConverter) { - return authoritiesConverter(authoritiesConverter); - } - - /** - * Adds authorities to what is extracted from the token.
- * 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 authorities) { - addedAuthorities.addAll(authorities.collect(Collectors.toSet())); - return downcast(); - } - - /** - * Adds authorities to what is extracted from the token.
- * 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.
- * 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 getAuthorities(Jwt token) { - final Collection principalAuthorities = super.getAuthorities(token); - - return addedAuthorities.isEmpty() ? principalAuthorities - : Stream.concat(principalAuthorities.stream(), addedAuthorities.stream()).collect(Collectors.toSet()); - } - - /** - * @author Jérôme Wacongne <ch4mp@c4-soft.com> - * @since 5.2 - */ - static class JwtTestingBuilder extends Jwt.Builder { - - 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(); - } - } -} diff --git a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java index 1fb45042c3..89f339f1e4 100644 --- a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java +++ b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -16,11 +16,15 @@ package org.springframework.security.test.web.reactive.server; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; +import reactor.core.publisher.Mono; + +import org.springframework.core.convert.converter.Converter; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.lang.Nullable; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -33,18 +37,19 @@ 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.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; 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; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClientConfigurer; +import org.springframework.util.Assert; 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 static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB; /** * Test utilities for working with Spring Security and @@ -121,13 +126,30 @@ public class SecurityMockServerConfigurers { * declarative and do not require the JWT to be valid. * * @return the {@link JwtMutator} to further configure or use + * @since 5.2 */ public static JwtMutator mockJwt() { - return new JwtMutator(); + return mockJwt(jwt -> {}); } - - public static JwtMutator mockJwt(Consumer> jwt) { - return new JwtMutator().token(jwt); + + /** + * 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. + * + * @param jwtBuilderConsumer For configuring the underlying {@link Jwt} + * @return the {@link JwtMutator} to further configure or use + * @since 5.2 + */ + public static JwtMutator mockJwt(Consumer jwtBuilderConsumer) { + Jwt.Builder jwtBuilder = Jwt.withTokenValue("token") + .header("alg", "none") + .claim(SUB, "user") + .claim("scope", "read"); + jwtBuilderConsumer.accept(jwtBuilder); + return new JwtMutator(jwtBuilder.build()); } public static CsrfMutator csrf() { @@ -315,23 +337,68 @@ public class SecurityMockServerConfigurers { return webFilterChain.filter(exchange); } } - + /** + * Updates the WebServerExchange using + * {@code {@link SecurityMockServerConfigurers#mockAuthentication(Authentication)}}. + * * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @author Josh Cummings * @since 5.2 */ - public static class JwtMutator extends JwtAuthenticationTokenTestingBuilder - implements - WebTestClientConfigurer, MockServerConfigurer { + public static class JwtMutator implements WebTestClientConfigurer, MockServerConfigurer { + private Jwt jwt; + private Collection authorities; + + private JwtMutator(Jwt jwt) { + this.jwt = jwt; + this.authorities = new JwtGrantedAuthoritiesConverter().convert(jwt); + } + + /** + * Use the provided authorities in the token + * @param authorities the authorities to use + * @return the {@link JwtMutator} for further configuration + */ + public JwtMutator authorities(Collection authorities) { + Assert.notNull(authorities, "authorities cannot be null"); + this.authorities = authorities; + return this; + } + + /** + * Use the provided authorities in the token + * @param authorities the authorities to use + * @return the {@link JwtMutator} for further configuration + */ + public JwtMutator authorities(GrantedAuthority... authorities) { + Assert.notNull(authorities, "authorities cannot be null"); + this.authorities = Arrays.asList(authorities); + return this; + } + + /** + * Provides the configured {@link Jwt} so that custom authorities can be derived + * from it + * + * @param authoritiesConverter the conversion strategy from {@link Jwt} to a {@link Collection} + * of {@link GrantedAuthority}s + * @return the {@link JwtMutator} for further configuration + */ + public JwtMutator authorities(Converter> authoritiesConverter) { + Assert.notNull(authoritiesConverter, "authoritiesConverter cannot be null"); + this.authorities = authoritiesConverter.convert(this.jwt); + return this; + } @Override public void beforeServerCreated(WebHttpHandlerBuilder builder) { - mockAuthentication(build()).beforeServerCreated(builder); + configurer().beforeServerCreated(builder); } @Override public void afterConfigureAdded(WebTestClient.MockServerSpec serverSpec) { - mockAuthentication(build()).afterConfigureAdded(serverSpec); + configurer().afterConfigureAdded(serverSpec); } @Override @@ -339,7 +406,11 @@ public class SecurityMockServerConfigurers { WebTestClient.Builder builder, @Nullable WebHttpHandlerBuilder httpHandlerBuilder, @Nullable ClientHttpConnector connector) { - mockAuthentication(build()).afterConfigurerAdded(builder, httpHandlerBuilder, connector); + configurer().afterConfigurerAdded(builder, httpHandlerBuilder, connector); + } + + private T configurer() { + return mockAuthentication(new JwtAuthenticationToken(this.jwt, this.authorities)); } } } 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 2ff1c704ea..05b259d0a6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * 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. @@ -27,10 +27,10 @@ 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; +import org.springframework.core.convert.converter.Converter; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; @@ -48,8 +48,8 @@ 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.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; 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; @@ -63,6 +63,8 @@ import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.util.Assert; import org.springframework.util.DigestUtils; +import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB; + /** * Contains {@link MockMvc} {@link RequestPostProcessor} implementations for Spring * Security. @@ -223,11 +225,41 @@ public final class SecurityMockMvcRequestPostProcessors { * @return the {@link JwtRequestPostProcessor} for additional customization */ public static JwtRequestPostProcessor jwt() { - return new JwtRequestPostProcessor(); + return jwt(jwt -> {}); } - - public static JwtRequestPostProcessor jwt(Consumer> jwt) { - return jwt().token(jwt); + + /** + * 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
  • + *
+ * + * @param jwtBuilderConsumer For configuring the underlying {@link Jwt} + * @return the {@link JwtRequestPostProcessor} for additional customization + * @since 5.2 + */ + public static JwtRequestPostProcessor jwt(Consumer jwtBuilderConsumer) { + Jwt.Builder jwtBuilder = Jwt.withTokenValue("token") + .header("alg", "none") + .claim(SUB, "user") + .claim("scope", "read"); + jwtBuilderConsumer.accept(jwtBuilder); + return new JwtRequestPostProcessor(jwtBuilder.build()); } /** @@ -590,7 +622,7 @@ public final class SecurityMockMvcRequestPostProcessors { * Support class for {@link RequestPostProcessor}'s that establish a Spring Security * context */ - static class SecurityContextRequestPostProcessorSupport { + private static abstract class SecurityContextRequestPostProcessorSupport { /** * Saves the specified {@link Authentication} into an empty @@ -599,7 +631,7 @@ public final class SecurityMockMvcRequestPostProcessors { * @param authentication the {@link Authentication} to save * @param request the {@link HttpServletRequest} to use */ - static final void save(Authentication authentication, HttpServletRequest request) { + final void save(Authentication authentication, HttpServletRequest request) { SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); securityContext.setAuthentication(authentication); save(securityContext, request); @@ -611,7 +643,7 @@ public final class SecurityMockMvcRequestPostProcessors { * @param securityContext the {@link SecurityContext} to save * @param request the {@link HttpServletRequest} to use */ - static final void save(SecurityContext securityContext, HttpServletRequest request) { + final void save(SecurityContext securityContext, HttpServletRequest request) { SecurityContextRepository securityContextRepository = WebTestUtils .getSecurityContextRepository(request); boolean isTestRepository = securityContextRepository instanceof TestSecurityContextRepository; @@ -639,7 +671,7 @@ public final class SecurityMockMvcRequestPostProcessors { * stateless mode */ static class TestSecurityContextRepository implements SecurityContextRepository { - final static String ATTR_NAME = TestSecurityContextRepository.class + private final static String ATTR_NAME = TestSecurityContextRepository.class .getName().concat(".REPO"); private final SecurityContextRepository delegate; @@ -751,6 +783,8 @@ public final class SecurityMockMvcRequestPostProcessors { @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(this.authentication); save(this.authentication, request); return request; } @@ -938,22 +972,64 @@ public final class SecurityMockMvcRequestPostProcessors { } } - private SecurityMockMvcRequestPostProcessors() { - } - /** * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @author Josh Cummings * @since 5.2 */ - public static class JwtRequestPostProcessor extends JwtAuthenticationTokenTestingBuilder - implements - RequestPostProcessor { + public final static class JwtRequestPostProcessor implements RequestPostProcessor { + private Jwt jwt; + private Collection authorities; + + private JwtRequestPostProcessor(Jwt jwt) { + this.jwt = jwt; + this.authorities = new JwtGrantedAuthoritiesConverter().convert(jwt); + } + + /** + * Use the provided authorities in the token + * @param authorities the authorities to use + * @return the {@link JwtRequestPostProcessor} for further configuration + */ + public JwtRequestPostProcessor authorities(Collection authorities) { + Assert.notNull(authorities, "authorities cannot be null"); + this.authorities = authorities; + return this; + } + + /** + * Use the provided authorities in the token + * @param authorities the authorities to use + * @return the {@link JwtRequestPostProcessor} for further configuration + */ + public JwtRequestPostProcessor authorities(GrantedAuthority... authorities) { + Assert.notNull(authorities, "authorities cannot be null"); + this.authorities = Arrays.asList(authorities); + return this; + } + + /** + * Provides the configured {@link Jwt} so that custom authorities can be derived + * from it + * + * @param authoritiesConverter the conversion strategy from {@link Jwt} to a {@link Collection} + * of {@link GrantedAuthority}s + * @return the {@link JwtRequestPostProcessor} for further configuration + */ + public JwtRequestPostProcessor authorities(Converter> authoritiesConverter) { + Assert.notNull(authoritiesConverter, "authoritiesConverter cannot be null"); + this.authorities = authoritiesConverter.convert(this.jwt); + return this; + } @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { - SecurityContextRequestPostProcessorSupport.save(build(), request); - return request; + JwtAuthenticationToken token = new JwtAuthenticationToken(this.jwt, this.authorities); + return new AuthenticationRequestPostProcessor(token).postProcessRequest(request); } } + + private SecurityMockMvcRequestPostProcessors() { + } } 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 deleted file mode 100644 index e153f91491..0000000000 --- a/test/src/test/java/org/springframework/security/test/support/JwtAuthenticationTokenTestingBuilderTests.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 deleted file mode 100644 index 75923fac63..0000000000 --- a/test/src/test/java/org/springframework/security/test/support/JwtTestingBuilderTests.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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/AbstractMockServerConfigurersTests.java b/test/src/test/java/org/springframework/security/test/web/reactive/server/AbstractMockServerConfigurersTests.java index aa5b018b72..6af2661f64 100644 --- a/test/src/test/java/org/springframework/security/test/web/reactive/server/AbstractMockServerConfigurersTests.java +++ b/test/src/test/java/org/springframework/security/test/web/reactive/server/AbstractMockServerConfigurersTests.java @@ -16,22 +16,26 @@ package org.springframework.security.test.web.reactive.server; +import java.security.Principal; + import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.annotation.CurrentSecurityContext; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; - import static org.assertj.core.api.Assertions.assertThat; /** * @author Rob Winch + * @author Josh Cummings * @since 5.0 */ abstract class AbstractMockServerConfigurersTests { protected PrincipalController controller = new PrincipalController(); + protected SecurityContextController securityContextController = new SecurityContextController(); protected User.UserBuilder userBuilder = User .withUsername("user") @@ -71,4 +75,21 @@ abstract class AbstractMockServerConfigurersTests { this.principal = null; } } + + @RestController + protected static class SecurityContextController { + volatile SecurityContext securityContext; + + @RequestMapping("/**") + public SecurityContext get(@CurrentSecurityContext SecurityContext securityContext) { + this.securityContext = securityContext; + return securityContext; + } + + public SecurityContext removeSecurityContext() { + SecurityContext result = this.securityContext; + this.securityContext = null; + return result; + } + } } 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 deleted file mode 100644 index fb1ee4bd66..0000000000 --- a/test/src/test/java/org/springframework/security/test/web/reactive/server/JwtMutatorTests.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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/SecurityMockServerConfigurersJwtTests.java b/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurersJwtTests.java new file mode 100644 index 0000000000..adcf9a922f --- /dev/null +++ b/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurersJwtTests.java @@ -0,0 +1,139 @@ +/* + * 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 java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.core.GrantedAuthority; +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.web.reactive.result.method.annotation.CurrentSecurityContextArgumentResolver; +import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockJwt; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @author Josh Cummings + * @since 5.2 + */ +@RunWith(MockitoJUnitRunner.class) +public class SecurityMockServerConfigurersJwtTests extends AbstractMockServerConfigurersTests { + @Mock + GrantedAuthority authority1; + + @Mock + GrantedAuthority authority2; + + WebTestClient client = WebTestClient + .bindToController(securityContextController) + .webFilter(new SecurityContextServerWebExchangeWebFilter()) + .argumentResolvers(resolvers -> resolvers.addCustomResolver( + new CurrentSecurityContextArgumentResolver(new ReactiveAdapterRegistry()))) + .apply(springSecurity()) + .configureClient() + .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .build(); + + @Test + public void mockJwtWhenUsingDefaultsTheCreatesJwtAuthentication() { + client + .mutateWith(mockJwt()) + .get() + .exchange() + .expectStatus().isOk(); + + SecurityContext context = securityContextController.removeSecurityContext(); + assertThat(context.getAuthentication()).isInstanceOf( + JwtAuthenticationToken.class); + JwtAuthenticationToken token = (JwtAuthenticationToken) context.getAuthentication(); + assertThat(token.getAuthorities()).isNotEmpty(); + assertThat(token.getToken()).isNotNull(); + assertThat(token.getToken().getSubject()).isEqualTo("user"); + assertThat(token.getToken().getHeaders().get("alg")).isEqualTo("none"); + } + + @Test + public void mockJwtWhenProvidingBuilderConsumerThenProducesJwtAuthentication() { + String name = new String("user"); + client + .mutateWith(mockJwt(jwt -> jwt.subject(name))) + .get() + .exchange() + .expectStatus().isOk(); + + SecurityContext context = securityContextController.removeSecurityContext(); + assertThat(context.getAuthentication()).isInstanceOf( + JwtAuthenticationToken.class); + JwtAuthenticationToken token = (JwtAuthenticationToken) context.getAuthentication(); + assertThat(token.getToken().getSubject()).isSameAs(name); + } + + @Test + public void mockJwtWhenProvidingCustomAuthoritiesThenProducesJwtAuthentication() { + client + .mutateWith(mockJwt(jwt -> jwt.claim("scope", "ignored authorities")) + .authorities(this.authority1, this.authority2)) + .get() + .exchange() + .expectStatus().isOk(); + + SecurityContext context = securityContextController.removeSecurityContext(); + assertThat((List) context.getAuthentication().getAuthorities()) + .containsOnly(this.authority1, this.authority2); + } + + @Test + public void mockJwtWhenProvidingScopedAuthoritiesThenProducesJwtAuthentication() { + client + .mutateWith(mockJwt(jwt -> jwt.claim("scope", "scoped authorities"))) + .get() + .exchange() + .expectStatus().isOk(); + + SecurityContext context = securityContextController.removeSecurityContext(); + assertThat((List) context.getAuthentication().getAuthorities()) + .containsOnly(new SimpleGrantedAuthority("SCOPE_scoped"), + new SimpleGrantedAuthority("SCOPE_authorities")); + } + + @Test + public void mockJwtWhenProvidingGrantedAuthoritiesThenProducesJwtAuthentication() { + client + .mutateWith(mockJwt(jwt -> jwt.claim("scope", "ignored authorities")) + .authorities(jwt -> Arrays.asList(this.authority1))) + .get() + .exchange() + .expectStatus().isOk(); + + SecurityContext context = securityContextController.removeSecurityContext(); + assertThat((List) context.getAuthentication().getAuthorities()) + .containsOnly(this.authority1); + } +} 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 deleted file mode 100644 index 449fd6d37e..0000000000 --- a/test/src/test/java/org/springframework/security/test/web/reactive/server/TestController.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 deleted file mode 100644 index fcf40a9122..0000000000 --- a/test/src/test/java/org/springframework/security/test/web/servlet/request/JwtRequestPostProcessorTests.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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(); - } - -} diff --git a/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessorsJwtTests.java b/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessorsJwtTests.java new file mode 100644 index 0000000000..565de65bc0 --- /dev/null +++ b/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessorsJwtTests.java @@ -0,0 +1,157 @@ +/* + * 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 java.util.Arrays; +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockServletContext; +import org.springframework.security.config.BeanIds; +import org.springframework.security.core.GrantedAuthority; +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.context.TestSecurityContextHolder; +import org.springframework.security.test.web.support.WebTestUtils; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.context.SecurityContextPersistenceFilter; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; + +/** + * Tests for {@link SecurityMockMvcRequestPostProcessors#jwt} + * + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @author Josh Cummings + * @since 5.2 + */ +@RunWith(MockitoJUnitRunner.class) +public class SecurityMockMvcRequestPostProcessorsJwtTests { + @Captor + private ArgumentCaptor contextCaptor; + + @Mock + private SecurityContextRepository repository; + + private MockHttpServletRequest request; + + @Mock + private GrantedAuthority authority1; + @Mock + private GrantedAuthority authority2; + + @Before + public void setup() { + SecurityContextPersistenceFilter filter = new SecurityContextPersistenceFilter(this.repository); + MockServletContext servletContext = new MockServletContext(); + servletContext.setAttribute(BeanIds.SPRING_SECURITY_FILTER_CHAIN, + new FilterChainProxy(new DefaultSecurityFilterChain(AnyRequestMatcher.INSTANCE, filter))); + this.request = new MockHttpServletRequest(servletContext); + WebTestUtils.setSecurityContextRepository(this.request, this.repository); + } + + @After + public void cleanup() { + TestSecurityContextHolder.clearContext(); + } + + @Test + public void jwtWhenUsingDefaultsThenProducesDefaultJwtAuthentication() { + jwt().postProcessRequest(this.request); + + verify(this.repository).saveContext(this.contextCaptor.capture(), eq(this.request), + any(HttpServletResponse.class)); + SecurityContext context = this.contextCaptor.getValue(); + assertThat(context.getAuthentication()).isInstanceOf( + JwtAuthenticationToken.class); + JwtAuthenticationToken token = (JwtAuthenticationToken) context.getAuthentication(); + assertThat(token.getAuthorities()).isNotEmpty(); + assertThat(token.getToken()).isNotNull(); + assertThat(token.getToken().getSubject()).isEqualTo("user"); + assertThat(token.getToken().getHeaders().get("alg")).isEqualTo("none"); + } + + @Test + public void jwtWhenProvidingBuilderConsumerThenProducesJwtAuthentication() { + String name = new String("user"); + jwt(jwt -> jwt.subject(name)).postProcessRequest(this.request); + + verify(this.repository).saveContext(this.contextCaptor.capture(), eq(this.request), + any(HttpServletResponse.class)); + SecurityContext context = this.contextCaptor.getValue(); + assertThat(context.getAuthentication()).isInstanceOf( + JwtAuthenticationToken.class); + JwtAuthenticationToken token = (JwtAuthenticationToken) context.getAuthentication(); + assertThat(token.getToken().getSubject()).isSameAs(name); + } + + @Test + public void jwtWhenProvidingCustomAuthoritiesThenProducesJwtAuthentication() { + jwt(jwt -> jwt.claim("scope", "ignored authorities")) + .authorities(this.authority1, this.authority2) + .postProcessRequest(this.request); + + verify(this.repository).saveContext(this.contextCaptor.capture(), eq(this.request), + any(HttpServletResponse.class)); + SecurityContext context = this.contextCaptor.getValue(); + assertThat((List) context.getAuthentication().getAuthorities()) + .containsOnly(this.authority1, this.authority2); + } + + @Test + public void jwtWhenProvidingScopedAuthoritiesThenProducesJwtAuthentication() { + jwt(jwt -> jwt.claim("scope", "scoped authorities")) + .postProcessRequest(this.request); + + verify(this.repository).saveContext(this.contextCaptor.capture(), eq(this.request), + any(HttpServletResponse.class)); + SecurityContext context = this.contextCaptor.getValue(); + assertThat((List) context.getAuthentication().getAuthorities()) + .containsOnly(new SimpleGrantedAuthority("SCOPE_scoped"), + new SimpleGrantedAuthority("SCOPE_authorities")); + } + + @Test + public void jwtWhenProvidingGrantedAuthoritiesThenProducesJwtAuthentication() { + jwt(jwt -> jwt.claim("scope", "ignored authorities")) + .authorities(jwt -> Arrays.asList(this.authority1)) + .postProcessRequest(this.request); + + verify(this.repository).saveContext(this.contextCaptor.capture(), eq(this.request), + any(HttpServletResponse.class)); + SecurityContext context = this.contextCaptor.getValue(); + assertThat((List) context.getAuthentication().getAuthorities()) + .containsOnly(this.authority1); + } +}