浏览代码

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
Josh Cummings 7 年之前
父节点
当前提交
40ccdb93f7
共有 50 个文件被更改,包括 4100 次插入0 次删除
  1. 1 0
      config/spring-security-config.gradle
  2. 29 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java
  3. 225 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java
  4. 828 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java
  5. 1 0
      config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Default.jwks
  6. 1 0
      config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Empty.jwks
  7. 1 0
      config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Expired.token
  8. 1 0
      config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Kid.token
  9. 1 0
      config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-MalformedPayload.token
  10. 1 0
      config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TooEarly.token
  11. 0 0
      config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TwoKeys.jwks
  12. 1 0
      config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Unsigned.token
  13. 1 0
      config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScope.token
  14. 1 0
      config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScp.token
  15. 1 0
      config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageWriteScp.token
  16. 1 0
      config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidNoScopes.token
  17. 14 0
      oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle
  18. 79 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationToken.java
  19. 125 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java
  20. 46 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorCodes.java
  21. 102 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java
  22. 151 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java
  23. 74 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java
  24. 20 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/package-info.java
  25. 20 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/package-info.java
  26. 124 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.java
  27. 143 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilter.java
  28. 43 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java
  29. 127 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java
  30. 137 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandler.java
  31. 20 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/package-info.java
  32. 20 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/package-info.java
  33. 52 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationTokenTests.java
  34. 138 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorTests.java
  35. 230 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java
  36. 107 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java
  37. 202 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPointTests.java
  38. 173 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilterTests.java
  39. 160 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java
  40. 250 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandlerTests.java
  41. 104 0
      samples/boot/oauth2resourceserver/README.adoc
  42. 13 0
      samples/boot/oauth2resourceserver/spring-security-samples-boot-oauth2resourceserver.gradle
  43. 101 0
      samples/boot/oauth2resourceserver/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java
  44. 1 0
      samples/boot/oauth2resourceserver/src/integration-test/resources/application-test.yml
  45. 30 0
      samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerApplication.java
  46. 38 0
      samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerController.java
  47. 45 0
      samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java
  48. 115 0
      samples/boot/oauth2resourceserver/src/main/java/sample/provider/MockProvider.java
  49. 1 0
      samples/boot/oauth2resourceserver/src/main/resources/META-INF/spring.factories
  50. 1 0
      samples/boot/oauth2resourceserver/src/main/resources/application.yml

+ 1 - 0
config/spring-security-config.gradle

