Resource Server Jwt Support

Introducing initial support for Jwt-Encoded Bearer Token authorization
with remote JWK set signature verification.

High-level features include:

- Accepting bearer tokens as headers and form or query parameters
- Verifying signatures from a remote Jwk set

And:

- A DSL for easy configuration
- A sample to demonstrate usage

Fixes: gh-5128
Fixes: gh-5125
Fixes: gh-5121
Fixes: gh-5130
Fixes: gh-5226
Fixes: gh-5237
This commit is contained in:
Josh Cummings
2018-06-12 21:33:26 -06:00
committed by Rob Winch
parent 6e67c0dcea
commit 40ccdb93f7
50 changed files with 4101 additions and 0 deletions

View File

@@ -0,0 +1,828 @@
/*
* Copyright 2002-2018 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
*
* http://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.config.annotation.web.configurers.oauth2.server.resource;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.stream.Collectors;
import javax.annotation.PreDestroy;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
/**
* Tests for {@link OAuth2ResourceServerConfigurer}
*
* @author Josh Cummings
*/
public class OAuth2ResourceServerConfigurerTests {
@Autowired
MockMvc mvc;
@Autowired(required = false)
MockWebServer authz;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@Test
public void getWhenUsingDefaultsWithValidBearerTokenThenAcceptsRequest()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ValidNoScopes");
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isOk())
.andExpect(content().string("ok"));
}
@Test
public void getWhenUsingDefaultsWithExpiredBearerTokenThenInvalidToken()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("Expired");
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Expired JWT"));
}
@Test
public void getWhenUsingDefaultsWithBadJwkEndpointThenInvalidToken()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire();
this.authz.enqueue(new MockResponse().setBody("malformed"));
String token = this.token("ValidNoScopes");
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Malformed Jwk set"));
}
@Test
public void getWhenUsingDefaultsWithUnavailableJwkEndpointThenInvalidToken()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire();
this.authz.shutdown();
String token = this.token("ValidNoScopes");
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: " +
"Couldn't retrieve remote JWK set: Connection refused (Connection refused)"));
}
@Test
public void getWhenUsingDefaultsWithMalformedBearerTokenThenInvalidToken()
throws Exception {
this.spring.register(DefaultConfig.class).autowire();
this.mvc.perform(get("/").with(bearerToken("an\"invalid\"token")))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("Bearer token is malformed"));
}
@Test
public void getWhenUsingDefaultsWithMalformedPayloadThenInvalidToken()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("MalformedPayload");
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: " +
"Malformed payload"));
}
@Test
public void getWhenUsingDefaultsWithUnsignedBearerTokenThenInvalidToken()
throws Exception {
this.spring.register(DefaultConfig.class).autowire();
String token = this.token("Unsigned");
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("Unsupported algorithm of none"));
}
@Test
public void getWhenUsingDefaultsWithBearerTokenBeforeNotBeforeThenInvalidToken()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("TooEarly");
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: " +
"JWT before use time"));
}
@Test
public void getWhenUsingDefaultsWithBearerTokenInTwoPlacesThenInvalidRequest()
throws Exception {
this.spring.register(DefaultConfig.class).autowire();
this.mvc.perform(get("/")
.with(bearerToken("token"))
.with(bearerToken("token").asParam()))
.andExpect(status().isBadRequest())
.andExpect(invalidRequestHeader("Found multiple bearer tokens in the request"));
}
@Test
public void getWhenUsingDefaultsWithBearerTokenInTwoParametersThenInvalidRequest()
throws Exception {
this.spring.register(DefaultConfig.class).autowire();
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("access_token", "token1");
params.add("access_token", "token2");
this.mvc.perform(get("/")
.params(params))
.andExpect(status().isBadRequest())
.andExpect(invalidRequestHeader("Found multiple bearer tokens in the request"));
}
@Test
public void postWhenUsingDefaultsWithBearerTokenAsFormParameterThenIgnoresToken()
throws Exception {
this.spring.register(DefaultConfig.class).autowire();
this.mvc.perform(post("/") // engage csrf
.with(bearerToken("token").asParam()))
.andExpect(status().isForbidden())
.andExpect(header().doesNotExist(HttpHeaders.WWW_AUTHENTICATE));
}
@Test
public void postWhenCsrfDisabledWithBearerTokenAsFormParameterThenIgnoresToken()
throws Exception {
this.spring.register(CsrfDisabledConfig.class).autowire();
this.mvc.perform(post("/")
.with(bearerToken("token").asParam()))
.andExpect(status().isUnauthorized())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer"));
}
@Test
public void getWhenUsingDefaultsWithNoBearerTokenThenUnauthorized()
throws Exception {
this.spring.register(DefaultConfig.class).autowire();
this.mvc.perform(get("/"))
.andExpect(status().isUnauthorized())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer"));
}
@Test
public void getWhenUsingDefaultsWithSufficientlyScopedBearerTokenThenAcceptsRequest()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ValidMessageReadScope");
this.mvc.perform(get("/requires-read-scope")
.with(bearerToken(token)))
.andExpect(status().isOk())
.andExpect(content().string("SCOPE_message:read"));
}
@Test
public void getWhenUsingDefaultsWithInsufficientScopeThenInsufficientScopeError()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ValidNoScopes");
this.mvc.perform(get("/requires-read-scope")
.with(bearerToken(token)))
.andExpect(status().isForbidden())
.andExpect(insufficientScopeHeader(""));
}
@Test
public void getWhenUsingDefaultsWithInsufficientScpThenInsufficientScopeError()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ValidMessageWriteScp");
this.mvc.perform(get("/requires-read-scope")
.with(bearerToken(token)))
.andExpect(status().isForbidden())
.andExpect(insufficientScopeHeader("message:write"));
}
@Test
public void getWhenUsingDefaultsAndAuthorizationServerHasNoMatchingKeyThenInvalidToken()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire();
this.authz.enqueue(this.jwks("Empty"));
String token = this.token("ValidNoScopes");
this.mvc.perform(get("/")
.with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: " +
"Signed JWT rejected: Another algorithm expected, or no matching key(s) found"));
}
@Test
public void getWhenUsingDefaultsAndAuthorizationServerHasMultipleMatchingKeysThenOk()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("TwoKeys"));
String token = this.token("ValidNoScopes");
this.mvc.perform(get("/authenticated")
.with(bearerToken(token)))
.andExpect(status().isOk())
.andExpect(content().string("test-subject"));
}
@Test
public void getWhenUsingDefaultsAndKeyMatchesByKidThenOk()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("TwoKeys"));
String token = this.token("Kid");
this.mvc.perform(get("/authenticated")
.with(bearerToken(token)))
.andExpect(status().isOk())
.andExpect(content().string("test-subject"));
}
// -- Method Security
@Test
public void getWhenUsingMethodSecurityWithValidBearerTokenThenAcceptsRequest()
throws Exception {
this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ValidMessageReadScope");
this.mvc.perform(get("/ms-requires-read-scope")
.with(bearerToken(token)))
.andExpect(status().isOk())
.andExpect(content().string("SCOPE_message:read"));
}
@Test
public void getWhenUsingMethodSecurityWithValidBearerTokenHavingScpAttributeThenAcceptsRequest()
throws Exception {
this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ValidMessageReadScp");
this.mvc.perform(get("/ms-requires-read-scope")
.with(bearerToken(token)))
.andExpect(status().isOk())
.andExpect(content().string("SCOPE_message:read"));
}
@Test
public void getWhenUsingMethodSecurityWithInsufficientScopeThenInsufficientScopeError()
throws Exception {
this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ValidNoScopes");
this.mvc.perform(get("/ms-requires-read-scope")
.with(bearerToken(token)))
.andExpect(status().isForbidden())
.andExpect(insufficientScopeHeader(""));
}
@Test
public void getWhenUsingMethodSecurityWithInsufficientScpThenInsufficientScopeError()
throws Exception {
this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ValidMessageWriteScp");
this.mvc.perform(get("/ms-requires-read-scope")
.with(bearerToken(token)))
.andExpect(status().isForbidden())
.andExpect(insufficientScopeHeader("message:write"));
}
@Test
public void getWhenUsingMethodSecurityWithDenyAllThenInsufficientScopeError()
throws Exception {
this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ValidMessageReadScope");
this.mvc.perform(get("/ms-deny")
.with(bearerToken(token)))
.andExpect(status().isForbidden())
.andExpect(insufficientScopeHeader("message:read"));
}
// -- Resource Server should not engage csrf
@Test
public void postWhenUsingDefaultsWithValidBearerTokenAndNoCsrfTokenThenOk()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ValidNoScopes");
this.mvc.perform(post("/authenticated")
.with(bearerToken(token)))
.andExpect(status().isOk())
.andExpect(content().string("test-subject"));
}
@Test
public void postWhenUsingDefaultsWithNoBearerTokenThenCsrfDenies()
throws Exception {
this.spring.register(DefaultConfig.class).autowire();
this.mvc.perform(post("/authenticated"))
.andExpect(status().isForbidden())
.andExpect(header().doesNotExist(HttpHeaders.WWW_AUTHENTICATE));
}
@Test
public void postWhenUsingDefaultsWithExpiredBearerTokenAndNoCsrfThenInvalidToken()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("Expired");
this.mvc.perform(post("/authenticated")
.with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Expired JWT"));
}
// -- Resource Server should not create sessions
@Test
public void requestWhenDefaultConfiguredThenSessionIsNotCreated()
throws Exception {
this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ValidNoScopes");
MvcResult result = this.mvc.perform(get("/")
.with(bearerToken(token)))
.andExpect(status().isOk())
.andReturn();
assertThat(result.getRequest().getSession(false)).isNull();
}
@Test
public void requestWhenUsingDefaultsAndNoBearerTokenThenSessionIsNotCreated()
throws Exception {
this.spring.register(DefaultConfig.class, BasicController.class).autowire();
MvcResult result = this.mvc.perform(get("/"))
.andExpect(status().isUnauthorized())
.andReturn();
assertThat(result.getRequest().getSession(false)).isNull();
}
@Test
public void requestWhenSessionManagementConfiguredThenUserConfigurationOverrides()
throws Exception {
this.spring.register(WebServerConfig.class, AlwaysSessionCreationConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ValidNoScopes");
MvcResult result = this.mvc.perform(get("/")
.with(bearerToken(token)))
.andExpect(status().isOk())
.andReturn();
assertThat(result.getRequest().getSession(false)).isNotNull();
}
// -- In combination with other authentication providers
@Test
public void getWhenAlsoUsingHttpBasicThenCorrectProviderEngages()
throws Exception {
this.spring.register(WebServerConfig.class, BasicAndResourceServerConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ValidNoScopes");
this.mvc.perform(get("/authenticated")
.with(bearerToken(token)))
.andExpect(status().isOk())
.andExpect(content().string("test-subject"));
this.mvc.perform(get("/authenticated")
.with(httpBasic("basic-user", "basic-password")))
.andExpect(status().isOk())
.andExpect(content().string("basic-user"));
}
// -- Incorrect Configuration
@Test
public void configuredWhenMissingJwtAuthenticationProviderThenWiringException() {
assertThatCode(() -> this.spring.register(JwtlessConfig.class).autowire())
.isInstanceOf(BeanCreationException.class)
.hasMessageContaining("no instance of JwtDecoder");
}
@Test
public void configureWhenMissingJwkSetUriThenWiringException() {
assertThatCode(() -> this.spring.register(JwtHalfConfiguredConfig.class).autowire())
.isInstanceOf(BeanCreationException.class)
.hasMessageContaining("no instance of JwtDecoder");
}
// -- support
@EnableWebSecurity
static class DefaultConfig extends WebSecurityConfigurerAdapter {
@Value("${mock.jwk-set-uri:https://example.org}") String uri;
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
.antMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')")
.anyRequest().authenticated()
.and()
.oauth2()
.resourceServer()
.jwt()
.jwkSetUri(this.uri);
// @formatter:on
}
}
@EnableWebSecurity
static class CsrfDisabledConfig extends WebSecurityConfigurerAdapter {
@Value("${mock.jwk-set-uri:https://example.org}") String uri;
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
.antMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')")
.anyRequest().authenticated()
.and()
.csrf().disable()
.oauth2()
.resourceServer()
.jwt()
.jwkSetUri(this.uri);
// @formatter:on
}
}
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
static class MethodSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${mock.jwk-set-uri:https://example.org}") String uri;
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2()
.resourceServer()
.jwt()
.jwkSetUri(this.uri);
// @formatter:on
}
}
@EnableWebSecurity
static class JwtlessConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2()
.resourceServer();
// @formatter:on
}
}
@EnableWebSecurity
static class BasicAndResourceServerConfig extends WebSecurityConfigurerAdapter {
@Value("${mock.jwk-set-uri:https://example.org}") String uri;
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.oauth2()
.resourceServer()
.jwt()
.jwkSetUri(this.uri);
// @formatter:on
}
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(
org.springframework.security.core.userdetails.User.withDefaultPasswordEncoder()
.username("basic-user")
.password("basic-password")
.roles("USER")
.build());
}
}
@EnableWebSecurity
static class JwtHalfConfiguredConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2()
.resourceServer()
.jwt(); // missing key configuration, e.g. jwkSetUri
// @formatter:on
}
}
@EnableWebSecurity
static class AlwaysSessionCreationConfig extends WebSecurityConfigurerAdapter {
@Value("${mock.jwk-set-uri}") String uri;
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.and()
.oauth2()
.resourceServer()
.jwt()
.jwkSetUri(this.uri);
// @formatter:on
}
}
@RestController
static class BasicController {
@GetMapping("/")
public String get() {
return "ok";
}
@PostMapping("/post")
public String post() {
return "post";
}
@RequestMapping(value = "/authenticated", method = { GET, POST })
public String authenticated(@AuthenticationPrincipal Authentication authentication) {
return authentication.getName();
}
@GetMapping("/requires-read-scope")
public String requiresReadScope(@AuthenticationPrincipal JwtAuthenticationToken token) {
return token.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.filter(auth -> auth.endsWith("message:read"))
.findFirst().orElse(null);
}
@GetMapping("/ms-requires-read-scope")
@PreAuthorize("hasAuthority('SCOPE_message:read')")
public String msRequiresReadScope(@AuthenticationPrincipal JwtAuthenticationToken token) {
return requiresReadScope(token);
}
@GetMapping("/ms-deny")
@PreAuthorize("denyAll")
public String deny() {
return "hmm, that's odd";
}
}
@Configuration
static class WebServerConfig implements BeanPostProcessor {
private final MockWebServer server = new MockWebServer();
@PreDestroy
public void shutdown() throws IOException {
this.server.shutdown();
}
@Bean
public MockWebServer authz() {
return this.server;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebSecurityConfigurerAdapter) {
Field f = ReflectionUtils.findField(bean.getClass(), field ->
field.getAnnotation(Value.class) != null);
if (f != null) {
ReflectionUtils.setField(f, bean, this.server.url("/.well-known/jwks.json").toString());
}
}
return null;
}
}
private static class BearerTokenRequestPostProcessor implements RequestPostProcessor {
private boolean asRequestParameter;
private String token;
public BearerTokenRequestPostProcessor(String token) {
this.token = token;
}
public BearerTokenRequestPostProcessor asParam() {
this.asRequestParameter = true;
return this;
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
if (this.asRequestParameter) {
request.setParameter("access_token", this.token);
} else {
request.addHeader("Authorization", "Bearer " + this.token);
}
return request;
}
}
private static BearerTokenRequestPostProcessor bearerToken(String token) {
return new BearerTokenRequestPostProcessor(token);
}
private static ResultMatcher invalidRequestHeader(String message) {
return header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer " +
"error=\"invalid_request\", " +
"error_description=\"" + message + "\", " +
"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"");
}
private static ResultMatcher invalidTokenHeader(String message) {
return header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer " +
"error=\"invalid_token\", " +
"error_description=\"" + message + "\", " +
"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"");
}
private static ResultMatcher insufficientScopeHeader(String scope) {
return header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer " +
"error=\"insufficient_scope\"" +
", error_description=\"The token provided has insufficient scope [" + scope + "] for this request\"" +
", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"" +
(StringUtils.hasText(scope) ? ", scope=\"" + scope + "\"" : ""));
}
private String token(String name) throws IOException {
return resource(name + ".token");
}
private MockResponse jwks(String name) throws IOException {
String response = resource(name + ".jwks");
return new MockResponse()
.setResponseCode(200)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setBody(response);
}
private String resource(String suffix) throws IOException {
String name = this.getClass().getSimpleName() + "-" + suffix;
ClassPathResource resource = new ClassPathResource(name, this.getClass());
try ( BufferedReader reader = new BufferedReader(new FileReader(resource.getFile())) ) {
return reader.lines().collect(Collectors.joining());
}
}
}