Skip to content

Commit e59d8a5

Browse files
ch4mpyjzheaux
authored andcommitted
Mock Jwt Test Support and Jwt.Builder
Fixes: gh-6634 Fixes: gh-6851
1 parent f699854 commit e59d8a5

File tree

13 files changed

+948
-14
lines changed

13 files changed

+948
-14
lines changed

oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/Jwt.java

+146-3
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@
1515
*/
1616
package org.springframework.security.oauth2.jwt;
1717

18-
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
19-
import org.springframework.util.Assert;
20-
18+
import java.net.URL;
2119
import java.time.Instant;
20+
import java.util.Collection;
2221
import java.util.Collections;
22+
import java.util.HashMap;
2323
import java.util.LinkedHashMap;
2424
import java.util.Map;
25+
import java.util.stream.Collectors;
26+
import java.util.stream.Stream;
27+
28+
import org.springframework.security.core.SpringSecurityCoreVersion;
29+
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
30+
import org.springframework.util.Assert;
2531

2632
/**
2733
* An implementation of an {@link AbstractOAuth2Token} representing a JSON Web Token (JWT).
@@ -41,6 +47,8 @@
4147
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516">JSON Web Encryption (JWE)</a>
4248
*/
4349
public class Jwt extends AbstractOAuth2Token implements JwtClaimAccessor {
50+
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
51+
4452
private final Map<String, Object> headers;
4553
private final Map<String, Object> claims;
4654

@@ -80,4 +88,139 @@ public Map<String, Object> getHeaders() {
8088
public Map<String, Object> getClaims() {
8189
return this.claims;
8290
}
91+
92+
public static Builder<?> builder() {
93+
return new Builder<>();
94+
}
95+
96+
/**
97+
* Helps configure a {@link Jwt}
98+
*
99+
* @author Jérôme Wacongne &lt;ch4mp&#64;c4-soft.com&gt;
100+
*/
101+
public static class Builder<T extends Builder<T>> {
102+
protected String tokenValue;
103+
protected final Map<String, Object> claims = new HashMap<>();
104+
protected final Map<String, Object> headers = new HashMap<>();
105+
106+
protected Builder() {
107+
}
108+
109+
public T tokenValue(String tokenValue) {
110+
this.tokenValue = tokenValue;
111+
return downcast();
112+
}
113+
114+
public T claim(String name, Object value) {
115+
this.claims.put(name, value);
116+
return downcast();
117+
}
118+
119+
public T clearClaims(Map<String, Object> claims) {
120+
this.claims.clear();
121+
return downcast();
122+
}
123+
124+
/**
125+
* Adds to existing claims (does not replace existing ones)
126+
* @param claims claims to add
127+
* @return this builder to further configure
128+
*/
129+
public T claims(Map<String, Object> claims) {
130+
this.claims.putAll(claims);
131+
return downcast();
132+
}
133+
134+
public T header(String name, Object value) {
135+
this.headers.put(name, value);
136+
return downcast();
137+
}
138+
139+
public T clearHeaders(Map<String, Object> headers) {
140+
this.headers.clear();
141+
return downcast();
142+
}
143+
144+
/**
145+
* Adds to existing headers (does not replace existing ones)
146+
* @param headers headers to add
147+
* @return this builder to further configure
148+
*/
149+
public T headers(Map<String, Object> headers) {
150+
headers.entrySet().stream().forEach(e -> this.header(e.getKey(), e.getValue()));
151+
return downcast();
152+
}
153+
154+
public Jwt build() {
155+
final JwtClaimSet claimSet = new JwtClaimSet(claims);
156+
return new Jwt(
157+
this.tokenValue,
158+
claimSet.getClaimAsInstant(JwtClaimNames.IAT),
159+
claimSet.getClaimAsInstant(JwtClaimNames.EXP),
160+
this.headers,
161+
claimSet);
162+
}
163+
164+
public T audience(Stream<String> audience) {
165+
this.claim(JwtClaimNames.AUD, audience.collect(Collectors.toList()));
166+
return downcast();
167+
}
168+
169+
public T audience(Collection<String> audience) {
170+
return audience(audience.stream());
171+
}
172+
173+
public T audience(String... audience) {
174+
return audience(Stream.of(audience));
175+
}
176+
177+
public T expiresAt(Instant expiresAt) {
178+
this.claim(JwtClaimNames.EXP, expiresAt.getEpochSecond());
179+
return downcast();
180+
}
181+
182+
public T jti(String jti) {
183+
this.claim(JwtClaimNames.JTI, jti);
184+
return downcast();
185+
}
186+
187+
public T issuedAt(Instant issuedAt) {
188+
this.claim(JwtClaimNames.IAT, issuedAt.getEpochSecond());
189+
return downcast();
190+
}
191+
192+
public T issuer(URL issuer) {
193+
this.claim(JwtClaimNames.ISS, issuer.toExternalForm());
194+
return downcast();
195+
}
196+
197+
public T notBefore(Instant notBefore) {
198+
this.claim(JwtClaimNames.NBF, notBefore.getEpochSecond());
199+
return downcast();
200+
}
201+
202+
public T subject(String subject) {
203+
this.claim(JwtClaimNames.SUB, subject);
204+
return downcast();
205+
}
206+
207+
@SuppressWarnings("unchecked")
208+
protected T downcast() {
209+
return (T) this;
210+
}
211+
}
212+
213+
private static final class JwtClaimSet extends HashMap<String, Object> implements JwtClaimAccessor {
214+
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
215+
216+
public JwtClaimSet(Map<String, Object> claims) {
217+
super(claims);
218+
}
219+
220+
@Override
221+
public Map<String, Object> getClaims() {
222+
return this;
223+
}
224+
225+
}
83226
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.jwt;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import org.junit.Test;
21+
22+
/**
23+
* Tests for {@link Jwt.Builder}.
24+
*/
25+
public class JwtBuilderTests {
26+
27+
@Test()
28+
public void builderCanBeReused() {
29+
final Jwt.Builder<?> tokensBuilder = Jwt.builder();
30+
31+
final Jwt first = tokensBuilder
32+
.tokenValue("V1")
33+
.header("TEST_HEADER_1", "H1")
34+
.claim("TEST_CLAIM_1", "C1")
35+
.build();
36+
37+
final Jwt second = tokensBuilder
38+
.tokenValue("V2")
39+
.header("TEST_HEADER_1", "H2")
40+
.header("TEST_HEADER_2", "H3")
41+
.claim("TEST_CLAIM_1", "C2")
42+
.claim("TEST_CLAIM_2", "C3")
43+
.build();
44+
45+
assertThat(first.getHeaders()).hasSize(1);
46+
assertThat(first.getHeaders().get("TEST_HEADER_1")).isEqualTo("H1");
47+
assertThat(first.getClaims()).hasSize(1);
48+
assertThat(first.getClaims().get("TEST_CLAIM_1")).isEqualTo("C1");
49+
assertThat(first.getTokenValue()).isEqualTo("V1");
50+
51+
assertThat(second.getHeaders()).hasSize(2);
52+
assertThat(second.getHeaders().get("TEST_HEADER_1")).isEqualTo("H2");
53+
assertThat(second.getHeaders().get("TEST_HEADER_2")).isEqualTo("H3");
54+
assertThat(second.getClaims()).hasSize(2);
55+
assertThat(second.getClaims().get("TEST_CLAIM_1")).isEqualTo("C2");
56+
assertThat(second.getClaims().get("TEST_CLAIM_2")).isEqualTo("C3");
57+
assertThat(second.getTokenValue()).isEqualTo("V2");
58+
}
59+
}

oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java

+73
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717

1818
import java.util.Collection;
1919
import java.util.Map;
20+
import java.util.function.Consumer;
21+
import java.util.stream.Collectors;
22+
import java.util.stream.Stream;
2023

24+
import org.springframework.core.convert.converter.Converter;
2125
import org.springframework.security.core.GrantedAuthority;
2226
import org.springframework.security.core.SpringSecurityCoreVersion;
2327
import org.springframework.security.core.Transient;
@@ -71,4 +75,73 @@ public Map<String, Object> getTokenAttributes() {
7175
public String getName() {
7276
return this.getToken().getSubject();
7377
}
78+
79+
public static Builder<?> builder(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
80+
return new Builder<>(Jwt.builder(), authoritiesConverter);
81+
}
82+
83+
public static Builder<?> builder() {
84+
return builder(new JwtGrantedAuthoritiesConverter());
85+
}
86+
87+
/**
88+
* Helps configure a {@link JwtAuthenticationToken}
89+
*
90+
* @author Jérôme Wacongne &lt;ch4mp&#64;c4-soft.com&gt;
91+
* @since 5.2
92+
*/
93+
public static class Builder<T extends Builder<T>> {
94+
95+
private Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter;
96+
97+
private final Jwt.Builder<?> jwt;
98+
99+
protected Builder(Jwt.Builder<?> principalBuilder, Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
100+
this.authoritiesConverter = authoritiesConverter;
101+
this.jwt = principalBuilder;
102+
}
103+
104+
public T authoritiesConverter(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
105+
this.authoritiesConverter = authoritiesConverter;
106+
return downcast();
107+
}
108+
109+
public T token(Consumer<Jwt.Builder<?>> jwtBuilderConsumer) {
110+
jwtBuilderConsumer.accept(jwt);
111+
return downcast();
112+
}
113+
114+
public T name(String name) {
115+
jwt.subject(name);
116+
return downcast();
117+
}
118+
119+
/**
120+
* Shortcut to set "scope" claim with a space separated string containing provided scope collection
121+
* @param scopes strings to join with spaces and set as "scope" claim
122+
* @return this builder to further configure
123+
*/
124+
public T scopes(String... scopes) {
125+
jwt.claim("scope", Stream.of(scopes).collect(Collectors.joining(" ")));
126+
return downcast();
127+
}
128+
129+
public JwtAuthenticationToken build() {
130+
final Jwt token = jwt.build();
131+
return new JwtAuthenticationToken(token, getAuthorities(token));
132+
}
133+
134+
protected Jwt getToken() {
135+
return jwt.build();
136+
}
137+
138+
protected Collection<GrantedAuthority> getAuthorities(Jwt token) {
139+
return authoritiesConverter.convert(token);
140+
}
141+
142+
@SuppressWarnings("unchecked")
143+
protected T downcast() {
144+
return (T) this;
145+
}
146+
}
74147
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package sample;
17+
18+
import static org.hamcrest.CoreMatchers.is;
19+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
20+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
21+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
22+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
23+
24+
import org.junit.Test;
25+
import org.junit.runner.RunWith;
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
28+
import org.springframework.boot.test.mock.mockito.MockBean;
29+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
30+
import org.springframework.security.oauth2.jwt.JwtDecoder;
31+
import org.springframework.test.context.junit4.SpringRunner;
32+
import org.springframework.test.web.servlet.MockMvc;
33+
34+
/**
35+
*
36+
* @author Jérôme Wacongne &lt;[email protected]&gt;
37+
* @since 5.2.0
38+
*
39+
*/
40+
@RunWith(SpringRunner.class)
41+
@WebMvcTest(OAuth2ResourceServerController.class)
42+
public class OAuth2ResourceServerControllerTests {
43+
44+
@Autowired
45+
MockMvc mockMvc;
46+
47+
@MockBean
48+
JwtDecoder jwtDecoder;
49+
50+
@Test
51+
public void indexGreetsAuthenticatedUser() throws Exception {
52+
mockMvc.perform(get("/").with(jwt().name("ch4mpy")))
53+
.andExpect(content().string(is("Hello, ch4mpy!")));
54+
}
55+
56+
@Test
57+
public void messageCanBeReadWithScopeMessageReadAuthority() throws Exception {
58+
mockMvc.perform(get("/message").with(jwt().scopes("message:read")))
59+
.andExpect(content().string(is("secret message")));
60+
61+
mockMvc.perform(get("/message").with(jwt().authorities(new SimpleGrantedAuthority(("SCOPE_message:read")))))
62+
.andExpect(content().string(is("secret message")));
63+
}
64+
65+
@Test
66+
public void messageCanNotBeReadWithoutScopeMessageReadAuthority() throws Exception {
67+
mockMvc.perform(get("/message").with(jwt()))
68+
.andExpect(status().isForbidden());
69+
}
70+
71+
}

test/spring-security-test.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ dependencies {
77
compile 'org.springframework:spring-test'
88

99
optional project(':spring-security-config')
10+
optional project(':spring-security-oauth2-resource-server')
11+
optional project(':spring-security-oauth2-jose')
1012
optional 'io.projectreactor:reactor-core'
1113
optional 'org.springframework:spring-webflux'
1214

0 commit comments

Comments
 (0)