@@ -13,6 +13,7 @@ dependencies {
 	optional project(':spring-security-messaging')
 	optional project(':spring-security-oauth2-client')
 	optional project(':spring-security-oauth2-jose')
+	optional project(':spring-security-oauth2-resource-server')
 	optional project(':spring-security-openid')
 	optional project(':spring-security-web')
 	optional 'io.projectreactor:reactor-core'

+ 29 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java

@@ -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
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java

@@ -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
config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java

@@ -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
config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Default.jwks

@@ -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
config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Empty.jwks

@@ -0,0 +1 @@
+{"keys":[]}

+ 1 - 0
config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Expired.token

@@ -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
config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Kid.token

@@ -0,0 +1 @@
+eyJraWQiOiJvbmUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjQ2ODM4ODM1NzJ9.UhukjNEowC5lLCccvdjCUJad5J9FGNModegMZGe9qKIbXxmfseTttZUNn3_K_6aNCfimtmRktCRbw3fUTcje2TFJOJ6SmomLcQyjq7S41Wq6oBSA2fdqOOU4vNvrk8_pSExsSyN9bfWiJ51I8Agzbq5eUDNo_HEpaJZimrIe9f2_njU1GxvAWsq_h4UhHEgPPb3kY9kN9hVYX_oShhh7JxbLJBnfsKBOKGEWOsE65GlmDgQV4om6RGjJaz6jFHKJTCpH08ADA3j2dqT0LNy4PrUmbnjPjWVtSQJkGcgUkcQW6qz0K86ZfJZZng_iB2VadRm5qO-99ySKmlxa5A-_Iw

+ 1 - 0
config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-MalformedPayload.token

@@ -0,0 +1 @@
+eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODM4OTEyMTl9.kpdv6ZXyYszZUzA4mJpviCBPzPftk6tIbIn5OoMuM09MKZCUCAFD8Y1tDmjzbWdkR_5CYiFMvSLq6DzAlugtGRAShc93dmDlyZmhcct2G477FxWaRKbtmFDjzuCjGyn7xHWpS7Wz6-Ngb-JyGI2m7FxXCgCpiYYBl-4-ONTuAT0fArJi_voA8K6YLnnjEjEprI3wsQRoS3Twa_fVdGkpMNlOGsQOqmlfjDrXpyfiANOe_ZztHxbDtJEZ9zfELxx9fzkZgTL1fD2Sj6HueDU-tMt-6IaGpBCLsg7d85RK001-U9u3Ph9awQC4QZK-8-F9OUUCY5RNcRJ57KEh9PjUfA

+ 1 - 0
config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TooEarly.token

@@ -0,0 +1 @@
+eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOjQ2ODM4OTI2NTUsImV4cCI6NDY4Mzg5MjY1NX0.MIaECJrmYjAByKNJoWHlP5ewg2xiW7GIxL8Vepp3ZIKf_jjM2OSMQlAWGmfD3Kf3bfesvSI7glw5qg_ZIv4FdIPaTvnmLRjWQkpk-QiLTJr_HM2wWeNbUJ1zciGWQlWAvabtQuyeGt1dsfQq53QLVNpvuioYdVg-gz_76uwDTxCKQU_99ksQhMMJsYJVDA_-uWGTzBANszcZykqwWFMaoXF4lkVPK4U68n18ISBB761wFusUCtyGWzwevX7wBAEJxcRy6ZVk3h7GyxZBsbRAd5fPn3dPMxNvL_CEp5jUYSAH-arAdDkvAph5Vk1yXof7FFRcffJpAy76HC66hR2JQA

文件差异内容过多而无法显示
+ 0 - 0
config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TwoKeys.jwks


+ 1 - 0
config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Unsigned.token

@@ -0,0 +1 @@
+eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJuYmYiOjE1MzAzMDA4MzgsImV4cCI6MjE0NjAwMzE5OSwiaWF0IjoxNTMwMzAwODM4LCJ0eXAiOiJKV1QifQ.

+ 1 - 0
config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScope.token

@@ -0,0 +1 @@
+eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6NDY4Mzg4MzIxMX0.cM7Eq9H20503czYVy1aVo8MqTQd8YsYGpv_lAV4PKr3y8NgvvosNjCSUs8rrGjQ0Sp3c4iXK6UVXq8pOJVeWXbSZa1IKAsIhiMIcg2xPFM6e71MVdX4bo255Yh8Nuh0p3xxP9isK_iAKNdMuVBOGfe9KATlmp2dOi0OpAjwSmxPJD1A7AC5f62YIe3Yx2gO6mbfANZJWQ7TxlUuCT_D5FEqg2FfYFqlFaluqWd_2X-esIsiDTxa1R9oF5XwgT6tsgvS7iYSiJw_uNKX0yU4eyLzYuIhnN_hVsr4jOZqPlsqCrkEohOGZg_Jir-7tLxZu0PqoH4ejC24FeDtC9xVa0w

+ 1 - 0
config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScp.token

@@ -0,0 +1 @@
+eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTpyZWFkIl0sImV4cCI6NDY4Mzg5Nzc3Nn0.LtMVtIiRIwSyc3aX35Zl0JVwLTcQZAB3dyBOMHNaHCKUljwMrf20a_gT79LfhjDzE_fUVUmFiAO32W1vFnYpZSVaMDUgeIOIOpxfoe9shj_uYenAwIS-_UxqGVIJiJoXNZh_MK80ShNpvsQwamxWEEOAMBtpWNiVYNDMdfgho9n3o5_Z7Gjy8RLBo1tbDREbO9kTFwGIxm_EYpezmRCRq4w1DdS6UDW321hkwMxPnCMSWOvp-hRpmgY2yjzLgPJ6Aucmg9TJ8jloAP1DjJoF1gRR7NTAk8LOGkSjTzVYDYMbCF51YdpojhItSk80YzXiEsv1mTz4oMM49jXBmfXFMA

+ 1 - 0
config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageWriteScp.token

@@ -0,0 +1 @@
+eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTp3cml0ZSJdLCJleHAiOjQ2ODM4OTY0OTl9.mxAFzoNjjo-7E4D_XYVme69Y7F-J--q41x6lHDTSOxzVNfQqtJ-U-N4pn7St5jElm9y3mSUxTtmwCnukaVVZkeI8aJjUc8V8nxUAsiZIDvQWjr9uW4xUIcE6MiwC0A9rhY-3I87u6No-KBTxyT80zLnCjtS2XpTId-NSd3vcYmM7Vzn4-8KoR_m-7XrjvrO69HlRrH2uUAXGnr1sn6vLp7YruupqKrHqa0e9pIpN-VRzC8Bx2LQP9mVMlQy4b1hx5MdjOTV3HUSnWiT-93z4rTMOoHScKDwmzFYoS7e00F5hyd4jzbpHdpDKnjLdwPQYz_HCmQ5MV21-Q4Q1jparIg

+ 1 - 0
config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidNoScopes.token

@@ -0,0 +1 @@
+eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjQ2ODM4Mjg2NzR9.LV_i9lzN_gAB2MUuZHJKm2tOfa3xWq_qfE2lx67eoYJZsY_20Ma98A3Hh2k0wnb_mNn6jfQhXbqvUy1llmQtsx3gMNhN2Axfe3UccSKYEb2Ow5OFlrMFYby1d_D4GfXKUFKq8jyMWVlrjk_XrfJyfzeo0MyZVzURSOXv1Ehbl5-xAS_N72jiAI7cIHlHGm93Hwdk8h7Tkkf_5t2dOMJM0mh0fOT9ou3J2_ngaNDfvlAmBLxHQiJ6JrFH5njqe4lSBTxJocDcgZwGVKd0WvV4W-jwA267tZjssDFmS3xZ9hoDO_M-EjlOiEPuWLd9nQCGJpBJ3z3WeC4qrKYghHTNLA

+ 14 - 0
oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle

@@ -0,0 +1,14 @@
+apply plugin: 'io.spring.convention.spring-module'
+
+dependencies {
+	compile project(':spring-security-core')
+	compile project(':spring-security-oauth2-core')
+	compile project(':spring-security-web')
+	compile springCoreDependency
+
+	optional project(':spring-security-oauth2-jose')
+
+	testCompile 'com.squareup.okhttp3:mockwebserver'
+
+	provided 'javax.servlet:javax.servlet-api'
+}

+ 79 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationToken.java

@@ -0,0 +1,79 @@
+/*
+ * 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.oauth2.server.resource;
+
+import java.util.Collections;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
+import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} that contains a
+ * <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>.
+ *
+ * Used by {@link BearerTokenAuthenticationFilter} to prepare an authentication attempt and supported
+ * by {@link JwtAuthenticationProvider}.
+ *
+ * @author Josh Cummings
+ * @since 5.1
+ */
+public class BearerTokenAuthenticationToken extends AbstractAuthenticationToken {
+	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+
+	private String token;
+
+	/**
+	 * Create a {@code BearerTokenAuthenticationToken} using the provided parameter(s)
+	 *
+	 * @param token - the bearer token
+	 */
+	public BearerTokenAuthenticationToken(String token) {
+		super(Collections.emptyList());
+
+		Assert.hasText(token, "token cannot be empty");
+
+		this.token = token;
+	}
+
+	/**
+	 * Get the <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>
+	 * @return the token that proves the caller's authority to perform the {@link javax.servlet.http.HttpServletRequest}
+	 */
+	public String getToken() {
+		return this.token;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Object getCredentials() {
+		return this.getToken();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Object getPrincipal() {
+		return this.getToken();
+	}
+}

+ 125 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java

@@ -0,0 +1,125 @@
+/*
+ * 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.oauth2.server.resource;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.util.Assert;
+
+/**
+ * A representation of a <a href="https://tools.ietf.org/html/rfc6750#section-3.1" target="_blank">Bearer Token Error</a>.
+ *
+ * @author Vedran Pavic
+ * @author Josh Cummings
+ * @since 5.1
+ * @see BearerTokenErrorCodes
+ * @see <a href="https://tools.ietf.org/html/rfc6750#section-3" target="_blank">RFC 6750 Section 3: The WWW-Authenticate
+ * Response Header Field</a>
+ */
+public final class BearerTokenError extends OAuth2Error {
+
+	private final HttpStatus httpStatus;
+
+	private final String scope;
+
+	/**
+	 * Create a {@code BearerTokenError} using the provided parameters
+	 *
+	 * @param errorCode the error code
+	 * @param httpStatus the HTTP status
+	 */
+	public BearerTokenError(String errorCode, HttpStatus httpStatus, String description, String errorUri) {
+		this(errorCode, httpStatus, description, errorUri, null);
+	}
+
+	/**
+	 * Create a {@code BearerTokenError} using the provided parameters
+	 *
+	 * @param errorCode the error code
+	 * @param httpStatus the HTTP status
+	 * @param description the description
+	 * @param errorUri the URI
+	 * @param scope the scope
+	 */
+	public BearerTokenError(String errorCode, HttpStatus httpStatus, String description, String errorUri, String scope) {
+		super(errorCode, description, errorUri);
+		Assert.notNull(httpStatus, "httpStatus cannot be null");
+
+		Assert.isTrue(isDescriptionValid(description),
+				"description contains invalid ASCII characters, it must conform to RFC 6750");
+		Assert.isTrue(isErrorCodeValid(errorCode),
+				"errorCode contains invalid ASCII characters, it must conform to RFC 6750");
+		Assert.isTrue(isErrorUriValid(errorUri),
+				"errorUri contains invalid ASCII characters, it must conform to RFC 6750");
+		Assert.isTrue(isScopeValid(scope),
+				"scope contains invalid ASCII characters, it must conform to RFC 6750");
+
+		this.httpStatus = httpStatus;
+		this.scope = scope;
+	}
+
+	/**
+	 * Return the HTTP status.
+	 * @return the HTTP status
+	 */
+	public HttpStatus getHttpStatus() {
+		return this.httpStatus;
+	}
+
+	/**
+	 * Return the scope.
+	 * @return the scope
+	 */
+	public String getScope() {
+		return this.scope;
+	}
+
+	private static boolean isDescriptionValid(String description) {
+		return description == null ||
+				description.chars().allMatch(c ->
+						withinTheRangeOf(c, 0x20, 0x21) ||
+						withinTheRangeOf(c, 0x23, 0x5B) ||
+						withinTheRangeOf(c, 0x5D, 0x7E));
+	}
+
+	private static boolean isErrorCodeValid(String errorCode) {
+		return errorCode.chars().allMatch(c ->
+						withinTheRangeOf(c, 0x20, 0x21) ||
+						withinTheRangeOf(c, 0x23, 0x5B) ||
+						withinTheRangeOf(c, 0x5D, 0x7E));
+	}
+
+	private static boolean isErrorUriValid(String errorUri) {
+		return errorUri == null ||
+				errorUri.chars().allMatch(c ->
+						c == 0x21 ||
+						withinTheRangeOf(c, 0x23, 0x5B) ||
+						withinTheRangeOf(c, 0x5D, 0x7E));
+	}
+
+	private static boolean isScopeValid(String scope) {
+		return scope == null ||
+				scope.chars().allMatch(c ->
+						withinTheRangeOf(c, 0x20, 0x21) ||
+						withinTheRangeOf(c, 0x23, 0x5B) ||
+						withinTheRangeOf(c, 0x5D, 0x7E));
+	}
+
+	private static boolean withinTheRangeOf(int c, int min, int max) {
+		return c >= min && c <= max;
+	}
+}

+ 46 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorCodes.java

@@ -0,0 +1,46 @@
+/*
+ * 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.oauth2.server.resource;
+
+/**
+ * Standard error codes defined by the OAuth 2.0 Authorization Framework: Bearer Token Usage.
+ *
+ * @author Vedran Pavic
+ * @since 5.1
+ * @see <a href="https://tools.ietf.org/html/rfc6750#section-3.1" target="_blank">RFC 6750 Section 3.1: Error Codes</a>
+ */
+public interface BearerTokenErrorCodes {
+
+	/**
+	 * {@code invalid_request} - The request is missing a required parameter, includes an unsupported parameter or
+	 * parameter value, repeats the same parameter, uses more than one method for including an access token, or is
+	 * otherwise malformed.
+	 */
+	String INVALID_REQUEST = "invalid_request";
+
+	/**
+	 * {@code invalid_token} - The access token provided is expired, revoked, malformed, or invalid for other
+	 * reasons.
+	 */
+	String INVALID_TOKEN = "invalid_token";
+
+	/**
+	 * {@code insufficient_scope} - The request requires higher privileges than provided by the access token.
+	 */
+	String INSUFFICIENT_SCOPE = "insufficient_scope";
+
+}

+ 102 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java

@@ -0,0 +1,102 @@
+/*
+ * 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.oauth2.server.resource.authentication;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.security.oauth2.core.AbstractOAuth2Token;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.util.Assert;
+
+/**
+ * Base class for {@link AbstractAuthenticationToken} implementations
+ * that expose common attributes between different OAuth 2.0 Access Token Formats.
+ *
+ * <p>
+ * For example, a {@link Jwt} could expose its {@link Jwt#getClaims() claims} via
+ * {@link #getTokenAttributes()} or an &quot;Introspected&quot; OAuth 2.0 Access Token
+ * could expose the attributes of the Introspection Response via {@link #getTokenAttributes()}.
+ *
+ * @author Joe Grandja
+ * @since 5.1
+ * @see OAuth2AccessToken
+ * @see Jwt
+ * @see <a target="_blank" href="https://tools.ietf.org/search/rfc7662#section-2.2">2.2 Introspection Response</a>
+ */
+public abstract class AbstractOAuth2TokenAuthenticationToken<T extends AbstractOAuth2Token> extends AbstractAuthenticationToken {
+	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+
+	private T token;
+
+	/**
+	 * Sub-class constructor.
+	 */
+	protected AbstractOAuth2TokenAuthenticationToken(T token) {
+
+		this(token, null);
+	}
+
+	/**
+	 * Sub-class constructor.
+	 *
+	 * @param authorities the authorities assigned to the Access Token
+	 */
+	protected AbstractOAuth2TokenAuthenticationToken(
+			T token,
+			Collection<? extends GrantedAuthority> authorities) {
+
+		super(authorities);
+
+		Assert.notNull(token, "token cannot be null");
+		this.token = token;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Object getPrincipal() {
+		return this.getToken();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Object getCredentials() {
+		return this.getToken();
+	}
+
+	/**
+	 * Get the token bound to this {@link Authentication}.
+	 */
+	public final T getToken() {
+		return this.token;
+	}
+
+	/**
+	 * Returns the attributes of the access token.
+	 *
+	 * @return a {@code Map} of the attributes in the access token.
+	 */
+	public abstract Map<String, Object> getTokenAttributes();
+}

+ 151 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java

@@ -0,0 +1,151 @@
+/*
+ * 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.oauth2.server.resource.authentication;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.stream.Collectors;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtException;
+import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
+import org.springframework.security.oauth2.server.resource.BearerTokenError;
+import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * An {@link AuthenticationProvider} implementation of the {@link Jwt}-encoded
+ * <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>s
+ * for protecting OAuth 2.0 Resource Servers.
+ * <p>
+ * <p>
+ * This {@link AuthenticationProvider} is responsible for decoding and verifying a {@link Jwt}-encoded access token,
+ * returning its claims set as part of the {@see Authentication} statement.
+ * <p>
+ * <p>
+ * Scopes are translated into {@link GrantedAuthority}s according to the following algorithm:
+ *
+ * 1. If there is a "scope" or "scp" attribute, then
+ * 		if a {@link String}, then split by spaces and return, or
+ * 		if a {@link Collection}, then simply return
+ * 2. Take the resulting {@link Collection} of {@link String}s and prepend the "SCOPE_" keyword, adding
+ * 		as {@link GrantedAuthority}s.
+ *
+ * @author Josh Cummings
+ * @author Joe Grandja
+ * @since 5.1
+ * @see AuthenticationProvider
+ * @see JwtDecoder
+ */
+public final class JwtAuthenticationProvider implements AuthenticationProvider {
+	private final JwtDecoder jwtDecoder;
+
+	private static final Collection<String> WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES =
+			Arrays.asList("scope", "scp");
+
+	private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_";
+
+	public JwtAuthenticationProvider(JwtDecoder jwtDecoder) {
+		Assert.notNull(jwtDecoder, "jwtDecoder cannot be null");
+
+		this.jwtDecoder = jwtDecoder;
+	}
+
+	/**
+	 * Decode and validate the
+	 * <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>.
+	 *
+	 * @param authentication the authentication request object.
+	 *
+	 * @return A successful authentication
+	 * @throws AuthenticationException if authentication failed for some reason
+	 */
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;
+
+		Jwt jwt;
+		try {
+			jwt = this.jwtDecoder.decode(bearer.getToken());
+		} catch (JwtException failed) {
+			OAuth2Error invalidToken;
+			try {
+				invalidToken = invalidToken(failed.getMessage());
+			} catch ( IllegalArgumentException malformed ) {
+				// some third-party library error messages are not suitable for RFC 6750's error message charset
+				invalidToken = invalidToken("An error occurred while attempting to decode the Jwt: Invalid token");
+			}
+			throw new OAuth2AuthenticationException(invalidToken, failed);
+		}
+
+		Collection<GrantedAuthority> authorities =
+				this.getScopes(jwt)
+						.stream()
+						.map(authority -> SCOPE_AUTHORITY_PREFIX + authority)
+						.map(SimpleGrantedAuthority::new)
+						.collect(Collectors.toList());
+
+		JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities);
+
+		token.setDetails(bearer.getDetails());
+
+		return token;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	private static OAuth2Error invalidToken(String message) {
+		return new BearerTokenError(
+				BearerTokenErrorCodes.INVALID_TOKEN,
+				HttpStatus.UNAUTHORIZED,
+				message,
+				"https://tools.ietf.org/html/rfc6750#section-3.1");
+	}
+
+	private static Collection<String> getScopes(Jwt jwt) {
+		for ( String attributeName : WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES ) {
+			Object scopes = jwt.getClaims().get(attributeName);
+			if (scopes instanceof String) {
+				if (StringUtils.hasText((String) scopes)) {
+					return Arrays.asList(((String) scopes).split(" "));
+				} else {
+					return Collections.emptyList();
+				}
+			} else if (scopes instanceof Collection) {
+				return (Collection<String>) scopes;
+			}
+		}
+
+		return Collections.emptyList();
+	}
+}

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

@@ -0,0 +1,74 @@
+/*
+ * 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.oauth2.server.resource.authentication;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.security.core.TransientAuthentication;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+/**
+ * An implementation of an {@link AbstractOAuth2TokenAuthenticationToken}
+ * representing a {@link Jwt} {@code Authentication}.
+ *
+ * @author Joe Grandja
+ * @since 5.1
+ * @see AbstractOAuth2TokenAuthenticationToken
+ * @see Jwt
+ */
+@TransientAuthentication
+public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken<Jwt> {
+	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+
+	/**
+	 * Constructs a {@code JwtAuthenticationToken} using the provided parameters.
+	 *
+	 * @param jwt the JWT
+	 */
+	public JwtAuthenticationToken(Jwt jwt) {
+		super(jwt);
+	}
+
+	/**
+	 * Constructs a {@code JwtAuthenticationToken} using the provided parameters.
+	 *
+	 * @param jwt the JWT
+	 * @param authorities the authorities assigned to the JWT
+	 */
+	public JwtAuthenticationToken(Jwt jwt, Collection<? extends GrantedAuthority> authorities) {
+		super(jwt, authorities);
+		this.setAuthenticated(true);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Map<String, Object> getTokenAttributes() {
+		return this.getToken().getClaims();
+	}
+
+	/**
+	 * The {@link Jwt}'s subject, if any
+	 */
+	@Override
+	public String getName() {
+		return this.getToken().getSubject();
+	}
+}

+ 20 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/package-info.java

@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * OAuth 2.0 Resource Server {@code Authentication}s and supporting classes and interfaces.
+ */
+package org.springframework.security.oauth2.server.resource.authentication;

+ 20 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/package-info.java

@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * OAuth 2.0 Resource Server core classes and interfaces providing support.
+ */
+package org.springframework.security.oauth2.server.resource;

+ 124 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.java

@@ -0,0 +1,124 @@
+/*
+ * 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.oauth2.server.resource.web;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.server.resource.BearerTokenError;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.util.StringUtils;
+
+/**
+ * An {@link AuthenticationEntryPoint} implementation used to commence authentication of protected resource requests
+ * using {@link BearerTokenAuthenticationFilter}.
+ * <p>
+ * Uses information provided by {@link BearerTokenError} to set HTTP response status code and populate
+ * {@code WWW-Authenticate} HTTP header.
+ *
+ * @author Vedran Pavic
+ * @since 5.1
+ * @see BearerTokenError
+ * @see <a href="https://tools.ietf.org/html/rfc6750#section-3" target="_blank">RFC 6750 Section 3: The WWW-Authenticate
+ * Response Header Field</a>
+ */
+public final class BearerTokenAuthenticationEntryPoint implements AuthenticationEntryPoint {
+
+	private String realmName;
+
+	/**
+	 * Collect error details from the provided parameters and format according to
+	 * RFC 6750, specifically {@code error}, {@code error_description}, {@code error_uri}, and {@scope scope}.
+	 *
+	 * @param request that resulted in an <code>AuthenticationException</code>
+	 * @param response so that the user agent can begin authentication
+	 * @param authException that caused the invocation
+	 */
+	@Override
+	public void commence(
+			HttpServletRequest request, HttpServletResponse response,
+			AuthenticationException authException)
+			throws IOException, ServletException {
+
+		HttpStatus status = HttpStatus.UNAUTHORIZED;
+
+		Map<String, String> parameters = new LinkedHashMap<>();
+
+		if (this.realmName != null) {
+			parameters.put("realm", this.realmName);
+		}
+
+		if (authException instanceof OAuth2AuthenticationException) {
+			OAuth2Error error = ((OAuth2AuthenticationException) authException).getError();
+
+			parameters.put("error", error.getErrorCode());
+
+			if (StringUtils.hasText(error.getDescription())) {
+				parameters.put("error_description", error.getDescription());
+			}
+
+			if (StringUtils.hasText(error.getUri())) {
+				parameters.put("error_uri", error.getUri());
+			}
+
+			if (error instanceof BearerTokenError) {
+				BearerTokenError bearerTokenError = (BearerTokenError) error;
+
+				if (StringUtils.hasText(bearerTokenError.getScope())) {
+					parameters.put("scope", bearerTokenError.getScope());
+				}
+
+				status = ((BearerTokenError) error).getHttpStatus();
+			}
+		}
+
+		String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters);
+
+		response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
+		response.setStatus(status.value());
+	}
+
+	/**
+	 * Set the default realm name to use in the bearer token error response
+	 *
+	 * @param realmName
+	 */
+	public final void setRealmName(String realmName) {
+		this.realmName = realmName;
+	}
+
+	private static String computeWWWAuthenticateHeaderValue(Map<String, String> parameters) {
+		String wwwAuthenticate = "Bearer";
+		if (!parameters.isEmpty()) {
+			wwwAuthenticate += parameters.entrySet().stream()
+					.map(attribute -> attribute.getKey() + "=\"" + attribute.getValue() + "\"")
+					.collect(Collectors.joining(", ", " ", ""));
+		}
+
+		return wwwAuthenticate;
+	}
+}

+ 143 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilter.java

@@ -0,0 +1,143 @@
+/*
+ * 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.oauth2.server.resource.web;
+
+import java.io.IOException;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.security.authentication.AuthenticationDetailsSource;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.util.Assert;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+/**
+ * Authenticates requests that contain an OAuth 2.0
+ * <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>.
+ *
+ * This filter should be wired with an {@link AuthenticationManager} that can authenticate a
+ * {@link BearerTokenAuthenticationToken}.
+ *
+ * @author Josh Cummings
+ * @author Vedran Pavic
+ * @author Joe Grandja
+ * @since 5.1
+ * @see <a href="https://tools.ietf.org/html/rfc6750" target="_blank">The OAuth 2.0 Authorization Framework: Bearer Token Usage</a>
+ * @see JwtAuthenticationProvider
+ */
+public final class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
+	private final AuthenticationManager authenticationManager;
+
+	private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource =
+			new WebAuthenticationDetailsSource();
+
+	private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
+
+	private AuthenticationEntryPoint authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint();
+
+	/**
+	 * Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s)
+	 * @param authenticationManager
+	 */
+	public BearerTokenAuthenticationFilter(AuthenticationManager authenticationManager) {
+		Assert.notNull(authenticationManager, "authenticationManager cannot be null");
+		this.authenticationManager = authenticationManager;
+	}
+
+	/**
+	 * Extract any <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a> from
+	 * the request and attempt an authentication.
+	 *
+	 * @param request
+	 * @param response
+	 * @param filterChain
+	 * @throws ServletException
+	 * @throws IOException
+	 */
+	@Override
+	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+			throws ServletException, IOException {
+
+		final boolean debug = this.logger.isDebugEnabled();
+
+		String token;
+
+		try {
+			token = this.bearerTokenResolver.resolve(request);
+		} catch ( OAuth2AuthenticationException invalid ) {
+			this.authenticationEntryPoint.commence(request, response, invalid);
+			return;
+		}
+
+		if (token == null) {
+			filterChain.doFilter(request, response);
+			return;
+		}
+
+		BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token);
+
+		authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
+
+		try {
+			Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
+
+			SecurityContext context = SecurityContextHolder.createEmptyContext();
+			context.setAuthentication(authenticationResult);
+			SecurityContextHolder.setContext(context);
+
+			filterChain.doFilter(request, response);
+		} catch (AuthenticationException failed) {
+			SecurityContextHolder.clearContext();
+
+			if (debug) {
+				this.logger.debug("Authentication request for failed: " + failed);
+			}
+
+			this.authenticationEntryPoint.commence(request, response, failed);
+		}
+	}
+
+	/**
+	 * Set the {@link BearerTokenResolver} to use. Defaults to {@link DefaultBearerTokenResolver}.
+	 * @param bearerTokenResolver the {@code BearerTokenResolver} to use
+	 */
+	public final void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
+		Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
+		this.bearerTokenResolver = bearerTokenResolver;
+	}
+
+	/**
+	 * Set the {@link AuthenticationEntryPoint} to use. Defaults to {@link BearerTokenAuthenticationEntryPoint}.
+	 * @param authenticationEntryPoint the {@code AuthenticationEntryPoint} to use
+	 */
+	public final void setAuthenticationEntryPoint(final AuthenticationEntryPoint authenticationEntryPoint) {
+		Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
+		this.authenticationEntryPoint = authenticationEntryPoint;
+	}
+
+}

+ 43 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java

@@ -0,0 +1,43 @@
+/*
+ * 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.oauth2.server.resource.web;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+
+/**
+ * A strategy for resolving <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>s
+ * from the {@link HttpServletRequest}.
+ *
+ * @author Vedran Pavic
+ * @since 5.1
+ * @see <a href="https://tools.ietf.org/html/rfc6750#section-2" target="_blank">RFC 6750 Section 2: Authenticated Requests</a>
+ */
+public interface BearerTokenResolver {
+
+	/**
+	 * Resolve any <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>
+	 * value from the request.
+	 *
+	 * @param request the request
+	 * @return the Bearer Token value or {@code null} if none found
+	 * @throws OAuth2AuthenticationException if the found token is invalid
+	 */
+	String resolve(HttpServletRequest request);
+
+}

+ 127 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java

@@ -0,0 +1,127 @@
+/*
+ * 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.oauth2.server.resource.web;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.server.resource.BearerTokenError;
+import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
+import org.springframework.util.StringUtils;
+
+/**
+ * The default {@link BearerTokenResolver} implementation based on RFC 6750.
+ *
+ * @author Vedran Pavic
+ * @since 5.1
+ * @see <a href="https://tools.ietf.org/html/rfc6750#section-2" target="_blank">RFC 6750 Section 2: Authenticated Requests</a>
+ */
+public final class DefaultBearerTokenResolver implements BearerTokenResolver {
+
+	private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+)=*$");
+
+	private boolean allowFormEncodedBodyParameter = false;
+
+	private boolean allowUriQueryParameter = false;
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public String resolve(HttpServletRequest request) {
+		String authorizationHeaderToken = resolveFromAuthorizationHeader(request);
+		String parameterToken = resolveFromRequestParameters(request);
+		if (authorizationHeaderToken != null) {
+			if (parameterToken != null) {
+				BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST,
+						HttpStatus.BAD_REQUEST,
+						"Found multiple bearer tokens in the request",
+						"https://tools.ietf.org/html/rfc6750#section-3.1");
+				throw new OAuth2AuthenticationException(error);
+			}
+			return authorizationHeaderToken;
+		}
+		else if (parameterToken != null && isParameterTokenSupportedForRequest(request)) {
+			return parameterToken;
+		}
+		return null;
+	}
+
+	/**
+	 * Set if transport of access token using form-encoded body parameter is supported. Defaults to {@code false}.
+	 * @param allowFormEncodedBodyParameter if the form-encoded body parameter is supported
+	 */
+	public void setAllowFormEncodedBodyParameter(boolean allowFormEncodedBodyParameter) {
+		this.allowFormEncodedBodyParameter = allowFormEncodedBodyParameter;
+	}
+
+	/**
+	 * Set if transport of access token using URI query parameter is supported. Defaults to {@code false}.
+	 *
+	 * The spec recommends against using this mechanism for sending bearer tokens, and even goes as far as
+	 * stating that it was only included for completeness.
+	 *
+	 * @param allowUriQueryParameter if the URI query parameter is supported
+	 */
+	public void setAllowUriQueryParameter(boolean allowUriQueryParameter) {
+		this.allowUriQueryParameter = allowUriQueryParameter;
+	}
+
+	private static String resolveFromAuthorizationHeader(HttpServletRequest request) {
+		String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
+		if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer")) {
+			Matcher matcher = authorizationPattern.matcher(authorization);
+
+			if (!matcher.matches()) {
+				BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN,
+						HttpStatus.UNAUTHORIZED,
+						"Bearer token is malformed",
+						"https://tools.ietf.org/html/rfc6750#section-3.1");
+				throw new OAuth2AuthenticationException(error);
+			}
+
+			return matcher.group("token");
+		}
+		return null;
+	}
+
+	private static String resolveFromRequestParameters(HttpServletRequest request) {
+		String[] values = request.getParameterValues("access_token");
+		if (values == null || values.length == 0)  {
+			return null;
+		}
+
+		if (values.length == 1) {
+			return values[0];
+		}
+
+		BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST,
+				HttpStatus.BAD_REQUEST,
+				"Found multiple bearer tokens in the request",
+				"https://tools.ietf.org/html/rfc6750#section-3.1");
+		throw new OAuth2AuthenticationException(error);
+	}
+
+	private boolean isParameterTokenSupportedForRequest(HttpServletRequest request) {
+		return ((this.allowFormEncodedBodyParameter && "POST".equals(request.getMethod()))
+				|| (this.allowUriQueryParameter && "GET".equals(request.getMethod())));
+	}
+}

