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:
parent
e59d8a529b
commit
d0f5b42884
|
@ -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 <ch4mp@c4-soft.com>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 <ch4mp@c4-soft.com>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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<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 <ch4mp@c4-soft.com>
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <ch4mp@c4-soft.com>
|
||||
* @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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <ch4mp@c4-soft.com>
|
||||
* @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() {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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<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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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<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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue