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
This commit is contained in:
Josh Cummings 2019-05-21 17:59:55 -06:00
parent e59d8a529b
commit d0f5b42884
No known key found for this signature in database
GPG Key ID: 49EF60DD7FF83443
16 changed files with 819 additions and 747 deletions

View File

@ -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 <a target="_blank" href="https://tools.ietf.org/html/rfc7516">JSON Web Encryption (JWE)</a>
*/
public class Jwt extends AbstractOAuth2Token implements JwtClaimAccessor {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Map<String, Object> headers;
private final Map<String, Object> claims;
@ -88,139 +90,181 @@ public class Jwt extends AbstractOAuth2Token implements JwtClaimAccessor {
public Map<String, Object> 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 &lt;ch4mp&#64;c4-soft.com&gt;
* @author Josh Cummings
* @since 5.2
*/
public static class Builder<T extends Builder<T>> {
protected String tokenValue;
protected final Map<String, Object> claims = new HashMap<>();
protected final Map<String, Object> headers = new HashMap<>();
protected Builder() {
}
public final static class Builder {
private String tokenValue;
private final Map<String, Object> claims = new LinkedHashMap<>();
private final Map<String, Object> 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<String, Object> claims) {
this.claims.clear();
return downcast();
}
/**
* Adds to existing claims (does not replace existing ones)
* @param claims claims to add
* @return this builder to further configure
* 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<String, Object> claims) {
this.claims.putAll(claims);
return downcast();
}
public T header(String name, Object value) {
this.headers.put(name, value);
return downcast();
}
public T clearHeaders(Map<String, Object> headers) {
this.headers.clear();
return downcast();
}
/**
* Adds to existing headers (does not replace existing ones)
* @param headers headers to add
* @return this builder to further configure
*/
public T headers(Map<String, Object> headers) {
headers.entrySet().stream().forEach(e -> this.header(e.getKey(), e.getValue()));
return downcast();
}
public Jwt build() {
final JwtClaimSet claimSet = new JwtClaimSet(claims);
return new Jwt(
this.tokenValue,
claimSet.getClaimAsInstant(JwtClaimNames.IAT),
claimSet.getClaimAsInstant(JwtClaimNames.EXP),
this.headers,
claimSet);
}
public T audience(Stream<String> audience) {
this.claim(JwtClaimNames.AUD, audience.collect(Collectors.toList()));
return downcast();
}
public T audience(Collection<String> audience) {
return audience(audience.stream());
}
public T audience(String... audience) {
return audience(Stream.of(audience));
}
public T expiresAt(Instant expiresAt) {
this.claim(JwtClaimNames.EXP, expiresAt.getEpochSecond());
return downcast();
}
public T jti(String jti) {
this.claim(JwtClaimNames.JTI, jti);
return downcast();
}
public T issuedAt(Instant issuedAt) {
this.claim(JwtClaimNames.IAT, issuedAt.getEpochSecond());
return downcast();
}
public T issuer(URL issuer) {
this.claim(JwtClaimNames.ISS, issuer.toExternalForm());
return downcast();
}
public T notBefore(Instant notBefore) {
this.claim(JwtClaimNames.NBF, notBefore.getEpochSecond());
return downcast();
}
public T subject(String subject) {
this.claim(JwtClaimNames.SUB, subject);
return downcast();
}
@SuppressWarnings("unchecked")
protected T downcast() {
return (T) this;
}
}
private static final class JwtClaimSet extends HashMap<String, Object> implements JwtClaimAccessor {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
public JwtClaimSet(Map<String, Object> claims) {
super(claims);
}
@Override
public Map<String, Object> getClaims() {
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<Map<String, Object>> 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<Map<String, Object>> 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<String> 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;
}
}
}

View File

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

View File

@ -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<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
return new Builder<>(Jwt.builder(), authoritiesConverter);
}
public static Builder<?> builder() {
return builder(new JwtGrantedAuthoritiesConverter());
}
/**
* Helps configure a {@link JwtAuthenticationToken}
*
* @author Jérôme Wacongne &lt;ch4mp&#64;c4-soft.com&gt;
* @since 5.2
*/
public static class Builder<T extends Builder<T>> {
private Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter;
private final Jwt.Builder<?> jwt;
protected Builder(Jwt.Builder<?> principalBuilder, Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
this.authoritiesConverter = authoritiesConverter;
this.jwt = principalBuilder;
}
public T authoritiesConverter(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
this.authoritiesConverter = authoritiesConverter;
return downcast();
}
public T token(Consumer<Jwt.Builder<?>> jwtBuilderConsumer) {
jwtBuilderConsumer.accept(jwt);
return downcast();
}
public T name(String name) {
jwt.subject(name);
return downcast();
}
/**
* Shortcut to set "scope" claim with a space separated string containing provided scope collection
* @param scopes strings to join with spaces and set as "scope" claim
* @return this builder to further configure
*/
public T scopes(String... scopes) {
jwt.claim("scope", Stream.of(scopes).collect(Collectors.joining(" ")));
return downcast();
}
public JwtAuthenticationToken build() {
final Jwt token = jwt.build();
return new JwtAuthenticationToken(token, getAuthorities(token));
}
protected Jwt getToken() {
return jwt.build();
}
protected Collection<GrantedAuthority> getAuthorities(Jwt token) {
return authoritiesConverter.convert(token);
}
@SuppressWarnings("unchecked")
protected T downcast() {
return (T) this;
}
}
}