+ 137 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandler.java

@@ -0,0 +1,137 @@
+/*
+ * 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.oauth2.server.resource.web.access;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
+import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.util.StringUtils;
+
+/**
+ * Translates any {@link AccessDeniedException} into an HTTP response in accordance with
+ * <a href="https://tools.ietf.org/html/rfc6750#section-3" target="_blank">RFC 6750 Section 3: The WWW-Authenticate</a>.
+ *
+ * So long as the class can prove that the request has a valid OAuth 2.0 {@link Authentication}, then will return an
+ * <a href="https://tools.ietf.org/html/rfc6750#section-3.1" target="_blank">insufficient scope error</a>; otherwise,
+ * it will simply indicate the scheme (Bearer) and any configured realm.
+ *
+ * @author Josh Cummings
+ * @since 5.1
+ */
+public final class BearerTokenAccessDeniedHandler implements AccessDeniedHandler {
+
+	private static final Collection<String> WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES =
+			Arrays.asList("scope", "scp");
+
+	private String realmName;
+
+	/**
+	 * Collect error details from the provided parameters and format according to
+	 * RFC 6750, specifically {@code error}, {@code error_description}, {@code error_uri}, and {@scope scope}.
+	 *
+	 * @param request that resulted in an <code>AccessDeniedException</code>
+	 * @param response so that the user agent can be advised of the failure
+	 * @param accessDeniedException that caused the invocation
+	 *
+	 */
+	@Override
+	public void handle(
+			HttpServletRequest request, HttpServletResponse response,
+			AccessDeniedException accessDeniedException)
+			throws IOException, ServletException {
+
+		Map<String, String> parameters = new LinkedHashMap<>();
+
+		if (this.realmName != null) {
+			parameters.put("realm", this.realmName);
+		}
+
+		if (request.getUserPrincipal() instanceof AbstractOAuth2TokenAuthenticationToken) {
+			AbstractOAuth2TokenAuthenticationToken token =
+					(AbstractOAuth2TokenAuthenticationToken) request.getUserPrincipal();
+
+			String scope = getScope(token);
+
+			parameters.put("error", BearerTokenErrorCodes.INSUFFICIENT_SCOPE);
+			parameters.put("error_description",
+					String.format("The token provided has insufficient scope [%s] for this request", scope));
+			parameters.put("error_uri", "https://tools.ietf.org/html/rfc6750#section-3.1");
+
+			if (StringUtils.hasText(scope)) {
+				parameters.put("scope", scope);
+			}
+		}
+
+		String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters);
+
+		response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
+		response.setStatus(HttpStatus.FORBIDDEN.value());
+	}
+
+	/**
+	 * Set the default realm name to use in the bearer token error response
+	 *
+	 * @param realmName
+	 */
+	public final void setRealmName(String realmName) {
+		this.realmName = realmName;
+	}
+
+	private static String getScope(AbstractOAuth2TokenAuthenticationToken token) {
+
+		Map<String, Object> attributes = token.getTokenAttributes();
+
+		for (String attributeName : WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES) {
+			Object scopes = attributes.get(attributeName);
+			if (scopes instanceof String) {
+				return (String) scopes;
+			} else if (scopes instanceof Collection) {
+				Collection coll = (Collection) scopes;
+				return (String) coll.stream()
+						.map(String::valueOf)
+						.collect(Collectors.joining(" "));
+			}
+		}
+
+		return "";
+	}
+
+	private static String computeWWWAuthenticateHeaderValue(Map<String, String> parameters) {
+		String wwwAuthenticate = "Bearer";
+		if (!parameters.isEmpty()) {
+			wwwAuthenticate += parameters.entrySet().stream()
+					.map(attribute -> attribute.getKey() + "=\"" + attribute.getValue() + "\"")
+					.collect(Collectors.joining(", ", " ", ""));
+		}
+
+		return wwwAuthenticate;
+	}
+}

