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:
+29
@@ -21,6 +21,7 @@ import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
|
||||
/**
|
||||
* An {@link AbstractHttpConfigurer} that provides support for the
|
||||
@@ -40,6 +41,8 @@ public final class OAuth2Configurer<B extends HttpSecurityBuilder<B>>
|
||||
|
||||
private OAuth2ClientConfigurer<B> clientConfigurer;
|
||||
|
||||
private OAuth2ResourceServerConfigurer<B> resourceServerConfigurer;
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2ClientConfigurer} for configuring OAuth 2.0 Client support.
|
||||
*
|
||||
@@ -52,11 +55,27 @@ public final class OAuth2Configurer<B extends HttpSecurityBuilder<B>>
|
||||
return this.clientConfigurer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2ResourceServerConfigurer} for configuring OAuth 2.0 Resource Server support.
|
||||
*
|
||||
* @return the {@link OAuth2ResourceServerConfigurer}
|
||||
*/
|
||||
public OAuth2ResourceServerConfigurer<B> resourceServer() {
|
||||
if (this.resourceServerConfigurer == null) {
|
||||
this.initResourceServerConfigurer();
|
||||
}
|
||||
return this.resourceServerConfigurer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(B builder) throws Exception {
|
||||
if (this.clientConfigurer != null) {
|
||||
this.clientConfigurer.init(builder);
|
||||
}
|
||||
|
||||
if (this.resourceServerConfigurer != null) {
|
||||
this.resourceServerConfigurer.init(builder);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -64,6 +83,10 @@ public final class OAuth2Configurer<B extends HttpSecurityBuilder<B>>
|
||||
if (this.clientConfigurer != null) {
|
||||
this.clientConfigurer.configure(builder);
|
||||
}
|
||||
|
||||
if (this.resourceServerConfigurer != null) {
|
||||
this.resourceServerConfigurer.configure(builder);
|
||||
}
|
||||
}
|
||||
|
||||
private void initClientConfigurer() {
|
||||
@@ -71,4 +94,10 @@ public final class OAuth2Configurer<B extends HttpSecurityBuilder<B>>
|
||||
this.clientConfigurer.setBuilder(this.getBuilder());
|
||||
this.clientConfigurer.addObjectPostProcessor(this.objectPostProcessor);
|
||||
}
|
||||
|
||||
private void initResourceServerConfigurer() {
|
||||
this.resourceServerConfigurer = new OAuth2ResourceServerConfigurer<>();
|
||||
this.resourceServerConfigurer.setBuilder(this.getBuilder());
|
||||
this.resourceServerConfigurer.addObjectPostProcessor(this.objectPostProcessor);
|
||||
}
|
||||
}
|
||||
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* 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 javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
||||
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
*
|
||||
* An {@link AbstractHttpConfigurer} for OAuth 2.0 Resource Server Support.
|
||||
*
|
||||
* By default, this wires a {@link BearerTokenAuthenticationFilter}, which can be used to parse the request
|
||||
* for bearer tokens and make an authentication attempt.
|
||||
*
|
||||
* <p>
|
||||
* The following configuration options are available:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #jwt()} - enables Jwt-encoded bearer token support</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* When using {@link #jwt()}, a Jwk Set Uri must be supplied via {@link JwtConfigurer#jwkSetUri}
|
||||
*
|
||||
* <h2>Security Filters</h2>
|
||||
*
|
||||
* The following {@code Filter}s are populated when {@link #jwt()} is configured:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link BearerTokenAuthenticationFilter}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Shared Objects Created</h2>
|
||||
*
|
||||
* The following shared objects are populated:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link SessionCreationPolicy} (optional)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Shared Objects Used</h2>
|
||||
*
|
||||
* The following shared objects are used:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link AuthenticationManager}</li>
|
||||
* </ul>
|
||||
*
|
||||
* If {@link #jwt()} isn't supplied, then the {@link BearerTokenAuthenticationFilter} is still added, but without
|
||||
* any OAuth 2.0 {@link AuthenticationProvider}s. This is useful if needing to switch out Spring Security's Jwt support
|
||||
* for a custom one.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.1
|
||||
* @see BearerTokenAuthenticationFilter
|
||||
* @see JwtAuthenticationProvider
|
||||
* @see NimbusJwtDecoderJwkSupport
|
||||
* @see AbstractHttpConfigurer
|
||||
*/
|
||||
public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<H>> extends
|
||||
AbstractHttpConfigurer<OAuth2ResourceServerConfigurer<H>, H> {
|
||||
|
||||
private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
|
||||
private BearerTokenRequestMatcher requestMatcher = new BearerTokenRequestMatcher();
|
||||
|
||||
private BearerTokenAuthenticationEntryPoint authenticationEntryPoint
|
||||
= new BearerTokenAuthenticationEntryPoint();
|
||||
|
||||
private BearerTokenAccessDeniedHandler accessDeniedHandler
|
||||
= new BearerTokenAccessDeniedHandler();
|
||||
|
||||
private JwtConfigurer jwtConfigurer = new JwtConfigurer();
|
||||
|
||||
public JwtConfigurer jwt() {
|
||||
return this.jwtConfigurer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBuilder(H http) {
|
||||
super.setBuilder(http);
|
||||
initSessionCreationPolicy(http);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(H http) throws Exception {
|
||||
registerDefaultDeniedHandler(http);
|
||||
registerDefaultEntryPoint(http);
|
||||
registerDefaultCsrfOverride(http);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(H http) throws Exception {
|
||||
BearerTokenResolver bearerTokenResolver = getBearerTokenResolver();
|
||||
this.requestMatcher.setBearerTokenResolver(bearerTokenResolver);
|
||||
|
||||
AuthenticationManager manager = http.getSharedObject(AuthenticationManager.class);
|
||||
|
||||
BearerTokenAuthenticationFilter filter =
|
||||
new BearerTokenAuthenticationFilter(manager);
|
||||
filter.setBearerTokenResolver(bearerTokenResolver);
|
||||
filter = postProcess(filter);
|
||||
|
||||
http.addFilterBefore(filter, BasicAuthenticationFilter.class);
|
||||
|
||||
JwtDecoder decoder = this.jwtConfigurer.getJwtDecoder();
|
||||
|
||||
if (decoder != null) {
|
||||
JwtAuthenticationProvider provider =
|
||||
new JwtAuthenticationProvider(decoder);
|
||||
provider = postProcess(provider);
|
||||
|
||||
http.authenticationProvider(provider);
|
||||
} else {
|
||||
throw new IllegalStateException("Jwt is the only supported format for bearer tokens " +
|
||||
"in Spring Security and no instance of JwtDecoder could be found. Make sure to specify " +
|
||||
"a jwk set uri by doing http.oauth2().resourceServer().jwt().jwkSetUri(uri)");
|
||||
}
|
||||
}
|
||||
|
||||
public class JwtConfigurer {
|
||||
private JwtDecoder decoder;
|
||||
|
||||
private JwtConfigurer() {}
|
||||
|
||||
public OAuth2ResourceServerConfigurer<H> jwkSetUri(String uri) {
|
||||
this.decoder = new NimbusJwtDecoderJwkSupport(uri);
|
||||
return OAuth2ResourceServerConfigurer.this;
|
||||
}
|
||||
|
||||
private JwtDecoder getJwtDecoder() {
|
||||
return this.decoder;
|
||||
}
|
||||
}
|
||||
|
||||
private void initSessionCreationPolicy(H http) {
|
||||
if (http.getSharedObject(SessionCreationPolicy.class) == null) {
|
||||
http.setSharedObject(SessionCreationPolicy.class, SessionCreationPolicy.STATELESS);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerDefaultDeniedHandler(H http) {
|
||||
ExceptionHandlingConfigurer<H> exceptionHandling = http
|
||||
.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptionHandling == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
exceptionHandling.defaultAccessDeniedHandlerFor(
|
||||
this.accessDeniedHandler,
|
||||
this.requestMatcher);
|
||||
}
|
||||
|
||||
private void registerDefaultEntryPoint(H http) {
|
||||
ExceptionHandlingConfigurer<H> exceptionHandling = http
|
||||
.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptionHandling == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor(
|
||||
this.authenticationEntryPoint,
|
||||
this.requestMatcher);
|
||||
}
|
||||
|
||||
private void registerDefaultCsrfOverride(H http) {
|
||||
CsrfConfigurer<H> csrf = http
|
||||
.getConfigurer(CsrfConfigurer.class);
|
||||
if (csrf == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
csrf.ignoringRequestMatchers(this.requestMatcher);
|
||||
}
|
||||
|
||||
private BearerTokenResolver getBearerTokenResolver() {
|
||||
return this.bearerTokenResolver;
|
||||
}
|
||||
|
||||
private static final class BearerTokenRequestMatcher implements RequestMatcher {
|
||||
private BearerTokenResolver bearerTokenResolver
|
||||
= new DefaultBearerTokenResolver();
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
return this.bearerTokenResolver.resolve(request) != null;
|
||||
}
|
||||
|
||||
public void setBearerTokenResolver(BearerTokenResolver tokenResolver) {
|
||||
Assert.notNull(tokenResolver, "resolver cannot be null");
|
||||
this.bearerTokenResolver = tokenResolver;
|
||||
}
|
||||
}
|
||||
}
|
||||
+828
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"keys":[{"p":"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M","kty":"RSA","q":"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E","d":"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ","e":"AQAB","use":"sig","kid":"one","qi":"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4","dp":"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0","dq":"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE","n":"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw"}]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"keys":[]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE1MzAyMzE3MTB9.c8vXYFwe1cBuglaZbmZFXJOmLsu_IQf-OsOiiOGhEJYOzu6h6v_qEzf2xxbu5TSvwAERmDITUSK41UIIvgU75WebtgilNnTR83B_gPM-7_FI2FLzlgVH7WayzvbYTQqepE_ZUMLFkGkK4r-dRiOyB9_cfl6jq_b5hE_biH1qrgPQrjlEhU8YxeK2EE05wsARLzyjoIYifkStjPC6rC-MLFIVk5JoITNzkTh7zYYSWtKWEgwd8S_vluVtJaPk-yKPb4tXcFRzCFl_qd7aCF8_LHyhw-4wvhWRIi8DmQmRU_a1RxR0mi-UCp0jMwmBZxxkSdqJ4l_EHI1yVqpgnbMLDw
|
||||
+1
@@ -0,0 +1 @@
|
||||
eyJraWQiOiJvbmUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjQ2ODM4ODM1NzJ9.UhukjNEowC5lLCccvdjCUJad5J9FGNModegMZGe9qKIbXxmfseTttZUNn3_K_6aNCfimtmRktCRbw3fUTcje2TFJOJ6SmomLcQyjq7S41Wq6oBSA2fdqOOU4vNvrk8_pSExsSyN9bfWiJ51I8Agzbq5eUDNo_HEpaJZimrIe9f2_njU1GxvAWsq_h4UhHEgPPb3kY9kN9hVYX_oShhh7JxbLJBnfsKBOKGEWOsE65GlmDgQV4om6RGjJaz6jFHKJTCpH08ADA3j2dqT0LNy4PrUmbnjPjWVtSQJkGcgUkcQW6qz0K86ZfJZZng_iB2VadRm5qO-99ySKmlxa5A-_Iw
|
||||
+1
@@ -0,0 +1 @@
|
||||
eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODM4OTEyMTl9.kpdv6ZXyYszZUzA4mJpviCBPzPftk6tIbIn5OoMuM09MKZCUCAFD8Y1tDmjzbWdkR_5CYiFMvSLq6DzAlugtGRAShc93dmDlyZmhcct2G477FxWaRKbtmFDjzuCjGyn7xHWpS7Wz6-Ngb-JyGI2m7FxXCgCpiYYBl-4-ONTuAT0fArJi_voA8K6YLnnjEjEprI3wsQRoS3Twa_fVdGkpMNlOGsQOqmlfjDrXpyfiANOe_ZztHxbDtJEZ9zfELxx9fzkZgTL1fD2Sj6HueDU-tMt-6IaGpBCLsg7d85RK001-U9u3Ph9awQC4QZK-8-F9OUUCY5RNcRJ57KEh9PjUfA
|
||||
+1
@@ -0,0 +1 @@
|
||||
eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOjQ2ODM4OTI2NTUsImV4cCI6NDY4Mzg5MjY1NX0.MIaECJrmYjAByKNJoWHlP5ewg2xiW7GIxL8Vepp3ZIKf_jjM2OSMQlAWGmfD3Kf3bfesvSI7glw5qg_ZIv4FdIPaTvnmLRjWQkpk-QiLTJr_HM2wWeNbUJ1zciGWQlWAvabtQuyeGt1dsfQq53QLVNpvuioYdVg-gz_76uwDTxCKQU_99ksQhMMJsYJVDA_-uWGTzBANszcZykqwWFMaoXF4lkVPK4U68n18ISBB761wFusUCtyGWzwevX7wBAEJxcRy6ZVk3h7GyxZBsbRAd5fPn3dPMxNvL_CEp5jUYSAH-arAdDkvAph5Vk1yXof7FFRcffJpAy76HC66hR2JQA
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"keys":[{"p":"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M","kty":"RSA","q":"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E","d":"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ","e":"AQAB","use":"sig","kid":"one","qi":"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4","dp":"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0","dq":"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE","n":"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw"},{"p":"_CI5g5In9T4ZgakV1i62UU6yjorEr5t2URHfRYqxN7S4aKsQOzggcPoqa78xRj8PAPuf3P0ArPEAHdS6bFK7RLrFXdvyEmSNTJa1gcLCf2Zmep8bsrhrCvh6seZNvfrSMV0ULmk0B75Fs8mqE7nwcIbPtBYkinlSIw-sKRv62DM","kty":"RSA","q":"pqfexT3HBAagH-iydGsWbjG6CcYyvSQZdFtUu4LIOBCYVA0dvkN9s7uU1eoevHN_ksf-hfrF5AQH0a5P0dIJ2pp1bFa9uo9DJ7khU9sIBk9_o8nST2QLHwPQmGTW8vVlcSF7Vffvzm2fV3cQ3dfI5lvtkqfX_Z3WkF8UjFjADe8","d":"FzB5xChO8e89JisxSueY5j1RUBmatIAs_8Z3LUHOw16GlAhBhbSNl-7bXkbcUWLq9M1zTLCD91SSZXBohf9j1ebqWnbjMqQmdkxlQcVRoKcnMJ5YBabCTMBXghQnJetUMh6x6hXRnR1CSBNRdZPf-K2bnxL3xRNRSfY_7bjpb_q5pyUsK66ugSKwuEOUDNf1ttOZi4PBTsxWMDyXi_7fNFjl-B831uWNDVwdY4j68PVwGPT87zjZYjZRTZXB4ILUP11ztw4s3s_bU1Lj0PeZJsA5rmjU1iBzqCNdzgYxNlfV7M62VCkE1Wtd6M97jtysiT-5wQUMxNugoOTc9thc1Q","e":"AQAB","use":"sig","kid":"two","qi":"bnGriiVGVea9vSaN_48YYTEoKYM1kF7TrCRKERkMWdi4EHF7pZNWBv8arxaLUzElllvtGlVTNwkZlG0gOhXBoLYbcfqVikDklkBxtsuZEBKgvX7zFlDIBlNjh98lcZqDqz7Rqwr-tavxTCq2LNNlK6x-dYL61Agw_LOilYqbSfA","dp":"MmT4z-ZnnCn0WSkdlziw8iFjqP_tfhf5lwyWbsTg1PyHG0yNqvh1637k-bI2PA8ghZbFhhr_hpGI7210cXA7w-n8xtzOToTQhS1eS_hMfcBO3VVt6NPZeVDe3S3l_gHi_0DWZsxaPO336o51MwooF6WqYBlI5nCHTUC1rWXNRmc","dq":"dd_ybywc4boV87vQzQsZWGOPpG4tYR5xap1WtzHvj8gdFgYY7YQrGr8orIzlpIFE0Hroibcv1PEM3sAd8NhQ4--v8isAEz5VT3lgG0Gm0V_VdfG_8StfulYmakOYzUvIrlXyOIIfebCLrX-nzGFd1aFbzgktelLzejXmAMadQL0","n":"pCOHBsaoxlt9-qVE_INhrbkmxm7WqwEeqUBBIgHvm_JzXbmJ4iQzVF5tzAbRayxUmPbZ4E80R5HlIC2CQ7yyweTbIIWIw_TcQzXR4u3twEN1awP4s1n-00Eeurr-s9c_txZQQiDkyrCMYc9vlmsneFfubyoTvg9h_rckd8w34AyE8-wxgBRqUbm1x4ozcVmUJHkaPbQfbhIighl7osoQ4t_wXjAhTN_c9XttVjXlRwqVYPFNYUcC9GoaXWJRHjydHNFeBboOZY3E8ND6DbJ4nVtxydpUQSjTC-N-wQmhKmtYadd2hh2yywvtXpL5Q98XSphrrIHK-GWY0j8kimpunQ"}]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJuYmYiOjE1MzAzMDA4MzgsImV4cCI6MjE0NjAwMzE5OSwiaWF0IjoxNTMwMzAwODM4LCJ0eXAiOiJKV1QifQ.
|
||||
+1
@@ -0,0 +1 @@
|
||||
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6NDY4Mzg4MzIxMX0.cM7Eq9H20503czYVy1aVo8MqTQd8YsYGpv_lAV4PKr3y8NgvvosNjCSUs8rrGjQ0Sp3c4iXK6UVXq8pOJVeWXbSZa1IKAsIhiMIcg2xPFM6e71MVdX4bo255Yh8Nuh0p3xxP9isK_iAKNdMuVBOGfe9KATlmp2dOi0OpAjwSmxPJD1A7AC5f62YIe3Yx2gO6mbfANZJWQ7TxlUuCT_D5FEqg2FfYFqlFaluqWd_2X-esIsiDTxa1R9oF5XwgT6tsgvS7iYSiJw_uNKX0yU4eyLzYuIhnN_hVsr4jOZqPlsqCrkEohOGZg_Jir-7tLxZu0PqoH4ejC24FeDtC9xVa0w
|
||||
+1
@@ -0,0 +1 @@
|
||||
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTpyZWFkIl0sImV4cCI6NDY4Mzg5Nzc3Nn0.LtMVtIiRIwSyc3aX35Zl0JVwLTcQZAB3dyBOMHNaHCKUljwMrf20a_gT79LfhjDzE_fUVUmFiAO32W1vFnYpZSVaMDUgeIOIOpxfoe9shj_uYenAwIS-_UxqGVIJiJoXNZh_MK80ShNpvsQwamxWEEOAMBtpWNiVYNDMdfgho9n3o5_Z7Gjy8RLBo1tbDREbO9kTFwGIxm_EYpezmRCRq4w1DdS6UDW321hkwMxPnCMSWOvp-hRpmgY2yjzLgPJ6Aucmg9TJ8jloAP1DjJoF1gRR7NTAk8LOGkSjTzVYDYMbCF51YdpojhItSk80YzXiEsv1mTz4oMM49jXBmfXFMA
|
||||
+1
@@ -0,0 +1 @@
|
||||
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTp3cml0ZSJdLCJleHAiOjQ2ODM4OTY0OTl9.mxAFzoNjjo-7E4D_XYVme69Y7F-J--q41x6lHDTSOxzVNfQqtJ-U-N4pn7St5jElm9y3mSUxTtmwCnukaVVZkeI8aJjUc8V8nxUAsiZIDvQWjr9uW4xUIcE6MiwC0A9rhY-3I87u6No-KBTxyT80zLnCjtS2XpTId-NSd3vcYmM7Vzn4-8KoR_m-7XrjvrO69HlRrH2uUAXGnr1sn6vLp7YruupqKrHqa0e9pIpN-VRzC8Bx2LQP9mVMlQy4b1hx5MdjOTV3HUSnWiT-93z4rTMOoHScKDwmzFYoS7e00F5hyd4jzbpHdpDKnjLdwPQYz_HCmQ5MV21-Q4Q1jparIg
|
||||
+1
@@ -0,0 +1 @@
|
||||
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjQ2ODM4Mjg2NzR9.LV_i9lzN_gAB2MUuZHJKm2tOfa3xWq_qfE2lx67eoYJZsY_20Ma98A3Hh2k0wnb_mNn6jfQhXbqvUy1llmQtsx3gMNhN2Axfe3UccSKYEb2Ow5OFlrMFYby1d_D4GfXKUFKq8jyMWVlrjk_XrfJyfzeo0MyZVzURSOXv1Ehbl5-xAS_N72jiAI7cIHlHGm93Hwdk8h7Tkkf_5t2dOMJM0mh0fOT9ou3J2_ngaNDfvlAmBLxHQiJ6JrFH5njqe4lSBTxJocDcgZwGVKd0WvV4W-jwA267tZjssDFmS3xZ9hoDO_M-EjlOiEPuWLd9nQCGJpBJ3z3WeC4qrKYghHTNLA
|
||||
Reference in New Issue
Block a user