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); + } +}