+ 20 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/package-info.java

@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * OAuth 2.0 Resource Server access denial classes and interfaces.
+ */
+package org.springframework.security.oauth2.server.resource.web.access;

+ 20 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/package-info.java

@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * OAuth 2.0 Resource Server {@code Filter}'s and supporting classes and interfaces.
+ */
+package org.springframework.security.oauth2.server.resource.web;

+ 52 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationTokenTests.java

@@ -0,0 +1,52 @@
+/*
+ * 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.oauth2.server.resource;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/**
+ * Tests for {@link BearerTokenAuthenticationToken}
+ *
+ * @author Josh Cummings
+ */
+public class BearerTokenAuthenticationTokenTests {
+	@Test
+	public void constructorWhenTokenIsNullThenThrowsException() {
+		assertThatCode(() -> new BearerTokenAuthenticationToken(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessageContaining("token cannot be empty");
+	}
+
+	@Test
+	public void constructorWhenTokenIsEmptyThenThrowsException() {
+		assertThatCode(() -> new BearerTokenAuthenticationToken(""))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessageContaining("token cannot be empty");
+	}
+
+	@Test
+	public void constructorWhenTokenHasValueThenConstructedCorrectly() {
+		BearerTokenAuthenticationToken token = new BearerTokenAuthenticationToken("token");
+
+		assertThat(token.getToken()).isEqualTo("token");
+		assertThat(token.getPrincipal()).isEqualTo("token");
+		assertThat(token.getCredentials()).isEqualTo("token");
+	}
+}

+ 138 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorTests.java

@@ -0,0 +1,138 @@
+/*
+ * 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.oauth2.server.resource;
+
+import org.junit.Test;
+
+import org.springframework.http.HttpStatus;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/**
+ * Tests for {@link BearerTokenError}
+ *
+ * @author Vedran Pavic
+ * @author Josh Cummings
+ */
+public class BearerTokenErrorTests {
+
+	private static final String TEST_ERROR_CODE = "test-code";
+
+	private static final HttpStatus TEST_HTTP_STATUS = HttpStatus.UNAUTHORIZED;
+
+	private static final String TEST_DESCRIPTION = "test-description";
+
+	private static final String TEST_URI = "http://example.com";
+
+	private static final String TEST_SCOPE = "test-scope";
+
+	@Test
+	public void constructorWithErrorCodeWhenErrorCodeIsValidThenCreated() {
+		BearerTokenError error = new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, null, null);
+
+		assertThat(error.getErrorCode()).isEqualTo(TEST_ERROR_CODE);
+		assertThat(error.getHttpStatus()).isEqualTo(TEST_HTTP_STATUS);
+		assertThat(error.getDescription()).isNull();
+		assertThat(error.getUri()).isNull();
+		assertThat(error.getScope()).isNull();
+	}
+
+	@Test
+	public void constructorWithErrorCodeAndHttpStatusWhenErrorCodeIsNullThenThrowIllegalArgumentException() {
+		assertThatCode(() -> new BearerTokenError(null, TEST_HTTP_STATUS, null, null))
+				.isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty");
+	}
+
+	@Test
+	public void constructorWithErrorCodeAndHttpStatusWhenErrorCodeIsEmptyThenThrowIllegalArgumentException() {
+		assertThatCode(() -> new BearerTokenError("", TEST_HTTP_STATUS, null, null))
+				.isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty");
+	}
+
+	@Test
+	public void constructorWithErrorCodeAndHttpStatusWhenHttpStatusIsNullThenThrowIllegalArgumentException() {
+		assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, null, null, null))
+				.isInstanceOf(IllegalArgumentException.class).hasMessage("httpStatus cannot be null");
+	}
+
+	@Test
+	public void constructorWithAllParametersWhenAllParametersAreValidThenCreated() {
+		BearerTokenError error = new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI,
+				TEST_SCOPE);
+
+		assertThat(error.getErrorCode()).isEqualTo(TEST_ERROR_CODE);
+		assertThat(error.getHttpStatus()).isEqualTo(TEST_HTTP_STATUS);
+		assertThat(error.getDescription()).isEqualTo(TEST_DESCRIPTION);
+		assertThat(error.getUri()).isEqualTo(TEST_URI);
+		assertThat(error.getScope()).isEqualTo(TEST_SCOPE);
+	}
+
+	@Test
+	public void constructorWithAllParametersWhenErrorCodeIsNullThenThrowIllegalArgumentException() {
+		assertThatCode(() -> new BearerTokenError(null, TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI, TEST_SCOPE))
+				.isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty");
+	}
+
+	@Test
+	public void constructorWithAllParametersWhenErrorCodeIsEmptyThenThrowIllegalArgumentException() {
+		assertThatCode(() -> new BearerTokenError("", TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI, TEST_SCOPE))
+				.isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty");
+	}
+
+	@Test
+	public void constructorWithAllParametersWhenHttpStatusIsNullThenThrowIllegalArgumentException() {
+		assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, null, TEST_DESCRIPTION, TEST_URI, TEST_SCOPE))
+				.isInstanceOf(IllegalArgumentException.class).hasMessage("httpStatus cannot be null");
+	}
+
+	@Test
+	public void constructorWithAllParametersWhenErrorCodeIsInvalidThenThrowIllegalArgumentException() {
+		assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE + "\"", TEST_HTTP_STATUS, TEST_DESCRIPTION,
+				TEST_URI, TEST_SCOPE))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessageContaining("errorCode")
+				.hasMessageContaining("RFC 6750");
+	}
+
+	@Test
+	public void constructorWithAllParametersWhenDescriptionIsInvalidThenThrowIllegalArgumentException() {
+		assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION + "\"",
+				TEST_URI, TEST_SCOPE))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessageContaining("description")
+				.hasMessageContaining("RFC 6750");
+	}
+
+	@Test
+	public void constructorWithAllParametersWhenErrorUriIsInvalidThenThrowIllegalArgumentException() {
+		assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION,
+				TEST_URI + "\"", TEST_SCOPE))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessageContaining("errorUri")
+				.hasMessageContaining("RFC 6750");
+	}
+
+	@Test
+	public void constructorWithAllParametersWhenScopeIsInvalidThenThrowIllegalArgumentException() {
+		assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION,
+				TEST_URI, TEST_SCOPE + "\""))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessageContaining("scope")
+				.hasMessageContaining("RFC 6750");
+	}
+}

+ 230 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java

