|  | @@ -64,11 +64,17 @@ import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
 | 
	
		
			
				|  |  |  import org.springframework.security.oauth2.jwt.Jwt;
 | 
	
		
			
				|  |  |  import org.springframework.security.oauth2.jwt.JwtClaimNames;
 | 
	
		
			
				|  |  |  import org.springframework.security.oauth2.jwt.JwtDecoder;
 | 
	
		
			
				|  |  | +import org.springframework.security.oauth2.jwt.JwtException;
 | 
	
		
			
				|  |  |  import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
 | 
	
		
			
				|  |  |  import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
 | 
	
		
			
				|  |  | +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
 | 
	
		
			
				|  |  |  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.provisioning.InMemoryUserDetailsManager;
 | 
	
		
			
				|  |  | +import org.springframework.security.web.AuthenticationEntryPoint;
 | 
	
		
			
				|  |  | +import org.springframework.security.web.access.AccessDeniedHandler;
 | 
	
		
			
				|  |  | +import org.springframework.security.web.access.AccessDeniedHandlerImpl;
 | 
	
		
			
				|  |  |  import org.springframework.test.web.servlet.MockMvc;
 | 
	
		
			
				|  |  |  import org.springframework.test.web.servlet.MvcResult;
 | 
	
		
			
				|  |  |  import org.springframework.test.web.servlet.ResultMatcher;
 | 
	
	
		
			
				|  | @@ -85,6 +91,7 @@ import org.springframework.web.context.support.GenericWebApplicationContext;
 | 
	
		
			
				|  |  |  import static org.assertj.core.api.Assertions.assertThat;
 | 
	
		
			
				|  |  |  import static org.assertj.core.api.Assertions.assertThatCode;
 | 
	
		
			
				|  |  |  import static org.hamcrest.CoreMatchers.containsString;
 | 
	
		
			
				|  |  | +import static org.hamcrest.core.StringStartsWith.startsWith;
 | 
	
		
			
				|  |  |  import static org.mockito.ArgumentMatchers.anyString;
 | 
	
		
			
				|  |  |  import static org.mockito.Mockito.mock;
 | 
	
		
			
				|  |  |  import static org.mockito.Mockito.when;
 | 
	
	
		
			
				|  | @@ -784,8 +791,101 @@ public class OAuth2ResourceServerConfigurerTests {
 | 
	
		
			
				|  |  |  				.isInstanceOf(NoUniqueBeanDefinitionException.class);
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +	// -- exception handling
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	@Test
 | 
	
		
			
				|  |  | +	public void requestWhenRealmNameConfiguredThenUsesOnUnauthenticated()
 | 
	
		
			
				|  |  | +			throws Exception {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		this.spring.register(RealmNameConfiguredOnEntryPoint.class, JwtDecoderConfig.class).autowire();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
 | 
	
		
			
				|  |  | +		when(decoder.decode(anyString())).thenThrow(JwtException.class);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		this.mvc.perform(get("/authenticated")
 | 
	
		
			
				|  |  | +				.with(bearerToken("invalid_token")))
 | 
	
		
			
				|  |  | +				.andExpect(status().isUnauthorized())
 | 
	
		
			
				|  |  | +				.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer realm=\"myRealm\"")));
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	@Test
 | 
	
		
			
				|  |  | +	public void requestWhenRealmNameConfiguredThenUsesOnAccessDenied()
 | 
	
		
			
				|  |  | +			throws Exception {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		this.spring.register(RealmNameConfiguredOnAccessDeniedHandler.class, JwtDecoderConfig.class).autowire();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
 | 
	
		
			
				|  |  | +		when(decoder.decode(anyString())).thenReturn(JWT);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		this.mvc.perform(get("/authenticated")
 | 
	
		
			
				|  |  | +				.with(bearerToken("insufficiently_scoped")))
 | 
	
		
			
				|  |  | +				.andExpect(status().isForbidden())
 | 
	
		
			
				|  |  | +				.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer realm=\"myRealm\"")));
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	@Test
 | 
	
		
			
				|  |  | +	public void authenticationEntryPointWhenGivenNullThenThrowsException() {
 | 
	
		
			
				|  |  | +		ApplicationContext context = mock(ApplicationContext.class);
 | 
	
		
			
				|  |  | +		OAuth2ResourceServerConfigurer configurer = new OAuth2ResourceServerConfigurer(context);
 | 
	
		
			
				|  |  | +		assertThatCode(() -> configurer.authenticationEntryPoint(null))
 | 
	
		
			
				|  |  | +				.isInstanceOf(IllegalArgumentException.class);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	@Test
 | 
	
		
			
				|  |  | +	public void accessDeniedHandlerWhenGivenNullThenThrowsException() {
 | 
	
		
			
				|  |  | +		ApplicationContext context = mock(ApplicationContext.class);
 | 
	
		
			
				|  |  | +		OAuth2ResourceServerConfigurer configurer = new OAuth2ResourceServerConfigurer(context);
 | 
	
		
			
				|  |  | +		assertThatCode(() -> configurer.accessDeniedHandler(null))
 | 
	
		
			
				|  |  | +				.isInstanceOf(IllegalArgumentException.class);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  	// -- In combination with other authentication providers
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +	@Test
 | 
	
		
			
				|  |  | +	public void requestWhenBasicAndResourceServerEntryPointsThenMatchedByRequest()
 | 
	
		
			
				|  |  | +			throws Exception {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		this.spring.register(BasicAndResourceServerConfig.class, JwtDecoderConfig.class).autowire();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
 | 
	
		
			
				|  |  | +		when(decoder.decode(anyString())).thenThrow(JwtException.class);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		this.mvc.perform(get("/authenticated")
 | 
	
		
			
				|  |  | +				.with(httpBasic("some", "user")))
 | 
	
		
			
				|  |  | +				.andExpect(status().isUnauthorized())
 | 
	
		
			
				|  |  | +				.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Basic")));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		this.mvc.perform(get("/authenticated"))
 | 
	
		
			
				|  |  | +				.andExpect(status().isUnauthorized())
 | 
	
		
			
				|  |  | +				.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Basic")));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		this.mvc.perform(get("/authenticated")
 | 
	
		
			
				|  |  | +				.with(bearerToken("invalid_token")))
 | 
	
		
			
				|  |  | +				.andExpect(status().isUnauthorized())
 | 
	
		
			
				|  |  | +				.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer")));
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	@Test
 | 
	
		
			
				|  |  | +	public void requestWhenDefaultAndResourceServerAccessDeniedHandlersThenMatchedByRequest()
 | 
	
		
			
				|  |  | +			throws Exception {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		this.spring.register(ExceptionHandlingAndResourceServerWithAccessDeniedHandlerConfig.class,
 | 
	
		
			
				|  |  | +				JwtDecoderConfig.class).autowire();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
 | 
	
		
			
				|  |  | +		when(decoder.decode(anyString())).thenReturn(JWT);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		this.mvc.perform(get("/authenticated")
 | 
	
		
			
				|  |  | +				.with(httpBasic("basic-user", "basic-password")))
 | 
	
		
			
				|  |  | +				.andExpect(status().isForbidden())
 | 
	
		
			
				|  |  | +				.andExpect(header().doesNotExist(HttpHeaders.WWW_AUTHENTICATE));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		this.mvc.perform(get("/authenticated")
 | 
	
		
			
				|  |  | +				.with(bearerToken("insufficiently_scoped")))
 | 
	
		
			
				|  |  | +				.andExpect(status().isForbidden())
 | 
	
		
			
				|  |  | +				.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer")));
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  	@Test
 | 
	
		
			
				|  |  |  	public void getWhenAlsoUsingHttpBasicThenCorrectProviderEngages()
 | 
	
		
			
				|  |  |  			throws Exception {
 | 
	
	
		
			
				|  | @@ -901,6 +1001,85 @@ public class OAuth2ResourceServerConfigurerTests {
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +	@EnableWebSecurity
 | 
	
		
			
				|  |  | +	static class RealmNameConfiguredOnEntryPoint extends WebSecurityConfigurerAdapter {
 | 
	
		
			
				|  |  | +		@Override
 | 
	
		
			
				|  |  | +		protected void configure(HttpSecurity http) throws Exception {
 | 
	
		
			
				|  |  | +			// @formatter:off
 | 
	
		
			
				|  |  | +			http
 | 
	
		
			
				|  |  | +				.authorizeRequests()
 | 
	
		
			
				|  |  | +					.anyRequest().authenticated()
 | 
	
		
			
				|  |  | +					.and()
 | 
	
		
			
				|  |  | +				.oauth2()
 | 
	
		
			
				|  |  | +					.resourceServer()
 | 
	
		
			
				|  |  | +						.authenticationEntryPoint(authenticationEntryPoint())
 | 
	
		
			
				|  |  | +						.jwt();
 | 
	
		
			
				|  |  | +			// @formatter:on
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		AuthenticationEntryPoint authenticationEntryPoint() {
 | 
	
		
			
				|  |  | +			BearerTokenAuthenticationEntryPoint entryPoint =
 | 
	
		
			
				|  |  | +					new BearerTokenAuthenticationEntryPoint();
 | 
	
		
			
				|  |  | +			entryPoint.setRealmName("myRealm");
 | 
	
		
			
				|  |  | +			return entryPoint;
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	@EnableWebSecurity
 | 
	
		
			
				|  |  | +	static class RealmNameConfiguredOnAccessDeniedHandler extends WebSecurityConfigurerAdapter {
 | 
	
		
			
				|  |  | +		@Override
 | 
	
		
			
				|  |  | +		protected void configure(HttpSecurity http) throws Exception {
 | 
	
		
			
				|  |  | +			// @formatter:off
 | 
	
		
			
				|  |  | +			http
 | 
	
		
			
				|  |  | +				.authorizeRequests()
 | 
	
		
			
				|  |  | +					.anyRequest().denyAll()
 | 
	
		
			
				|  |  | +					.and()
 | 
	
		
			
				|  |  | +				.oauth2()
 | 
	
		
			
				|  |  | +					.resourceServer()
 | 
	
		
			
				|  |  | +						.accessDeniedHandler(accessDeniedHandler())
 | 
	
		
			
				|  |  | +						.jwt();
 | 
	
		
			
				|  |  | +			// @formatter:on
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		AccessDeniedHandler accessDeniedHandler() {
 | 
	
		
			
				|  |  | +			BearerTokenAccessDeniedHandler accessDeniedHandler =
 | 
	
		
			
				|  |  | +					new BearerTokenAccessDeniedHandler();
 | 
	
		
			
				|  |  | +			accessDeniedHandler.setRealmName("myRealm");
 | 
	
		
			
				|  |  | +			return accessDeniedHandler;
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	@EnableWebSecurity
 | 
	
		
			
				|  |  | +	static class ExceptionHandlingAndResourceServerWithAccessDeniedHandlerConfig extends WebSecurityConfigurerAdapter {
 | 
	
		
			
				|  |  | +		@Override
 | 
	
		
			
				|  |  | +		protected void configure(HttpSecurity http) throws Exception {
 | 
	
		
			
				|  |  | +			// @formatter:off
 | 
	
		
			
				|  |  | +			http
 | 
	
		
			
				|  |  | +				.authorizeRequests()
 | 
	
		
			
				|  |  | +					.anyRequest().denyAll()
 | 
	
		
			
				|  |  | +					.and()
 | 
	
		
			
				|  |  | +				.exceptionHandling()
 | 
	
		
			
				|  |  | +					.defaultAccessDeniedHandlerFor(new AccessDeniedHandlerImpl(), request -> false)
 | 
	
		
			
				|  |  | +					.and()
 | 
	
		
			
				|  |  | +				.httpBasic()
 | 
	
		
			
				|  |  | +					.and()
 | 
	
		
			
				|  |  | +				.oauth2()
 | 
	
		
			
				|  |  | +					.resourceServer()
 | 
	
		
			
				|  |  | +						.jwt();
 | 
	
		
			
				|  |  | +			// @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 BasicAndResourceServerConfig extends WebSecurityConfigurerAdapter {
 | 
	
		
			
				|  |  |  		@Value("${mock.jwk-set-uri:https://example.org}") String uri;
 |