-
Notifications
You must be signed in to change notification settings - Fork 6k
mockJwt() flow API for reactive server #6748
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5573d72
62a0c9a
b8bac2d
b84c5d7
9ec2d9f
ba9372b
aa8dba3
7547c4c
3f90dce
635ec22
959df0a
15e1446
89958e7
3b45f9f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,13 +15,19 @@ | |
*/ | ||
package org.springframework.security.oauth2.jwt; | ||
|
||
import org.springframework.security.oauth2.core.AbstractOAuth2Token; | ||
import org.springframework.util.Assert; | ||
|
||
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 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). | ||
|
@@ -41,6 +47,8 @@ | |
* @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; | ||
|
||
|
@@ -80,4 +88,139 @@ public Map<String, Object> getHeaders() { | |
public Map<String, Object> getClaims() { | ||
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() { | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please make the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
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; | ||
} | ||
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,7 +17,11 @@ | |
|
||
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; | ||
|
@@ -71,4 +75,73 @@ public Map<String, Object> getTokenAttributes() { | |
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>> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The boarder utility of this class isn't apparent to me. Let's expose this functionality in |
||
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't the user just do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as explained in a comment above, "name" is a property of |
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's take this out as the user can already do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this method is
|
||
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; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add
@since 5.2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done