@@ -0,0 +1,230 @@
+/*
+ * 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.oauth2.server.resource.authentication;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Predicate;
+
+import org.assertj.core.util.Maps;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtException;
+import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
+import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link JwtAuthenticationProvider}
+ *
+ * @author Josh Cummings
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class JwtAuthenticationProviderTests {
+	@Mock
+	JwtDecoder jwtDecoder;
+
+	JwtAuthenticationProvider provider;
+
+	@Before
+	public void setup() {
+		this.provider =
+				new JwtAuthenticationProvider(this.jwtDecoder);
+	}
+
+	@Test
+	public void authenticateWhenJwtDecodesThenAuthenticationHasAttributesContainedInJwt() {
+		BearerTokenAuthenticationToken token = this.authentication();
+
+		Map<String, Object> claims = new HashMap<>();
+		claims.put("name", "value");
+		Jwt jwt = this.jwt(claims);
+
+		when(this.jwtDecoder.decode("token")).thenReturn(jwt);
+
+		JwtAuthenticationToken authentication =
+				(JwtAuthenticationToken) this.provider.authenticate(token);
+
+		assertThat(authentication.getTokenAttributes()).isEqualTo(claims);
+	}
+
+	@Test
+	public void authenticateWhenJwtDecodeFailsThenRespondsWithInvalidToken() {
+		BearerTokenAuthenticationToken token = this.authentication();
+
+		when(this.jwtDecoder.decode("token")).thenThrow(JwtException.class);
+
+		assertThatCode(() -> this.provider.authenticate(token))
+				.matches(failed -> failed instanceof OAuth2AuthenticationException)
+				.matches(errorCode(BearerTokenErrorCodes.INVALID_TOKEN));
+	}
+
+	@Test
+	public void authenticateWhenTokenHasScopeAttributeThenTranslatedToAuthorities() {
+		BearerTokenAuthenticationToken token = this.authentication();
+
+		Jwt jwt = this.jwt(Maps.newHashMap("scope", "message:read message:write"));
+
+		when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt);
+
+		JwtAuthenticationToken authentication =
+				(JwtAuthenticationToken) this.provider.authenticate(token);
+
+		Collection<GrantedAuthority> authorities = authentication.getAuthorities();
+
+		assertThat(authorities).containsExactly(
+				new SimpleGrantedAuthority("SCOPE_message:read"),
+				new SimpleGrantedAuthority("SCOPE_message:write"));
+	}
+
+	@Test
+	public void authenticateWhenTokenHasEmptyScopeAttributeThenTranslatedToNoAuthorities() {
+		BearerTokenAuthenticationToken token = this.authentication();
+
+		Jwt jwt = this.jwt(Maps.newHashMap("scope", ""));
+
+		when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt);
+
+		JwtAuthenticationToken authentication =
+				(JwtAuthenticationToken) this.provider.authenticate(token);
+
+		Collection<GrantedAuthority> authorities = authentication.getAuthorities();
+
+		assertThat(authorities).containsExactly();
+	}
+
+	@Test
+	public void authenticateWhenTokenHasScpAttributeThenTranslatedToAuthorities() {
+		BearerTokenAuthenticationToken token = this.authentication();
+
+		Jwt jwt = this.jwt(Maps.newHashMap("scp", Arrays.asList("message:read", "message:write")));
+
+		when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt);
+
+		JwtAuthenticationToken authentication =
+				(JwtAuthenticationToken) this.provider.authenticate(token);
+
+		Collection<GrantedAuthority> authorities = authentication.getAuthorities();
+
+		assertThat(authorities).containsExactly(
+				new SimpleGrantedAuthority("SCOPE_message:read"),
+				new SimpleGrantedAuthority("SCOPE_message:write"));
+	}
+
+	@Test
+	public void authenticateWhenTokenHasEmptyScpAttributeThenTranslatedToNoAuthorities() {
+		BearerTokenAuthenticationToken token = this.authentication();
+
+		Jwt jwt = this.jwt(Maps.newHashMap("scp", Arrays.asList()));
+
+		when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt);
+
+		JwtAuthenticationToken authentication =
+				(JwtAuthenticationToken) this.provider.authenticate(token);
+
+		Collection<GrantedAuthority> authorities = authentication.getAuthorities();
+
+		assertThat(authorities).containsExactly();
+	}
+
+	@Test
+	public void authenticateWhenTokenHasBothScopeAndScpThenScopeAttributeIsTranslatedToAuthorities() {
+		BearerTokenAuthenticationToken token = this.authentication();
+
+		Map<String, Object> claims = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write"));
+		claims.put("scope", "missive:read missive:write");
+		Jwt jwt = this.jwt(claims);
+
+		when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt);
+
+		JwtAuthenticationToken authentication =
+				(JwtAuthenticationToken) this.provider.authenticate(token);
+
+		Collection<GrantedAuthority> authorities = authentication.getAuthorities();
+
+		assertThat(authorities).containsExactly(
+				new SimpleGrantedAuthority("SCOPE_missive:read"),
+				new SimpleGrantedAuthority("SCOPE_missive:write"));
+	}
+
+	@Test
+	public void authenticateWhenTokenHasEmptyScopeAndNonEmptyScpThenScopeAttributeIsTranslatedToNoAuthorities() {
+		BearerTokenAuthenticationToken token = this.authentication();
+
+		Map<String, Object> claims = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write"));
+		claims.put("scope", "");
+		Jwt jwt = this.jwt(claims);
+
+		when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt);
+
+		JwtAuthenticationToken authentication =
+				(JwtAuthenticationToken) this.provider.authenticate(token);
+
+		Collection<GrantedAuthority> authorities = authentication.getAuthorities();
+
+		assertThat(authorities).containsExactly();
+	}
+
+	@Test
+	public void authenticateWhenDecoderThrowsIncompatibleErrorMessageThenWrapsWithGenericOne() {
+		BearerTokenAuthenticationToken token = this.authentication();
+
+		when(this.jwtDecoder.decode(token.getToken())).thenThrow(new JwtException("with \"invalid\" chars"));
+
+		assertThatCode(() -> this.provider.authenticate(token))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.hasFieldOrPropertyWithValue(
+						"error.description",
+						"An error occurred while attempting to decode the Jwt: Invalid token");
+	}
+
+	@Test
+	public void supportsWhenBearerTokenAuthenticationTokenThenReturnsTrue() {
+		assertThat(this.provider.supports(BearerTokenAuthenticationToken.class)).isTrue();
+	}
+
+	private BearerTokenAuthenticationToken authentication() {
+		return new BearerTokenAuthenticationToken("token");
+	}
+
+	private Jwt jwt(Map<String, Object> claims) {
+		Map<String, Object> headers = new HashMap<>();
+		headers.put("alg", JwsAlgorithms.RS256);
+
+		return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims);
+	}
+
+	private Predicate<? super Throwable> errorCode(String errorCode) {
+		return failed ->
+				((OAuth2AuthenticationException) failed).getError().getErrorCode() == errorCode;
+	}
+}

+ 107 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java

@@ -0,0 +1,107 @@
+/*
+ * 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.oauth2.server.resource.authentication;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.assertj.core.util.Maps;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/**
+ * Tests for {@link JwtAuthenticationToken}
+ *
+ * @author Josh Cummings
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class JwtAuthenticationTokenTests {
+
+	@Test
+	public void getNameWhenJwtHasSubjectThenReturnsSubject() {
+		Jwt jwt = this.jwt(Maps.newHashMap("sub", "Carl"));
+
+		JwtAuthenticationToken token = new JwtAuthenticationToken(jwt);
+
+		assertThat(token.getName()).isEqualTo("Carl");
+	}
+
+	@Test
+	public void getNameWhenJwtHasNoSubjectThenReturnsNull() {
+		Jwt jwt = this.jwt(Maps.newHashMap("claim", "value"));
+
+		JwtAuthenticationToken token = new JwtAuthenticationToken(jwt);
+
+		assertThat(token.getName()).isNull();
+	}
+
+	@Test
+	public void constructorWhenJwtIsNullThenThrowsException() {
+		assertThatCode(() -> new JwtAuthenticationToken(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessageContaining("token cannot be null");
+	}
+
+	@Test
+	public void constructorWhenUsingCorrectParametersThenConstructedCorrectly() {
+		Collection authorities = Arrays.asList(new SimpleGrantedAuthority("test"));
+		Map claims = Maps.newHashMap("claim", "value");
+		Jwt jwt = this.jwt(claims);
+
+		JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities);
+
+		assertThat(token.getAuthorities()).isEqualTo(authorities);
+		assertThat(token.getPrincipal()).isEqualTo(jwt);
+		assertThat(token.getCredentials()).isEqualTo(jwt);
+		assertThat(token.getToken()).isEqualTo(jwt);
+		assertThat(token.getTokenAttributes()).isEqualTo(claims);
+		assertThat(token.isAuthenticated()).isTrue();
+	}
+
+	@Test
+	public void constructorWhenUsingOnlyJwtThenConstructedCorrectly() {
+		Map claims = Maps.newHashMap("claim", "value");
+		Jwt jwt = this.jwt(claims);
+
+		JwtAuthenticationToken token = new JwtAuthenticationToken(jwt);
+
+		assertThat(token.getAuthorities()).isEmpty();
+		assertThat(token.getPrincipal()).isEqualTo(jwt);
+		assertThat(token.getCredentials()).isEqualTo(jwt);
+		assertThat(token.getToken()).isEqualTo(jwt);
+		assertThat(token.getTokenAttributes()).isEqualTo(claims);
+		assertThat(token.isAuthenticated()).isFalse();
+	}
+
+	private Jwt jwt(Map<String, Object> claims) {
+		Map<String, Object> headers = new HashMap<>();
+		headers.put("alg", JwsAlgorithms.RS256);
+
+		return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims);
+	}
+}

+ 202 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPointTests.java

@@ -0,0 +1,202 @@
+/*
+ * 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.oauth2.server.resource.web;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.server.resource.BearerTokenError;
+import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/**
+ * Tests for {@link BearerTokenAuthenticationEntryPoint}.
+ *
+ * @author Vedran Pavic
+ * @author Josh Cummings
+ */
+public class BearerTokenAuthenticationEntryPointTests {
+
+	private BearerTokenAuthenticationEntryPoint authenticationEntryPoint;
+
+	@Before
+	public void setUp() {
+		this.authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint();
+	}
+
+	@Test
+	public void commenceWhenNoBearerTokenErrorThenStatus401AndAuthHeader()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		this.authenticationEntryPoint.commence(request, response, new BadCredentialsException("test"));
+
+		assertThat(response.getStatus()).isEqualTo(401);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer");
+	}
+
+	@Test
+	public void commenceWhenNoBearerTokenErrorAndRealmSetThenStatus401AndAuthHeaderWithRealm()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		this.authenticationEntryPoint.setRealmName("test");
+		this.authenticationEntryPoint.commence(request, response, new BadCredentialsException("test"));
+
+		assertThat(response.getStatus()).isEqualTo(401);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer realm=\"test\"");
+	}
+
+	@Test
+	public void commenceWhenInvalidRequestErrorThenStatus400AndHeaderWithError()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		BearerTokenError error = new BearerTokenError(
+				BearerTokenErrorCodes.INVALID_REQUEST,
+				HttpStatus.BAD_REQUEST,
+				null,
+				null);
+
+		this.authenticationEntryPoint.commence(request, response,
+				new OAuth2AuthenticationException(error));
+
+		assertThat(response.getStatus()).isEqualTo(400);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"invalid_request\"");
+	}
+
+	@Test
+	public void commenceWhenInvalidRequestErrorThenStatus400AndHeaderWithErrorDetails()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, HttpStatus.BAD_REQUEST,
+				"The access token expired", null, null);
+
+		this.authenticationEntryPoint.commence(request, response,
+				new OAuth2AuthenticationException(error));
+
+		assertThat(response.getStatus()).isEqualTo(400);
+		assertThat(response.getHeader("WWW-Authenticate"))
+				.isEqualTo("Bearer error=\"invalid_request\", error_description=\"The access token expired\"");
+	}
+
+	@Test
+	public void commenceWhenInvalidRequestErrorThenStatus400AndHeaderWithErrorUri()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, HttpStatus.BAD_REQUEST,
+				null, "http://example.com", null);
+
+		this.authenticationEntryPoint.commence(request, response,
+				new OAuth2AuthenticationException(error));
+
+		assertThat(response.getStatus()).isEqualTo(400);
+		assertThat(response.getHeader("WWW-Authenticate"))
+				.isEqualTo("Bearer error=\"invalid_request\", error_uri=\"http://example.com\"");
+	}
+
+	@Test
+	public void commenceWhenInvalidTokenErrorThenStatus401AndHeaderWithError()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, HttpStatus.UNAUTHORIZED,
+				null, null);
+
+		this.authenticationEntryPoint.commence(request, response,
+				new OAuth2AuthenticationException(error));
+
+		assertThat(response.getStatus()).isEqualTo(401);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"invalid_token\"");
+	}
+
+	@Test
+	public void commenceWhenInsufficientScopeErrorThenStatus403AndHeaderWithError()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN,
+				null, null);
+
+		this.authenticationEntryPoint.commence(request, response,
+				new OAuth2AuthenticationException(error));
+
+		assertThat(response.getStatus()).isEqualTo(403);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\"");
+	}
+
+	@Test
+	public void commenceWhenInsufficientScopeErrorThenStatus403AndHeaderWithErrorAndScope()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN,
+				null, null, "test.read test.write");
+
+		this.authenticationEntryPoint.commence(request, response,
+				new OAuth2AuthenticationException(error));
+
+		assertThat(response.getStatus()).isEqualTo(403);
+		assertThat(response.getHeader("WWW-Authenticate"))
+				.isEqualTo("Bearer error=\"insufficient_scope\", scope=\"test.read test.write\"");
+	}
+
+	@Test
+	public void commenceWhenInsufficientScopeAndRealmSetThenStatus403AndHeaderWithErrorAndAllDetails()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN,
+				"Insufficient scope", "http://example.com", "test.read test.write");
+
+		this.authenticationEntryPoint.setRealmName("test");
+		this.authenticationEntryPoint.commence(request, response,
+				new OAuth2AuthenticationException(error));
+
+		assertThat(response.getStatus()).isEqualTo(403);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo(
+				"Bearer realm=\"test\", error=\"insufficient_scope\", error_description=\"Insufficient scope\", "
+						+ "error_uri=\"http://example.com\", scope=\"test.read test.write\"");
+	}
+
+	@Test
+	public void setRealmNameWhenNullRealmNameThenNoExceptionThrown() {
+		assertThatCode(() -> this.authenticationEntryPoint.setRealmName(null))
+				.doesNotThrowAnyException();
+	}
+
+}

