mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-04-17 04:39:54 +00:00
Mock Jwt Test Support and Jwt.Builder
Fixes: gh-6634 Fixes: gh-6851
This commit is contained in:
parent
f6998547b8
commit
e59d8a529b
@ -15,13 +15,19 @@
|
|||||||
*/
|
*/
|
||||||
package org.springframework.security.oauth2.jwt;
|
package org.springframework.security.oauth2.jwt;
|
||||||
|
|
||||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
import java.net.URL;
|
||||||
import org.springframework.util.Assert;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||||
|
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of an {@link AbstractOAuth2Token} representing a JSON Web Token (JWT).
|
* An implementation of an {@link AbstractOAuth2Token} representing a JSON Web Token (JWT).
|
||||||
@ -41,6 +47,8 @@ import java.util.Map;
|
|||||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516">JSON Web Encryption (JWE)</a>
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516">JSON Web Encryption (JWE)</a>
|
||||||
*/
|
*/
|
||||||
public class Jwt extends AbstractOAuth2Token implements JwtClaimAccessor {
|
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> headers;
|
||||||
private final Map<String, Object> claims;
|
private final Map<String, Object> claims;
|
||||||
|
|
||||||
@ -80,4 +88,139 @@ public class Jwt extends AbstractOAuth2Token implements JwtClaimAccessor {
|
|||||||
public Map<String, Object> getClaims() {
|
public Map<String, Object> getClaims() {
|
||||||
return this.claims;
|
return this.claims;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Builder<?> builder() {
|
||||||
|
return new Builder<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helps configure a {@link Jwt}
|
||||||
|
*
|
||||||
|
* @author Jérôme Wacongne <ch4mp@c4-soft.com>
|
||||||
|
*/
|
||||||
|
public static class Builder<T extends Builder<T>> {
|
||||||
|
protected String tokenValue;
|
||||||
|
protected final Map<String, Object> claims = new HashMap<>();
|
||||||
|
protected final Map<String, Object> headers = new HashMap<>();
|
||||||
|
|
||||||
|
protected Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public T tokenValue(String tokenValue) {
|
||||||
|
this.tokenValue = tokenValue;
|
||||||
|
return downcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T claim(String name, Object value) {
|
||||||
|
this.claims.put(name, value);
|
||||||
|
return downcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T clearClaims(Map<String, Object> claims) {
|
||||||
|
this.claims.clear();
|
||||||
|
return downcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds to existing claims (does not replace existing ones)
|
||||||
|
* @param claims claims to add
|
||||||
|
* @return this builder to further configure
|
||||||
|
*/
|
||||||
|
public T claims(Map<String, Object> claims) {
|
||||||
|
this.claims.putAll(claims);
|
||||||
|
return downcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T header(String name, Object value) {
|
||||||
|
this.headers.put(name, value);
|
||||||
|
return downcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T clearHeaders(Map<String, Object> headers) {
|
||||||
|
this.headers.clear();
|
||||||
|
return downcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds to existing headers (does not replace existing ones)
|
||||||
|
* @param headers headers to add
|
||||||
|
* @return this builder to further configure
|
||||||
|
*/
|
||||||
|
public T headers(Map<String, Object> headers) {
|
||||||
|
headers.entrySet().stream().forEach(e -> this.header(e.getKey(), e.getValue()));
|
||||||
|
return downcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Jwt build() {
|
||||||
|
final JwtClaimSet claimSet = new JwtClaimSet(claims);
|
||||||
|
return new Jwt(
|
||||||
|
this.tokenValue,
|
||||||
|
claimSet.getClaimAsInstant(JwtClaimNames.IAT),
|
||||||
|
claimSet.getClaimAsInstant(JwtClaimNames.EXP),
|
||||||
|
this.headers,
|
||||||
|
claimSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T audience(Stream<String> audience) {
|
||||||
|
this.claim(JwtClaimNames.AUD, audience.collect(Collectors.toList()));
|
||||||
|
return downcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T audience(Collection<String> audience) {
|
||||||
|
return audience(audience.stream());
|
||||||
|
}
|
||||||
|
|
||||||
|
public T audience(String... audience) {
|
||||||
|
return audience(Stream.of(audience));
|
||||||
|
}
|
||||||
|
|
||||||
|
public T expiresAt(Instant expiresAt) {
|
||||||
|
this.claim(JwtClaimNames.EXP, expiresAt.getEpochSecond());
|
||||||
|
return downcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T jti(String jti) {
|
||||||
|
this.claim(JwtClaimNames.JTI, jti);
|
||||||
|
return downcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T issuedAt(Instant issuedAt) {
|
||||||
|
this.claim(JwtClaimNames.IAT, issuedAt.getEpochSecond());
|
||||||
|
return downcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T issuer(URL issuer) {
|
||||||
|
this.claim(JwtClaimNames.ISS, issuer.toExternalForm());
|
||||||
|
return downcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T notBefore(Instant notBefore) {
|
||||||
|
this.claim(JwtClaimNames.NBF, notBefore.getEpochSecond());
|
||||||
|
return downcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T subject(String subject) {
|
||||||
|
this.claim(JwtClaimNames.SUB, subject);
|
||||||
|
return downcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected T downcast() {
|
||||||
|
return (T) this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class JwtClaimSet extends HashMap<String, Object> implements JwtClaimAccessor {
|
||||||
|
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||||
|
|
||||||
|
public JwtClaimSet(Map<String, Object> claims) {
|
||||||
|
super(claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getClaims() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.jwt;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link Jwt.Builder}.
|
||||||
|
*/
|
||||||
|
public class JwtBuilderTests {
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
public void builderCanBeReused() {
|
||||||
|
final Jwt.Builder<?> tokensBuilder = Jwt.builder();
|
||||||
|
|
||||||
|
final Jwt first = tokensBuilder
|
||||||
|
.tokenValue("V1")
|
||||||
|
.header("TEST_HEADER_1", "H1")
|
||||||
|
.claim("TEST_CLAIM_1", "C1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final Jwt second = tokensBuilder
|
||||||
|
.tokenValue("V2")
|
||||||
|
.header("TEST_HEADER_1", "H2")
|
||||||
|
.header("TEST_HEADER_2", "H3")
|
||||||
|
.claim("TEST_CLAIM_1", "C2")
|
||||||
|
.claim("TEST_CLAIM_2", "C3")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(first.getHeaders()).hasSize(1);
|
||||||
|
assertThat(first.getHeaders().get("TEST_HEADER_1")).isEqualTo("H1");
|
||||||
|
assertThat(first.getClaims()).hasSize(1);
|
||||||
|
assertThat(first.getClaims().get("TEST_CLAIM_1")).isEqualTo("C1");
|
||||||
|
assertThat(first.getTokenValue()).isEqualTo("V1");
|
||||||
|
|
||||||
|
assertThat(second.getHeaders()).hasSize(2);
|
||||||
|
assertThat(second.getHeaders().get("TEST_HEADER_1")).isEqualTo("H2");
|
||||||
|
assertThat(second.getHeaders().get("TEST_HEADER_2")).isEqualTo("H3");
|
||||||
|
assertThat(second.getClaims()).hasSize(2);
|
||||||
|
assertThat(second.getClaims().get("TEST_CLAIM_1")).isEqualTo("C2");
|
||||||
|
assertThat(second.getClaims().get("TEST_CLAIM_2")).isEqualTo("C3");
|
||||||
|
assertThat(second.getTokenValue()).isEqualTo("V2");
|
||||||
|
}
|
||||||
|
}
|
@ -17,7 +17,11 @@ package org.springframework.security.oauth2.server.resource.authentication;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
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.GrantedAuthority;
|
||||||
import org.springframework.security.core.SpringSecurityCoreVersion;
|
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||||
import org.springframework.security.core.Transient;
|
import org.springframework.security.core.Transient;
|
||||||
@ -71,4 +75,73 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok
|
|||||||
public String getName() {
|
public String getName() {
|
||||||
return this.getToken().getSubject();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package sample;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Jérôme Wacongne <ch4mp@c4-soft.com>
|
||||||
|
* @since 5.2.0
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
@WebMvcTest(OAuth2ResourceServerController.class)
|
||||||
|
public class OAuth2ResourceServerControllerTests {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
MockMvc mockMvc;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
JwtDecoder jwtDecoder;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void indexGreetsAuthenticatedUser() throws Exception {
|
||||||
|
mockMvc.perform(get("/").with(jwt().name("ch4mpy")))
|
||||||
|
.andExpect(content().string(is("Hello, ch4mpy!")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void messageCanBeReadWithScopeMessageReadAuthority() throws Exception {
|
||||||
|
mockMvc.perform(get("/message").with(jwt().scopes("message:read")))
|
||||||
|
.andExpect(content().string(is("secret message")));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/message").with(jwt().authorities(new SimpleGrantedAuthority(("SCOPE_message:read")))))
|
||||||
|
.andExpect(content().string(is("secret message")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void messageCanNotBeReadWithoutScopeMessageReadAuthority() throws Exception {
|
||||||
|
mockMvc.perform(get("/message").with(jwt()))
|
||||||
|
.andExpect(status().isForbidden());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -7,6 +7,8 @@ dependencies {
|
|||||||
compile 'org.springframework:spring-test'
|
compile 'org.springframework:spring-test'
|
||||||
|
|
||||||
optional project(':spring-security-config')
|
optional project(':spring-security-config')
|
||||||
|
optional project(':spring-security-oauth2-resource-server')
|
||||||
|
optional project(':spring-security-oauth2-jose')
|
||||||
optional 'io.projectreactor:reactor-core'
|
optional 'io.projectreactor:reactor-core'
|
||||||
optional 'org.springframework:spring-webflux'
|
optional 'org.springframework:spring-webflux'
|
||||||
|
|
||||||
|
@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||||
|
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.test.support;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jérôme Wacongne <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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,11 @@
|
|||||||
|
|
||||||
package org.springframework.security.test.web.reactive.server;
|
package org.springframework.security.test.web.reactive.server;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
@ -26,6 +31,9 @@ import org.springframework.security.core.context.SecurityContext;
|
|||||||
import org.springframework.security.core.context.SecurityContextImpl;
|
import org.springframework.security.core.context.SecurityContextImpl;
|
||||||
import org.springframework.security.core.userdetails.User;
|
import org.springframework.security.core.userdetails.User;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||||
|
import org.springframework.security.test.support.JwtAuthenticationTokenTestingBuilder;
|
||||||
import org.springframework.security.web.server.csrf.CsrfWebFilter;
|
import org.springframework.security.web.server.csrf.CsrfWebFilter;
|
||||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||||
import org.springframework.test.web.reactive.server.MockServerConfigurer;
|
import org.springframework.test.web.reactive.server.MockServerConfigurer;
|
||||||
@ -35,12 +43,8 @@ import org.springframework.web.server.ServerWebExchange;
|
|||||||
import org.springframework.web.server.WebFilter;
|
import org.springframework.web.server.WebFilter;
|
||||||
import org.springframework.web.server.WebFilterChain;
|
import org.springframework.web.server.WebFilterChain;
|
||||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
import reactor.core.publisher.Mono;
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test utilities for working with Spring Security and
|
* Test utilities for working with Spring Security and
|
||||||
@ -109,6 +113,23 @@ public class SecurityMockServerConfigurers {
|
|||||||
return new UserExchangeMutator(username);
|
return new UserExchangeMutator(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the ServerWebExchange to establish a {@link SecurityContext} that has a
|
||||||
|
* {@link JwtAuthenticationToken} for the
|
||||||
|
* {@link Authentication} and a {@link Jwt} for the
|
||||||
|
* {@link Authentication#getPrincipal()}. All details are
|
||||||
|
* declarative and do not require the JWT to be valid.
|
||||||
|
*
|
||||||
|
* @return the {@link JwtMutator} to further configure or use
|
||||||
|
*/
|
||||||
|
public static JwtMutator mockJwt() {
|
||||||
|
return new JwtMutator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JwtMutator mockJwt(Consumer<Jwt.Builder<?>> jwt) {
|
||||||
|
return new JwtMutator().token(jwt);
|
||||||
|
}
|
||||||
|
|
||||||
public static CsrfMutator csrf() {
|
public static CsrfMutator csrf() {
|
||||||
return new CsrfMutator();
|
return new CsrfMutator();
|
||||||
}
|
}
|
||||||
@ -294,4 +315,31 @@ public class SecurityMockServerConfigurers {
|
|||||||
return webFilterChain.filter(exchange);
|
return webFilterChain.filter(exchange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jérôme Wacongne <ch4mp@c4-soft.com>
|
||||||
|
* @since 5.2
|
||||||
|
*/
|
||||||
|
public static class JwtMutator extends JwtAuthenticationTokenTestingBuilder<JwtMutator>
|
||||||
|
implements
|
||||||
|
WebTestClientConfigurer, MockServerConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeServerCreated(WebHttpHandlerBuilder builder) {
|
||||||
|
mockAuthentication(build()).beforeServerCreated(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConfigureAdded(WebTestClient.MockServerSpec<?> serverSpec) {
|
||||||
|
mockAuthentication(build()).afterConfigureAdded(serverSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConfigurerAdded(
|
||||||
|
WebTestClient.Builder builder,
|
||||||
|
@Nullable WebHttpHandlerBuilder httpHandlerBuilder,
|
||||||
|
@Nullable ClientHttpConnector connector) {
|
||||||
|
mockAuthentication(build()).afterConfigurerAdded(builder, httpHandlerBuilder, connector);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@ -45,7 +46,10 @@ import org.springframework.security.core.context.SecurityContext;
|
|||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.User;
|
import org.springframework.security.core.userdetails.User;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||||
import org.springframework.security.test.context.TestSecurityContextHolder;
|
import org.springframework.security.test.context.TestSecurityContextHolder;
|
||||||
|
import org.springframework.security.test.support.JwtAuthenticationTokenTestingBuilder;
|
||||||
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
|
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
|
||||||
import org.springframework.security.test.web.support.WebTestUtils;
|
import org.springframework.security.test.web.support.WebTestUtils;
|
||||||
import org.springframework.security.web.context.HttpRequestResponseHolder;
|
import org.springframework.security.web.context.HttpRequestResponseHolder;
|
||||||
@ -195,6 +199,37 @@ public final class SecurityMockMvcRequestPostProcessors {
|
|||||||
return new UserDetailsRequestPostProcessor(user);
|
return new UserDetailsRequestPostProcessor(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establish a {@link SecurityContext} that has a
|
||||||
|
* {@link JwtAuthenticationToken} for the
|
||||||
|
* {@link Authentication} and a {@link Jwt} for the
|
||||||
|
* {@link Authentication#getPrincipal()}. All details are
|
||||||
|
* declarative and do not require the JWT to be valid.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The support works by associating the authentication to the HttpServletRequest. To associate
|
||||||
|
* the request to the SecurityContextHolder you need to ensure that the
|
||||||
|
* SecurityContextPersistenceFilter is associated with the MockMvc instance. A few
|
||||||
|
* ways to do this are:
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Invoking apply {@link SecurityMockMvcConfigurers#springSecurity()}</li>
|
||||||
|
* <li>Adding Spring Security's FilterChainProxy to MockMvc</li>
|
||||||
|
* <li>Manually adding {@link SecurityContextPersistenceFilter} to the MockMvc
|
||||||
|
* instance may make sense when using MockMvcBuilders standaloneSetup</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @return the {@link JwtRequestPostProcessor} for additional customization
|
||||||
|
*/
|
||||||
|
public static JwtRequestPostProcessor jwt() {
|
||||||
|
return new JwtRequestPostProcessor();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JwtRequestPostProcessor jwt(Consumer<Jwt.Builder<?>> jwt) {
|
||||||
|
return jwt().token(jwt);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Establish a {@link SecurityContext} that uses the specified {@link Authentication}
|
* Establish a {@link SecurityContext} that uses the specified {@link Authentication}
|
||||||
* for the {@link Authentication#getPrincipal()} and a custom {@link UserDetails}. All
|
* for the {@link Authentication#getPrincipal()} and a custom {@link UserDetails}. All
|
||||||
@ -555,7 +590,7 @@ public final class SecurityMockMvcRequestPostProcessors {
|
|||||||
* Support class for {@link RequestPostProcessor}'s that establish a Spring Security
|
* Support class for {@link RequestPostProcessor}'s that establish a Spring Security
|
||||||
* context
|
* context
|
||||||
*/
|
*/
|
||||||
private static abstract class SecurityContextRequestPostProcessorSupport {
|
static class SecurityContextRequestPostProcessorSupport {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the specified {@link Authentication} into an empty
|
* Saves the specified {@link Authentication} into an empty
|
||||||
@ -564,7 +599,7 @@ public final class SecurityMockMvcRequestPostProcessors {
|
|||||||
* @param authentication the {@link Authentication} to save
|
* @param authentication the {@link Authentication} to save
|
||||||
* @param request the {@link HttpServletRequest} to use
|
* @param request the {@link HttpServletRequest} to use
|
||||||
*/
|
*/
|
||||||
final void save(Authentication authentication, HttpServletRequest request) {
|
static final void save(Authentication authentication, HttpServletRequest request) {
|
||||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||||
securityContext.setAuthentication(authentication);
|
securityContext.setAuthentication(authentication);
|
||||||
save(securityContext, request);
|
save(securityContext, request);
|
||||||
@ -576,7 +611,7 @@ public final class SecurityMockMvcRequestPostProcessors {
|
|||||||
* @param securityContext the {@link SecurityContext} to save
|
* @param securityContext the {@link SecurityContext} to save
|
||||||
* @param request the {@link HttpServletRequest} to use
|
* @param request the {@link HttpServletRequest} to use
|
||||||
*/
|
*/
|
||||||
final void save(SecurityContext securityContext, HttpServletRequest request) {
|
static final void save(SecurityContext securityContext, HttpServletRequest request) {
|
||||||
SecurityContextRepository securityContextRepository = WebTestUtils
|
SecurityContextRepository securityContextRepository = WebTestUtils
|
||||||
.getSecurityContextRepository(request);
|
.getSecurityContextRepository(request);
|
||||||
boolean isTestRepository = securityContextRepository instanceof TestSecurityContextRepository;
|
boolean isTestRepository = securityContextRepository instanceof TestSecurityContextRepository;
|
||||||
@ -604,7 +639,7 @@ public final class SecurityMockMvcRequestPostProcessors {
|
|||||||
* stateless mode
|
* stateless mode
|
||||||
*/
|
*/
|
||||||
static class TestSecurityContextRepository implements SecurityContextRepository {
|
static class TestSecurityContextRepository implements SecurityContextRepository {
|
||||||
private final static String ATTR_NAME = TestSecurityContextRepository.class
|
final static String ATTR_NAME = TestSecurityContextRepository.class
|
||||||
.getName().concat(".REPO");
|
.getName().concat(".REPO");
|
||||||
|
|
||||||
private final SecurityContextRepository delegate;
|
private final SecurityContextRepository delegate;
|
||||||
@ -716,8 +751,6 @@ public final class SecurityMockMvcRequestPostProcessors {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
|
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
|
||||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
|
||||||
context.setAuthentication(this.authentication);
|
|
||||||
save(this.authentication, request);
|
save(this.authentication, request);
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
@ -907,4 +940,20 @@ public final class SecurityMockMvcRequestPostProcessors {
|
|||||||
|
|
||||||
private SecurityMockMvcRequestPostProcessors() {
|
private SecurityMockMvcRequestPostProcessors() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jérôme Wacongne <ch4mp@c4-soft.com>
|
||||||
|
* @since 5.2
|
||||||
|
*/
|
||||||
|
public static class JwtRequestPostProcessor extends JwtAuthenticationTokenTestingBuilder<JwtRequestPostProcessor>
|
||||||
|
implements
|
||||||
|
RequestPostProcessor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
|
||||||
|
SecurityContextRequestPostProcessorSupport.save(build(), request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.test.support;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jérôme Wacongne <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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.test.support;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||||
|
import org.springframework.security.test.support.JwtAuthenticationTokenTestingBuilder.JwtTestingBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author Jérôme Wacongne <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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.test.web.reactive.server;
|
||||||
|
|
||||||
|
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockJwt;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jérôme Wacongne <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,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.test.web.reactive.server;
|
||||||
|
|
||||||
|
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter;
|
||||||
|
import org.springframework.security.web.server.csrf.CsrfWebFilter;
|
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jérôme Wacongne <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();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.test.web.servlet.request;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||||
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.JwtRequestPostProcessor;
|
||||||
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.SecurityContextRequestPostProcessorSupport.TestSecurityContextRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jérôme Wacongne <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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user