View File

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

View File

@ -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'

View File

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

View File

@ -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.Builder<?>> 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<Jwt.Builder> 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 &lt;ch4mp&#64;c4-soft.com&gt;
* @author Josh Cummings
* @since 5.2
*/
public static class JwtMutator extends JwtAuthenticationTokenTestingBuilder<JwtMutator>
implements
WebTestClientConfigurer, MockServerConfigurer {
public static class JwtMutator implements WebTestClientConfigurer, MockServerConfigurer {
private Jwt jwt;
private Collection<GrantedAuthority> 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<GrantedAuthority> 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<Jwt, Collection<GrantedAuthority>> 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 extends WebTestClientConfigurer & MockServerConfigurer> T configurer() {
return mockAuthentication(new JwtAuthenticationToken(this.jwt, this.authorities));
}
}
}

View File

@ -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.Builder<?>> 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.
*
* <p>
* The support works by associating the authentication to the HttpServletRequest. To associate
* the request to the SecurityContextHolder you need to ensure that the
* SecurityContextPersistenceFilter is associated with the MockMvc instance. A few
* ways to do this are:
* </p>
*
* <ul>
* <li>Invoking apply {@link SecurityMockMvcConfigurers#springSecurity()}</li>
* <li>Adding Spring Security's FilterChainProxy to MockMvc</li>
* <li>Manually adding {@link SecurityContextPersistenceFilter} to the MockMvc
* instance may make sense when using MockMvcBuilders standaloneSetup</li>
* </ul>
*
* @param jwtBuilderConsumer For configuring the underlying {@link Jwt}
* @return the {@link JwtRequestPostProcessor} for additional customization
* @since 5.2
*/
public static JwtRequestPostProcessor jwt(Consumer<Jwt.Builder> 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 &lt;ch4mp&#64;c4-soft.com&gt;
* @author Josh Cummings
* @since 5.2
*/
public static class JwtRequestPostProcessor extends JwtAuthenticationTokenTestingBuilder<JwtRequestPostProcessor>
implements
RequestPostProcessor {
public final static class JwtRequestPostProcessor implements RequestPostProcessor {
private Jwt jwt;
private Collection<? extends GrantedAuthority> 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<GrantedAuthority> 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<Jwt, Collection<GrantedAuthority>> 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() {
}
}

View File

@ -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 &lt;ch4mp&#64;c4-soft.com&gt;
* @since 5.2
*/
public class JwtAuthenticationTokenTestingBuilderTests {
@Test
public void untouchedBuilderSetsDefaultValues() {
final JwtAuthenticationToken actual = new JwtAuthenticationTokenTestingBuilder<>().build();
assertThat(actual.getName()).isEqualTo("user");
assertThat(actual.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SCOPE_USER"));
assertThat(actual.getPrincipal()).isInstanceOf(Jwt.class);
assertThat(actual.getCredentials()).isInstanceOf(Jwt.class);
assertThat(actual.getDetails()).isNull();
// Token default values are tested in JwtTestingBuilderTests
assertThat(actual.getToken()).isEqualTo(new JwtAuthenticationTokenTestingBuilder.JwtTestingBuilder().build());
}
@Test
public void nameOverridesDefaultValue() {
assertThat(new JwtAuthenticationTokenTestingBuilder<>().name("ch4mpy").build().getName()).isEqualTo("ch4mpy");
}
@Test
public void authoritiesAddsToDefaultValue() {
assertThat(new JwtAuthenticationTokenTestingBuilder<>().authorities("TEST").build().getAuthorities())
.containsExactlyInAnyOrder(new SimpleGrantedAuthority("SCOPE_USER"), new SimpleGrantedAuthority("TEST"));
}
@Test
public void scopesOveridesDefaultValue() {
assertThat(new JwtAuthenticationTokenTestingBuilder<>().scopes("TEST").build().getAuthorities())
.containsExactly(new SimpleGrantedAuthority("SCOPE_TEST"));
}
@Test
public void nameSetsAuthenticationNameAndTokenSubjectClaim() {
final JwtAuthenticationToken actual = new JwtAuthenticationTokenTestingBuilder<>().name("ch4mpy").build();
assertThat(actual.getName()).isEqualTo("ch4mpy");
assertThat(actual.getTokenAttributes().get(JwtClaimNames.SUB)).isEqualTo("ch4mpy");
}
@Test
public void buildMergesConvertedClaimsAndAuthorities() {
final JwtAuthenticationToken actual = new JwtAuthenticationTokenTestingBuilder<>().name("ch4mpy")
.authorities(new SimpleGrantedAuthority("TEST_AUTHORITY"))
.scopes("scope:claim")
.build();
assertThat(actual.getAuthorities()).containsExactlyInAnyOrder(
new SimpleGrantedAuthority("TEST_AUTHORITY"),
new SimpleGrantedAuthority("SCOPE_scope:claim"));
}
}

View File

@ -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 &lt;ch4mp&#64;c4-soft.com&gt;
*/
public class JwtTestingBuilderTests {
@Test
public void testDefaultValuesAreSet() {
final Jwt actual = new JwtTestingBuilder().build();
assertThat(actual.getTokenValue()).isEqualTo("test.jwt.value");
assertThat(actual.getClaimAsString(JwtClaimNames.SUB)).isEqualTo("user");
assertThat(actual.getHeaders()).hasSize(1);
}
@Test
public void iatClaimAndExpClaimSetIssuedAtAndExpiresAt() {
final Jwt actual = new JwtTestingBuilder()
.claim(JwtClaimNames.IAT, Instant.parse("2019-03-21T13:52:25Z"))
.claim(JwtClaimNames.EXP, Instant.parse("2019-03-22T13:52:25Z"))
.build();
assertThat(actual.getIssuedAt()).isEqualTo(Instant.parse("2019-03-21T13:52:25Z"));
assertThat(actual.getExpiresAt()).isEqualTo(Instant.parse("2019-03-22T13:52:25Z"));
assertThat(actual.getClaimAsInstant(JwtClaimNames.IAT)).isEqualTo(Instant.parse("2019-03-21T13:52:25Z"));
assertThat(actual.getClaimAsInstant(JwtClaimNames.EXP)).isEqualTo(Instant.parse("2019-03-22T13:52:25Z"));
}
}

View File

@ -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;
}
}
}

View File

@ -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 &lt;ch4mp&#64;c4-soft.com&gt;
* @since 5.2
*/
public class JwtMutatorTests {
// @formatter:off
@Test
public void defaultJwtConfigurerConfiguresAuthenticationDefaultNameAndAuthorities() {
TestController.clientBuilder()
.apply(mockJwt()).build()
.get().uri("/greet").exchange()
.expectStatus().isOk()
.expectBody().toString().equals("Hello user!");
TestController.clientBuilder()
.apply(mockJwt()).build()
.get().uri("/authorities").exchange()
.expectStatus().isOk()
.expectBody().toString().equals("[\"ROLE_USER\"]");
}
@Test
public void nameAndScopesConfigureAuthenticationNameAndAuthorities() {
TestController.clientBuilder()
.apply(mockJwt().name("ch4mpy").scopes("message:read")).build()
.get().uri("/greet").exchange()
.expectStatus().isOk()
.expectBody().toString().equals("Hello ch4mpy!");
TestController.clientBuilder()
.apply(mockJwt().name("ch4mpy").scopes("message:read")).build()
.get().uri("/authorities").exchange()
.expectStatus().isOk()
.expectBody().toString().equals("[\"SCOPE_message:read\"]");
TestController.clientBuilder()
.apply(mockJwt().name("ch4mpy").scopes("message:read")).build()
.get().uri("/jwt").exchange()
.expectStatus().isOk()
.expectBody().toString().equals(
"Hello,ch4mpy! You are sucessfully authenticated and granted with [message:read] scopes using a JavaWebToken.");
}
// @formatter:on
}

View File

@ -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 &lt;ch4mp&#64;c4-soft.com&gt;
* @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<GrantedAuthority>) 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<GrantedAuthority>) 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<GrantedAuthority>) context.getAuthentication().getAuthorities())
.containsOnly(this.authority1);
}
}

View File

@ -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 &lt;ch4mp&#64;c4-soft.com&gt;
* @since 5.2
*/
@RestController
public class TestController {
@GetMapping("/greet")
public String greet(final Principal authentication) {
return String.format("Hello, %s!", authentication.getName());
}
@GetMapping("/authorities")
public String authentication(final Authentication authentication) {
return authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList())
.toString();
}
@GetMapping("/jwt")
// TODO: investigate why "@AuthenticationPrincipal Jwt token" does not work here
public String jwt(final Authentication authentication) {
final Jwt token = (Jwt) authentication.getPrincipal();
final String scopes = token.getClaimAsString("scope");
return String.format(
"Hello, %s! You are sucessfully authenticated and granted with %s scopes using a Jwt.",
token.getSubject(),
scopes);
}
public static WebTestClient.Builder clientBuilder() {
return WebTestClient.bindToController(new TestController())
.webFilter(new CsrfWebFilter(), new SecurityContextServerWebExchangeWebFilter())
.apply(springSecurity())
.configureClient()
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
}
public static WebTestClient client() {
return (WebTestClient) clientBuilder().build();
}
}

View File

@ -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 &lt;ch4mp&#64;c4-soft.com&gt;
* @since 5.2
*/
public class JwtRequestPostProcessorTests {
@Mock
MockHttpServletRequest request;
final static String TEST_NAME = "ch4mpy";
final static String[] TEST_AUTHORITIES = { "TEST_AUTHORITY" };
@Before
public void setup() throws Exception {
request = new MockHttpServletRequest();
}
@Test
public void nameAndAuthoritiesAndClaimsConfigureSecurityContextAuthentication() {
final JwtRequestPostProcessor rpp =
jwt().name(TEST_NAME).authorities(TEST_AUTHORITIES).scopes("test:claim");
final JwtAuthenticationToken actual = (JwtAuthenticationToken) authentication(rpp.postProcessRequest(request));
assertThat(actual.getName()).isEqualTo(TEST_NAME);
assertThat(actual.getAuthorities()).containsExactlyInAnyOrder(
new SimpleGrantedAuthority("TEST_AUTHORITY"),
new SimpleGrantedAuthority("SCOPE_test:claim"));
assertThat(actual.getTokenAttributes().get("scope")).isEqualTo("test:claim");
}
static Authentication authentication(final MockHttpServletRequest req) {
final SecurityContext securityContext = (SecurityContext) req.getAttribute(TestSecurityContextRepository.ATTR_NAME);
return securityContext == null ? null : securityContext.getAuthentication();
}
}

View File

@ -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 &lt;ch4mp&#64;c4-soft.com&gt;
* @author Josh Cummings
* @since 5.2
*/
@RunWith(MockitoJUnitRunner.class)
public class SecurityMockMvcRequestPostProcessorsJwtTests {
@Captor
private ArgumentCaptor<SecurityContext> 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<GrantedAuthority>) 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<GrantedAuthority>) 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<GrantedAuthority>) context.getAuthentication().getAuthorities())
.containsOnly(this.authority1);
}
}