+ 173 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilterTests.java

@@ -0,0 +1,173 @@
+/*
+ * 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.oauth2.server.resource.web;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.mock.web.MockFilterChain;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
+import org.springframework.security.oauth2.server.resource.BearerTokenError;
+import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests {@link BearerTokenAuthenticationFilterTests}
+ *
+ * @author Josh Cummings
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class BearerTokenAuthenticationFilterTests {
+	@Mock
+	AuthenticationEntryPoint authenticationEntryPoint;
+
+	@Mock
+	AuthenticationManager authenticationManager;
+
+	@Mock
+	BearerTokenResolver bearerTokenResolver;
+
+	MockHttpServletRequest request;
+
+	MockHttpServletResponse response;
+
+	MockFilterChain filterChain;
+
+	@InjectMocks
+	BearerTokenAuthenticationFilter filter;
+
+	@Before
+	public void httpMocks() {
+		this.request = new MockHttpServletRequest();
+		this.response = new MockHttpServletResponse();
+		this.filterChain = new MockFilterChain();
+	}
+
+	@Before
+	public void setterMocks() {
+		this.filter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
+		this.filter.setBearerTokenResolver(this.bearerTokenResolver);
+	}
+
+	@Test
+	public void doFilterWhenBearerTokenPresentThenAuthenticates() throws ServletException, IOException {
+		when(this.bearerTokenResolver.resolve(this.request)).thenReturn("token");
+
+		this.filter.doFilter(this.request, this.response, this.filterChain);
+
+		ArgumentCaptor<BearerTokenAuthenticationToken> captor =
+				ArgumentCaptor.forClass(BearerTokenAuthenticationToken.class);
+
+		verify(this.authenticationManager).authenticate(captor.capture());
+
+		assertThat(captor.getValue().getPrincipal()).isEqualTo("token");
+	}
+
+	@Test
+	public void doFilterWhenNoBearerTokenPresentThenDoesNotAuthenticate()
+			throws ServletException, IOException {
+
+		when(this.bearerTokenResolver.resolve(this.request)).thenReturn(null);
+
+		dontAuthenticate();
+	}
+
+	@Test
+	public void doFilterWhenMalformedBearerTokenThenPropagatesError() throws ServletException, IOException {
+		BearerTokenError error = new BearerTokenError(
+				BearerTokenErrorCodes.INVALID_REQUEST,
+				HttpStatus.BAD_REQUEST,
+				"description",
+				"uri");
+
+		OAuth2AuthenticationException exception = new OAuth2AuthenticationException(error);
+
+		when(this.bearerTokenResolver.resolve(this.request)).thenThrow(exception);
+
+		dontAuthenticate();
+
+		verify(this.authenticationEntryPoint).commence(this.request, this.response, exception);
+	}
+
+	@Test
+	public void doFilterWhenAuthenticationFailsThenPropagatesError() throws ServletException, IOException {
+		BearerTokenError error = new BearerTokenError(
+				BearerTokenErrorCodes.INVALID_TOKEN,
+				HttpStatus.UNAUTHORIZED,
+				"description",
+				"uri"
+		);
+
+		OAuth2AuthenticationException exception = new OAuth2AuthenticationException(error);
+
+		when(this.bearerTokenResolver.resolve(this.request)).thenReturn("token");
+		when(this.authenticationManager.authenticate(any(BearerTokenAuthenticationToken.class)))
+				.thenThrow(exception);
+
+		this.filter.doFilter(this.request, this.response, this.filterChain);
+
+		verify(this.authenticationEntryPoint).commence(this.request, this.response, exception);
+	}
+
+	@Test
+	public void setAuthenticationEntryPointWhenNullThenThrowsException() {
+		assertThatCode(() -> this.filter.setAuthenticationEntryPoint(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessageContaining("authenticationEntryPoint cannot be null");
+	}
+
+	@Test
+	public void setBearerTokenResolverWhenNullThenThrowsException() {
+		assertThatCode(() -> this.filter.setBearerTokenResolver(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessageContaining("bearerTokenResolver cannot be null");
+	}
+
+	@Test
+	public void constructorWhenNullAuthenticationManagerThenThrowsException() {
+		assertThatCode(() -> new BearerTokenAuthenticationFilter(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessageContaining("authenticationManager cannot be null");
+	}
+
+	private void dontAuthenticate()
+		throws ServletException, IOException {
+
+		this.filter.doFilter(this.request, this.response, this.filterChain);
+
+		verifyNoMoreInteractions(this.authenticationManager);
+	}
+}

+ 160 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java

@@ -0,0 +1,160 @@
+/*
+ * 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.oauth2.server.resource.web;
+
+import java.util.Base64;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/**
+ * Tests for {@link DefaultBearerTokenResolver}.
+ *
+ * @author Vedran Pavic
+ */
+public class DefaultBearerTokenResolverTests {
+
+	private static final String TEST_TOKEN = "test-token";
+
+	private DefaultBearerTokenResolver resolver;
+
+	@Before
+	public void setUp() {
+		this.resolver = new DefaultBearerTokenResolver();
+	}
+
+	@Test
+	public void resolveWhenValidHeaderIsPresentThenTokenIsResolved() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
+
+		assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
+	}
+
+	@Test
+	public void resolveWhenNoHeaderIsPresentThenTokenIsNotResolved() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+
+		assertThat(this.resolver.resolve(request)).isNull();
+	}
+
+	@Test
+	public void resolveWhenHeaderWithWrongSchemeIsPresentThenTokenIsNotResolved() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString("test:test".getBytes()));
+
+		assertThat(this.resolver.resolve(request)).isNull();
+	}
+
+	@Test
+	public void resolveWhenHeaderWithMissingTokenIsPresentThenAuthenticationExceptionIsThrown() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.addHeader("Authorization", "Bearer ");
+
+		assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class)
+				.hasMessageContaining(("Bearer token is malformed"));
+	}
+
+	@Test
+	public void resolveWhenHeaderWithInvalidCharactersIsPresentThenAuthenticationExceptionIsThrown() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.addHeader("Authorization", "Bearer an\"invalid\"token");
+
+		assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class)
+				.hasMessageContaining(("Bearer token is malformed"));
+	}
+
+	@Test
+	public void resolveWhenValidHeaderIsPresentTogetherWithFormParameterThenAuthenticationExceptionIsThrown() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
+		request.setMethod("POST");
+		request.setContentType("application/x-www-form-urlencoded");
+		request.addParameter("access_token", TEST_TOKEN);
+
+		assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class)
+				.hasMessageContaining("Found multiple bearer tokens in the request");
+	}
+
+	@Test
+	public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
+		request.setMethod("GET");
+		request.addParameter("access_token", TEST_TOKEN);
+
+		assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class)
+				.hasMessageContaining("Found multiple bearer tokens in the request");
+	}
+
+	@Test
+	public void resolveWhenRequestContainsTwoAccessTokenParametersThenAuthenticationExceptionIsThrown() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.addParameter("access_token", "token1", "token2");
+
+		assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class)
+				.hasMessageContaining("Found multiple bearer tokens in the request");
+	}
+
+	@Test
+	public void resolveWhenFormParameterIsPresentAndSupportedThenTokenIsResolved() {
+		this.resolver.setAllowFormEncodedBodyParameter(true);
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setMethod("POST");
+		request.setContentType("application/x-www-form-urlencoded");
+		request.addParameter("access_token", TEST_TOKEN);
+
+		assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
+	}
+
+	@Test
+	public void resolveWhenFormParameterIsPresentAndNotSupportedThenTokenIsNotResolved() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setMethod("POST");
+		request.setContentType("application/x-www-form-urlencoded");
+		request.addParameter("access_token", TEST_TOKEN);
+
+		assertThat(this.resolver.resolve(request)).isNull();
+	}
+
+	@Test
+	public void resolveWhenQueryParameterIsPresentAndSupportedThenTokenIsResolved() {
+		this.resolver.setAllowUriQueryParameter(true);
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setMethod("GET");
+		request.addParameter("access_token", TEST_TOKEN);
+
+		assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
+	}
+
+	@Test
+	public void resolveWhenQueryParameterIsPresentAndNotSupportedThenTokenIsNotResolved() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setMethod("GET");
+		request.addParameter("access_token", TEST_TOKEN);
+
+		assertThat(this.resolver.resolve(request)).isNull();
+	}
+}

+ 250 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandlerTests.java

