Skip to content

Commit

Permalink
Mock Jwt Test Support and Jwt.Builder
Browse files Browse the repository at this point in the history
Fixes: gh-6634
Fixes: gh-6851
  • Loading branch information
ch4mpy authored and jzheaux committed May 22, 2019
1 parent f699854 commit e59d8a5
Show file tree
Hide file tree
Showing 13 changed files with 948 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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;

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

}
}
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
Expand Up @@ -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;
Expand Down Expand Up @@ -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 &lt;ch4mp&#64;c4-soft.com&gt;
* @since 5.2
*/
public static class Builder<T extends Builder<T>> {

private Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter;

private final Jwt.Builder<?> jwt;

protected Builder(Jwt.Builder<?> principalBuilder, Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
this.authoritiesConverter = authoritiesConverter;
this.jwt = principalBuilder;
}

public T authoritiesConverter(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
this.authoritiesConverter = authoritiesConverter;
return downcast();
}

public T token(Consumer<Jwt.Builder<?>> jwtBuilderConsumer) {
jwtBuilderConsumer.accept(jwt);
return downcast();
}

public T name(String name) {
jwt.subject(name);
return downcast();
}

/**
* Shortcut to set "scope" claim with a space separated string containing provided scope collection
* @param scopes strings to join with spaces and set as "scope" claim
* @return this builder to further configure
*/
public T scopes(String... scopes) {
jwt.claim("scope", Stream.of(scopes).collect(Collectors.joining(" ")));
return downcast();
}

public JwtAuthenticationToken build() {
final Jwt token = jwt.build();
return new JwtAuthenticationToken(token, getAuthorities(token));
}

protected Jwt getToken() {
return jwt.build();
}

protected Collection<GrantedAuthority> getAuthorities(Jwt token) {
return authoritiesConverter.convert(token);
}

@SuppressWarnings("unchecked")
protected T downcast() {
return (T) this;
}
}
}
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 &lt;ch4mp@c4-soft.com&gt;
* @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());
}

}
2 changes: 2 additions & 0 deletions test/spring-security-test.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +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 'io.projectreactor:reactor-core'
optional 'org.springframework:spring-webflux'

Expand Down
Loading

0 comments on commit e59d8a5

Please sign in to comment.