@@ -0,0 +1,250 @@
+/*
+ * 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.oauth2.server.resource.web.access;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+
+import org.assertj.core.util.Maps;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AbstractOAuth2Token;
+import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/**
+ * Tests for {@link BearerTokenAccessDeniedHandlerTests}
+ *
+ * @author Josh Cummings
+ */
+public class BearerTokenAccessDeniedHandlerTests {
+	private BearerTokenAccessDeniedHandler accessDeniedHandler;
+
+	@Before
+	public void setUp() {
+		this.accessDeniedHandler = new BearerTokenAccessDeniedHandler();
+	}
+
+	@Test
+	public void handleWhenNotOAuth2AuthenticatedThenStatus403()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		Authentication authentication = new TestingAuthenticationToken("user", "pass");
+		request.setUserPrincipal(authentication);
+
+		this.accessDeniedHandler.handle(request, response, null);
+
+		assertThat(response.getStatus()).isEqualTo(403);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer");
+	}
+
+	@Test
+	public void handleWhenNotOAuth2AuthenticatedAndRealmSetThenStatus403AndAuthHeaderWithRealm()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		Authentication authentication = new TestingAuthenticationToken("user", "pass");
+		request.setUserPrincipal(authentication);
+
+		this.accessDeniedHandler.setRealmName("test");
+		this.accessDeniedHandler.handle(request, response, null);
+
+		assertThat(response.getStatus()).isEqualTo(403);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer realm=\"test\"");
+	}
+
+	@Test
+	public void handleWhenTokenHasNoScopesThenInsufficientScopeError()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		Authentication token = new TestingOAuth2TokenAuthenticationToken(Collections.emptyMap());
+		request.setUserPrincipal(token);
+
+		this.accessDeniedHandler.handle(request, response, null);
+
+		assertThat(response.getStatus()).isEqualTo(403);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " +
+				"error_description=\"The token provided has insufficient scope [] for this request\", " +
+				"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"");
+	}
+
+
+	@Test
+	public void handleWhenTokenHasScopeAttributeThenInsufficientScopeErrorWithScopes()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		Map<String, Object> attributes = Maps.newHashMap("scope", "message:read message:write");
+		Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes);
+		request.setUserPrincipal(token);
+
+		this.accessDeniedHandler.handle(request, response, null);
+
+		assertThat(response.getStatus()).isEqualTo(403);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " +
+				"error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " +
+				"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " +
+				"scope=\"message:read message:write\"");
+	}
+
+	@Test
+	public void handleWhenTokenHasEmptyScopeAttributeThenInsufficientScopeError()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		Map<String, Object> attributes = Maps.newHashMap("scope", "");
+		Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes);
+		request.setUserPrincipal(token);
+
+		this.accessDeniedHandler.handle(request, response, null);
+
+		assertThat(response.getStatus()).isEqualTo(403);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " +
+				"error_description=\"The token provided has insufficient scope [] for this request\", " +
+				"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"");
+	}
+
+	@Test
+	public void handleWhenTokenHasScpAttributeThenInsufficientScopeErrorWithScopes()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		Map<String, Object> attributes = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write"));
+		Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes);
+		request.setUserPrincipal(token);
+
+		this.accessDeniedHandler.handle(request, response, null);
+
+		assertThat(response.getStatus()).isEqualTo(403);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " +
+				"error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " +
+				"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " +
+				"scope=\"message:read message:write\"");
+	}
+
+	@Test
+	public void handleWhenTokenHasEmptyScpAttributeThenInsufficientScopeError()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		Map<String, Object> attributes = Maps.newHashMap("scp", Collections.emptyList());
+		Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes);
+		request.setUserPrincipal(token);
+
+		this.accessDeniedHandler.handle(request, response, null);
+
+		assertThat(response.getStatus()).isEqualTo(403);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " +
+				"error_description=\"The token provided has insufficient scope [] for this request\", " +
+				"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"");
+	}
+
+	@Test
+	public void handleWhenTokenHasBothScopeAndScpAttributesTheInsufficientErrorBasedOnScopeAttribute()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		Map<String, Object> attributes = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write"));
+		Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes);
+		request.setUserPrincipal(token);
+		attributes.put("scope", "missive:read missive:write");
+
+		this.accessDeniedHandler.handle(request, response, null);
+
+		assertThat(response.getStatus()).isEqualTo(403);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " +
+				"error_description=\"The token provided has insufficient scope [missive:read missive:write] for this request\", " +
+				"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " +
+				"scope=\"missive:read missive:write\"");
+	}
+
+	@Test
+	public void handleWhenTokenHasScopeAttributeAndRealmIsSetThenInsufficientScopeErrorWithScopesAndRealm()
+			throws Exception {
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		Map<String, Object> attributes = Maps.newHashMap("scope", "message:read message:write");
+		Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes);
+		request.setUserPrincipal(token);
+
+		this.accessDeniedHandler.setRealmName("test");
+		this.accessDeniedHandler.handle(request, response, null);
+
+		assertThat(response.getStatus()).isEqualTo(403);
+		assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer realm=\"test\", " +
+				"error=\"insufficient_scope\", " +
+				"error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " +
+				"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " +
+				"scope=\"message:read message:write\"");
+	}
+
+	@Test
+	public void setRealmNameWhenNullRealmNameThenNoExceptionThrown() {
+		assertThatCode(() -> this.accessDeniedHandler.setRealmName(null))
+				.doesNotThrowAnyException();
+	}
+
+	static class TestingOAuth2TokenAuthenticationToken
+			extends AbstractOAuth2TokenAuthenticationToken<TestingOAuth2TokenAuthenticationToken.TestingOAuth2Token> {
+
+		private Map<String, Object> attributes;
+
+		protected TestingOAuth2TokenAuthenticationToken(Map<String, Object> attributes) {
+			super(new TestingOAuth2Token("token"));
+			this.attributes = attributes;
+		}
+
+		@Override
+		public Map<String, Object> getTokenAttributes() {
+			return this.attributes;
+		}
+
+		static class TestingOAuth2Token extends AbstractOAuth2Token {
+			public TestingOAuth2Token(String tokenValue) {
+				super(tokenValue);
+			}
+		}
+	}
+}

+ 104 - 0
samples/boot/oauth2resourceserver/README.adoc

@@ -0,0 +1,104 @@
+= OAuth 2.0 Resource Server Sample
+
+This sample demonstrates integrating Resource Server with a mock Authorization Server, though it can be modified to integrate
+with your favorite Authorization Server.
+
+With it, you can run the integration tests or run the application as a stand-alone service to explore how you can
+secure your own service with OAuth 2.0 Bearer Tokens using Spring Security.
+
+== 1. Running the tests
+
+To run the tests, do:
+
+```bash
+./gradlew integrationTest
+```
+
+Or import the project into your IDE and run `OAuth2ResourceServerApplicationTests` from there.
+
+=== What is it doing?
+
+By default, the tests are pointing at a mock Authorization Server instance.
+
+The tests are configured with a set of hard-coded tokens originally obtained from the mock Authorization Server,
+and each makes a query to the Resource Server with their corresponding token.
+
+The Resource Server subsquently verifies with the Authorization Server and authorizes the request, returning the phrase
+
+```bash
+Hello, subject!
+```
+
+where "subject" is the value of the `sub` field in the JWT returned by the Authorization Server.
+
+== 2. Running the app
+
+To run as a stand-alone application, do:
+
+```bash
+./gradlew bootRun
+```
+
+Or import the project into your IDE and run `OAuth2ResourceServerApplication` from there.
+
+Once it is up, you can use the following token:
+
+```bash
+export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjo0NjgzODA1MTI4fQ.ULEPdHG-MK5GlrTQMhgqcyug2brTIZaJIrahUeq9zaiwUSdW83fJ7W1IDd2Z3n4a25JY2uhEcoV95lMfccHR6y_2DLrNvfta22SumY9PEDF2pido54LXG6edIGgarnUbJdR4rpRe_5oRGVa8gDx8FnuZsNv6StSZHAzw5OsuevSTJ1UbJm4UfX3wiahFOQ2OI6G-r5TB2rQNdiPHuNyzG5yznUqRIZ7-GCoMqHMaC-1epKxiX8gYXRROuUYTtcMNa86wh7OVDmvwVmFioRcR58UWBRoO1XQexTtOQq_t8KYsrPZhb9gkyW8x2bAQF-d0J0EJY8JslaH6n4RBaZISww
+```
+
+And then make this request:
+
+```bash
+curl -H "Authorization: Bearer $TOKEN" localhost:8080
+```
+
+Which will respond with the phrase:
+
+```bash
+Hello, subject!
+```
+
+where `subject` is the value of the `sub` field in the JWT returned by the Authorization Server.
+
+Or this:
+
+```bash
+export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOiJtZXNzYWdlOnJlYWQiLCJleHAiOjQ2ODM4MDUxNDF9.h-j6FKRFdnTdmAueTZCdep45e6DPwqM68ZQ8doIJ1exi9YxAlbWzOwId6Bd0L5YmCmp63gGQgsBUBLzwnZQ8kLUgUOBEC3UzSWGRqMskCY9_k9pX0iomX6IfF3N0PaYs0WPC4hO1s8wfZQ-6hKQ4KigFi13G9LMLdH58PRMK0pKEvs3gCbHJuEPw-K5ORlpdnleUTQIwINafU57cmK3KocTeknPAM_L716sCuSYGvDl6xUTXO7oPdrXhS_EhxLP6KxrpI1uD4Ea_5OWTh7S0Wx5LLDfU6wBG1DowN20d374zepOIEkR-Jnmr_QlR44vmRqS5ncrF-1R0EGcPX49U6A
+
+curl -H "Authorization: Bearer $TOKEN" localhost:8080/message
+```
+
+Will respond with:
+
+```bash
+secret message
+```
+
+== 2. Testing against other Authorization Servers
+
+_In order to use this sample, your Authorization Server must support JWTs that either use the "scope" or "scp" attribute._
+
+_Additionally, remember that if your authorization server is running locally on port 8080, you'll need to change the sample's port in the `application.yml` by adding something like `server.port: 8082`._
+
+To change the sample to point at your Authorization Server, simply find this property in the `application.yml`:
+
+```yaml
+sample.jwk-set-uri: mock://localhost:8081/.well-known/jwks.json
+```
+
+And change the property to your Authorization Server's JWK set endpoint:
+
+```yaml
+sample.jwk-set-uri: https://dev-123456.oktapreview.com/oauth2/default/v1/keys
+```
+
+And then you can run the app the same as before:
+
+```bash
+./gradlew bootRun
+```
+
+Make sure to obtain valid tokens from your Authorization Server in order to play with the sample Resource Server.
+To use the `/` endpoint, any valid token from your Authorization Server will do.
+To use the `/message` endpoint, the token should have the `message:read` scope.

+ 13 - 0
samples/boot/oauth2resourceserver/spring-security-samples-boot-oauth2resourceserver.gradle

@@ -0,0 +1,13 @@
+apply plugin: 'io.spring.convention.spring-sample-boot'
+
+dependencies {
+	compile project(':spring-security-config')
+	compile project(':spring-security-oauth2-jose')
+	compile project(':spring-security-oauth2-resource-server')
+
+	compile 'org.springframework.boot:spring-boot-starter-web'
+	compile 'com.squareup.okhttp3:mockwebserver'
+
+	testCompile project(':spring-security-test')
+	testCompile 'org.springframework.boot:spring-boot-starter-test'
+}

+ 101 - 0
samples/boot/oauth2resourceserver/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java

@@ -0,0 +1,101 @@
+/*
+ * 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
+ *
+ *      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 sample;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpHeaders;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.RequestPostProcessor;
+
+import static org.hamcrest.Matchers.containsString;
+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.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Integration tests for {@link OAuth2ResourceServerApplication}
+ *
+ * @author Josh Cummings
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+public class OAuth2ResourceServerApplicationITests {
+
+	String noScopesToken = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjo0NjgzODA1MTI4fQ.ULEPdHG-MK5GlrTQMhgqcyug2brTIZaJIrahUeq9zaiwUSdW83fJ7W1IDd2Z3n4a25JY2uhEcoV95lMfccHR6y_2DLrNvfta22SumY9PEDF2pido54LXG6edIGgarnUbJdR4rpRe_5oRGVa8gDx8FnuZsNv6StSZHAzw5OsuevSTJ1UbJm4UfX3wiahFOQ2OI6G-r5TB2rQNdiPHuNyzG5yznUqRIZ7-GCoMqHMaC-1epKxiX8gYXRROuUYTtcMNa86wh7OVDmvwVmFioRcR58UWBRoO1XQexTtOQq_t8KYsrPZhb9gkyW8x2bAQF-d0J0EJY8JslaH6n4RBaZISww";
+	String messageReadToken = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOiJtZXNzYWdlOnJlYWQiLCJleHAiOjQ2ODM4MDUxNDF9.h-j6FKRFdnTdmAueTZCdep45e6DPwqM68ZQ8doIJ1exi9YxAlbWzOwId6Bd0L5YmCmp63gGQgsBUBLzwnZQ8kLUgUOBEC3UzSWGRqMskCY9_k9pX0iomX6IfF3N0PaYs0WPC4hO1s8wfZQ-6hKQ4KigFi13G9LMLdH58PRMK0pKEvs3gCbHJuEPw-K5ORlpdnleUTQIwINafU57cmK3KocTeknPAM_L716sCuSYGvDl6xUTXO7oPdrXhS_EhxLP6KxrpI1uD4Ea_5OWTh7S0Wx5LLDfU6wBG1DowN20d374zepOIEkR-Jnmr_QlR44vmRqS5ncrF-1R0EGcPX49U6A";
+
+	@Autowired
+	MockMvc mvc;
+
+	@Test
+	public void performWhenValidBearerTokenThenAllows()
+		throws Exception {
+
+		this.mvc.perform(get("/").with(bearerToken(this.noScopesToken)))
+				.andExpect(status().isOk())
+				.andExpect(content().string(containsString("Hello, subject!")));
+	}
+
+	// -- tests with scopes
+
+	@Test
+	public void performWhenValidBearerTokenThenScopedRequestsAlsoWork()
+			throws Exception {
+
+		this.mvc.perform(get("/message").with(bearerToken(this.messageReadToken)))
+				.andExpect(status().isOk())
+				.andExpect(content().string(containsString("secret message")));
+	}
+
+	@Test
+	public void performWhenInsufficientlyScopedBearerTokenThenDeniesScopedMethodAccess()
+			throws Exception {
+
+		this.mvc.perform(get("/message").with(bearerToken(this.noScopesToken)))
+				.andExpect(status().isForbidden())
+				.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE,
+						containsString("Bearer error=\"insufficient_scope\"")));
+	}
+
+	private static class BearerTokenRequestPostProcessor implements RequestPostProcessor {
+		private String token;
+
+		public BearerTokenRequestPostProcessor(String token) {
+			this.token = token;
+		}
+
+		@Override
+		public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
+			request.addHeader("Authorization", "Bearer " + this.token);
+			return request;
+		}
+	}
+
+	private static BearerTokenRequestPostProcessor bearerToken(String token) {
+		return new BearerTokenRequestPostProcessor(token);
+	}
+}

+ 1 - 0
samples/boot/oauth2resourceserver/src/integration-test/resources/application-test.yml

@@ -0,0 +1 @@
+sample.jwk-set-uri: mock://localhost:0/.well-known/jwks.json

+ 30 - 0
samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerApplication.java

@@ -0,0 +1,30 @@
+/*
+ * 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 sample;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author Josh Cummings
+ */
+@SpringBootApplication
+public class OAuth2ResourceServerApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(OAuth2ResourceServerApplication.class, args);
+	}
+}

+ 38 - 0
samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerController.java

@@ -0,0 +1,38 @@
+/*
+ * 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 sample;
+
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author Josh Cummings
+ */
+@RestController
+public class OAuth2ResourceServerController {
+
+	@GetMapping("/")
+	public String index(@AuthenticationPrincipal Jwt jwt) {
+		return String.format("Hello, %s!", jwt.getSubject());
+	}
+
+	@GetMapping("/message")
+	public String message() {
+		return "secret message";
+	}
+}

+ 45 - 0
samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java

@@ -0,0 +1,45 @@
+/*
+ * 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 sample;
+
+import org.springframework.beans.factory.annotation.Value;
+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;
+
+/**
+ * @author Josh Cummings
+ */
+@EnableWebSecurity
+public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter {
+	@Value("${sample.jwk-set-uri}")
+	String jwkSetUri;
+
+	@Override
+	protected void configure(HttpSecurity http) throws Exception {
+		// @formatter:off
+		http
+			.authorizeRequests()
+				.antMatchers("/message/**").access("hasAuthority('SCOPE_message:read')")
+				.anyRequest().authenticated()
+				.and()
+			.oauth2()
+				.resourceServer()
+					.jwt()
+						.jwkSetUri(this.jwkSetUri);
+		// @formatter:on
+	}
+}

+ 115 - 0
samples/boot/oauth2resourceserver/src/main/java/sample/provider/MockProvider.java

@@ -0,0 +1,115 @@
+/*
+ * 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 sample.provider;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.PreDestroy;
+
+import okhttp3.mockwebserver.Dispatcher;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.MapPropertySource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+
+/**
+ * This is a miminal mock server that serves as a placeholder for a real Authorization Server (AS).
+ *
+ * For the sample to work, the AS used must support a JWK endpoint.
+ *
+ * For the integration tests to work, the AS used must be able to issue a token
+ * with the following characteristics:
+ *
+ * - The token has the "message:read" scope
+ * - The token has a "sub" of "subject"
+ * - The token is signed by a RS256 private key whose public key counterpart is served from the JWK endpoint of the AS.
+ *
+ * There is also a test that verifies insufficient scope. In that case, the token should have the following characteristics:
+ *
+ * - The token is missing the "message:read" scope
+ * - The token is signed by a RS256 private key whose public key counterpart is served from the JWK endpoint of the AS.
+ *
+ * @author Josh Cummings
+ */
+public class MockProvider implements EnvironmentPostProcessor {
+	private MockWebServer server = new MockWebServer();
+
+	private static final MockResponse JWKS_RESPONSE = response(
+			"{\"keys\":[{\"p\":\"2p-ViY7DE9ZrdWQb544m0Jp7Cv03YCSljqfim9pD4ALhObX0OrAznOiowTjwBky9JGffMwDBVSfJSD9TSU7aH2sbbfi0bZLMdekKAuimudXwUqPDxrrg0BCyvCYgLmKjbVT3zcdylWSog93CNTxGDPzauu-oc0XPNKCXnaDpNvE\",\"kty\":\"RSA\",\"q\":\"sP_QYavrpBvSJ86uoKVGj2AGl78CSsAtpf1ybSY5TwUlorXSdqapRbY69Y271b0aMLzlleUn9ZTBO1dlKV2_dw_lPADHVia8z3pxL-8sUhIXLsgj4acchMk4c9YX-sFh07xENnyZ-_TXm3llPLuL67HUfBC2eKe800TmCYVWc9U\",\"d\":\"bn1nFxCQT4KLTHqo8mo9HvHD0cRNRNdWcKNnnEQkCF6tKbt-ILRyQGP8O40axLd7CoNVG9c9p_-g4-2kwCtLJNv_STLtwfpCY7VN5o6-ZIpfTjiW6duoPrLWq64Hm_4LOBQTiZfUPcLhsuJRHbWqakj-kV_YbUyC2Ocf_dd8IAQcSrAU2SCcDebhDCWwRUFvaa9V5eq0851S9goaA-AJz-JXyePH6ZFr8JxmWkWxYZ5kdcMD-sm9ZbxE0CaEk32l4fE4hR-L8x2dDtjWA-ahKCZ091z-gV3HWtR2JOjvxoNRjxUo3UxaGiFJHWNIl0EYUJZu1Cb-5wIlEI7wPx5mwQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"qS0OK48M2CIAA6_4Wdw4EbCaAfcTLf5Oy9t5BOF_PFUKqoSpZ6JsT5H0a_4zkjt-oI969v78OTlvBKbmEyKO-KeytzHBAA5CsLmVcz0THrMSg6oXZqu66MPnvWoZN9FEN5TklPOvBFm8Bg1QZ3k-YMVaM--DLvhaYR95_mqaz50\",\"dp\":\"Too2NozLGD1XrXyhabZvy1E0EuaVFj0UHQPDLSpkZ_2g3BK6Art6T0xmE8RYtmqrKIEIdlI3IliAvyvAx_1D7zWTTRaj-xlZyqJFrnXWL7zj8UxT8PkB-r2E-ILZ3NAi1gxIWezlBTZ8M6NfObDFmbTc_3tJkN_raISo8z_ziIE\",\"dq\":\"U0yhSkY5yOsa9YcMoigGVBWSJLpNHtbg5NypjHrPv8OhWbkOSq7WvSstBkFk5AtyFvvfZLMLIkWWxxGzV0t6f1MoxBtttLrYYyCxwihiiGFhLbAdSuZ1wnxcqA9bC7UVECvrQmVTpsMs8UupfHKbQBpZ8OWAqrnuYNNtG4_4Bt0\",\"n\":\"lygtuZj0lJjqOqIWocF8Bb583QDdq-aaFg8PesOp2-EDda6GqCpL-_NZVOflNGX7XIgjsWHcPsQHsV9gWuOzSJ0iEuWvtQ6eGBP5M6m7pccLNZfwUse8Cb4Ngx3XiTlyuqM7pv0LPyppZusfEHVEdeelou7Dy9k0OQ_nJTI3b2E1WBoHC58CJ453lo4gcBm1efURN3LIVc1V9NQY_ESBKVdwqYyoJPEanURLVGRd6cQKn6YrCbbIRHjqAyqOE-z3KmgDJnPriljfR5XhSGyM9eqD9Xpy6zu_MAeMJJfSArp857zLPk-Wf5VP9STAcjyfdBIybMKnwBYr2qHMT675hQ\"}]}",
+			200
+	);
+
+	private static final MockResponse NOT_FOUND_RESPONSE = response(
+			"{ \"message\" : \"This mock authorization server responds to just one request: GET /.well-known/jwks.json.\" }",
+			404
+	);
+
+	public MockProvider() throws IOException {
+		Dispatcher dispatcher = new Dispatcher() {
+			@Override
+			public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
+				if ("/.well-known/jwks.json".equals(request.getPath())) {
+					return JWKS_RESPONSE;
+				}
+
+				return NOT_FOUND_RESPONSE;
+			}
+		};
+
+		this.server.setDispatcher(dispatcher);
+	}
+
+	@Override
+	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
+		String uri = environment.getProperty("sample.jwk-set-uri", "mock://localhost:0");
+
+		if (uri.startsWith("mock://")) {
+			try {
+				this.server.start(URI.create(uri).getPort());
+			} catch (IOException e) {
+				throw new IllegalStateException(e);
+			}
+
+			Map<String, Object> properties = new HashMap<>();
+			String url = this.server.url("/.well-known/jwks.json").toString();
+			properties.put("sample.jwk-set-uri", url);
+
+			MapPropertySource propertySource = new MapPropertySource("mock", properties);
+			environment.getPropertySources().addFirst(propertySource);
+		}
+	}
+
+	@PreDestroy
+	public void shutdown() throws IOException {
+		this.server.shutdown();
+	}
+
+	private static MockResponse response(String body, int status) {
+		return new MockResponse()
+				.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+				.setResponseCode(status)
+				.setBody(body);
+	}
+}

+ 1 - 0
samples/boot/oauth2resourceserver/src/main/resources/META-INF/spring.factories

@@ -0,0 +1 @@
+org.springframework.boot.env.EnvironmentPostProcessor=sample.provider.MockProvider

+ 1 - 0
samples/boot/oauth2resourceserver/src/main/resources/application.yml

@@ -0,0 +1 @@
+sample.jwk-set-uri: mock://localhost:8081/.well-known/jwks.json

部分文件因为文件数量过多而无法显示