Răsfoiți Sursa

Jwt -> Authentication Conversion

Exposes ability to specify a strategy for converting Jwt into an
Authentication, specifically in JwtAuthenticationProvider.

Fixes: gh-5629
Josh Cummings 7 ani în urmă
părinte
comite
d610f31425

+ 30 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java

@@ -19,6 +19,8 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.se
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequest;
 
 
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContext;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
 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.AbstractHttpConfigurer;
@@ -26,8 +28,10 @@ import org.springframework.security.config.annotation.web.configurers.CsrfConfig
 import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
 import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
 import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
 import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
 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.BearerTokenAuthenticationEntryPoint;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
@@ -71,6 +75,15 @@ import org.springframework.util.Assert;
  * </li>
  * </li>
  * </ul>
  * </ul>
  *
  *
+ * Also with {@link #jwt()} consider
+ *
+ * <ul>
+ * <li>
+ * customizing the conversion from a {@link Jwt} to an {@link org.springframework.security.core.Authentication} with
+ * {@link JwtConfigurer#jwtAuthenticationConverter(Converter)}
+ * </li>
+ * </ul>
+ *
  * <h2>Security Filters</h2>
  * <h2>Security Filters</h2>
  *
  *
  * The following {@code Filter}s are populated when {@link #jwt()} is configured:
  * The following {@code Filter}s are populated when {@link #jwt()} is configured:
@@ -182,9 +195,12 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 		}
 		}
 
 
 		JwtDecoder decoder = this.jwtConfigurer.getJwtDecoder();
 		JwtDecoder decoder = this.jwtConfigurer.getJwtDecoder();
+		Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter =
+				this.jwtConfigurer.getJwtAuthenticationConverter();
 
 
 		JwtAuthenticationProvider provider =
 		JwtAuthenticationProvider provider =
 				new JwtAuthenticationProvider(decoder);
 				new JwtAuthenticationProvider(decoder);
+		provider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
 		provider = postProcess(provider);
 		provider = postProcess(provider);
 
 
 		http.authenticationProvider(provider);
 		http.authenticationProvider(provider);
@@ -195,6 +211,9 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 
 
 		private JwtDecoder decoder;
 		private JwtDecoder decoder;
 
 
+		private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter =
+				new JwtAuthenticationConverter();
+
 		JwtConfigurer(ApplicationContext context) {
 		JwtConfigurer(ApplicationContext context) {
 			this.context = context;
 			this.context = context;
 		}
 		}
@@ -209,10 +228,21 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 			return this;
 			return this;
 		}
 		}
 
 
+		public JwtConfigurer jwtAuthenticationConverter
+				(Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter) {
+
+			this.jwtAuthenticationConverter = jwtAuthenticationConverter;
+			return this;
+		}
+
 		public OAuth2ResourceServerConfigurer<H> and() {
 		public OAuth2ResourceServerConfigurer<H> and() {
 			return OAuth2ResourceServerConfigurer.this;
 			return OAuth2ResourceServerConfigurer.this;
 		}
 		}
 
 
+		Converter<Jwt, ? extends AbstractAuthenticationToken> getJwtAuthenticationConverter() {
+			return this.jwtAuthenticationConverter;
+		}
+
 		JwtDecoder getJwtDecoder() {
 		JwtDecoder getJwtDecoder() {
 			if ( this.decoder == null ) {
 			if ( this.decoder == null ) {
 				return this.context.getBean(JwtDecoder.class);
 				return this.context.getBean(JwtDecoder.class);

+ 101 - 1
config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java

@@ -24,6 +24,7 @@ import java.time.Clock;
 import java.time.Duration;
 import java.time.Duration;
 import java.time.Instant;
 import java.time.Instant;
 import java.time.ZoneId;
 import java.time.ZoneId;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
@@ -47,12 +48,14 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.core.convert.converter.Converter;
 import org.springframework.core.io.ClassPathResource;
 import org.springframework.core.io.ClassPathResource;
 import org.springframework.data.util.ReflectionUtils;
 import org.springframework.data.util.ReflectionUtils;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.MediaType;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
 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.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -62,6 +65,7 @@ import org.springframework.security.config.test.SpringTestRule;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
@@ -71,8 +75,9 @@ import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtClaimNames;
 import org.springframework.security.oauth2.jwt.JwtClaimNames;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 import org.springframework.security.oauth2.jwt.JwtException;
 import org.springframework.security.oauth2.jwt.JwtException;
-import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
 import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
 import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
 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.BearerTokenAuthenticationEntryPoint;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
@@ -102,6 +107,7 @@ import static org.hamcrest.core.StringStartsWith.startsWith;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.when;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
@@ -125,6 +131,8 @@ public class OAuth2ResourceServerConfigurerTests {
 	private static final Map<String, Object> JWT_CLAIMS = Collections.singletonMap(JwtClaimNames.SUB, JWT_SUBJECT);
 	private static final Map<String, Object> JWT_CLAIMS = Collections.singletonMap(JwtClaimNames.SUB, JWT_SUBJECT);
 	private static final Jwt JWT = new Jwt(JWT_TOKEN, Instant.MIN, Instant.MAX, JWT_HEADERS, JWT_CLAIMS);
 	private static final Jwt JWT = new Jwt(JWT_TOKEN, Instant.MIN, Instant.MAX, JWT_HEADERS, JWT_CLAIMS);
 	private static final String JWK_SET_URI = "https://mock.org";
 	private static final String JWK_SET_URI = "https://mock.org";
+	private static final JwtAuthenticationToken JWT_AUTHENTICATION_TOKEN =
+			new JwtAuthenticationToken(JWT, Collections.emptyList());
 
 
 	@Autowired(required = false)
 	@Autowired(required = false)
 	MockMvc mvc;
 	MockMvc mvc;
@@ -898,6 +906,45 @@ public class OAuth2ResourceServerConfigurerTests {
 				.andExpect(invalidTokenHeader("Jwt expired at"));
 				.andExpect(invalidTokenHeader("Jwt expired at"));
 	}
 	}
 
 
+	// -- converter
+
+	@Test
+	public void requestWhenJwtAuthenticationConverterConfiguredOnDslThenIsUsed()
+			throws Exception {
+
+		this.spring.register(JwtDecoderConfig.class, JwtAuthenticationConverterConfiguredOnDsl.class,
+				BasicController.class).autowire();
+
+		Converter<Jwt, JwtAuthenticationToken> jwtAuthenticationConverter =
+				this.spring.getContext().getBean(JwtAuthenticationConverterConfiguredOnDsl.class)
+						.getJwtAuthenticationConverter();
+		when(jwtAuthenticationConverter.convert(JWT)).thenReturn(JWT_AUTHENTICATION_TOKEN);
+
+		JwtDecoder jwtDecoder = this.spring.getContext().getBean(JwtDecoder.class);
+		when(jwtDecoder.decode(anyString())).thenReturn(JWT);
+
+		this.mvc.perform(get("/")
+				.with(bearerToken(JWT_TOKEN)))
+				.andExpect(status().isOk());
+
+		verify(jwtAuthenticationConverter).convert(JWT);
+	}
+
+	@Test
+	public void requestWhenJwtAuthenticationConverterCustomizedAuthoritiesThenThoseAuthoritiesArePropagated()
+			throws Exception {
+
+		this.spring.register(JwtDecoderConfig.class, CustomAuthorityMappingConfig.class, BasicController.class)
+				.autowire();
+
+		JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
+		when(decoder.decode(JWT_TOKEN)).thenReturn(JWT);
+
+		this.mvc.perform(get("/requires-read-scope")
+				.with(bearerToken(JWT_TOKEN)))
+				.andExpect(status().isOk());
+	}
+
 	// -- In combination with other authentication providers
 	// -- In combination with other authentication providers
 
 
 	@Test
 	@Test
@@ -1139,6 +1186,59 @@ public class OAuth2ResourceServerConfigurerTests {
 		}
 		}
 	}
 	}
 
 
+	@EnableWebSecurity
+	static class JwtAuthenticationConverterConfiguredOnDsl extends WebSecurityConfigurerAdapter {
+		private final Converter<Jwt, JwtAuthenticationToken> jwtAuthenticationConverter = mock(Converter.class);
+
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+
+			http
+				.authorizeRequests()
+					.anyRequest().authenticated()
+					.and()
+				.oauth2()
+					.resourceServer()
+						.jwt()
+							.jwtAuthenticationConverter(getJwtAuthenticationConverter());
+
+			// @formatter:on
+		}
+
+		Converter<Jwt, JwtAuthenticationToken> getJwtAuthenticationConverter() {
+			return this.jwtAuthenticationConverter;
+		}
+	}
+
+	@EnableWebSecurity
+	static class CustomAuthorityMappingConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+
+			http
+				.authorizeRequests()
+					.antMatchers("/requires-read-scope").access("hasAuthority('message:read')")
+					.and()
+				.oauth2()
+					.resourceServer()
+						.jwt()
+							.jwtAuthenticationConverter(getJwtAuthenticationConverter());
+
+			// @formatter:on
+		}
+
+		Converter<Jwt, AbstractAuthenticationToken> getJwtAuthenticationConverter() {
+			return new JwtAuthenticationConverter() {
+				@Override
+				protected Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
+					return Collections.singletonList(new SimpleGrantedAuthority("message:read"));
+				}
+			};
+		}
+	}
+
 	@EnableWebSecurity
 	@EnableWebSecurity
 	static class BasicAndResourceServerConfig extends WebSecurityConfigurerAdapter {
 	static class BasicAndResourceServerConfig extends WebSecurityConfigurerAdapter {
 		@Value("${mock.jwk-set-uri:https://example.org}") String uri;
 		@Value("${mock.jwk-set-uri:https://example.org}") String uri;

+ 16 - 11
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtConverter.java → oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java

@@ -16,36 +16,41 @@
 
 
 package org.springframework.security.oauth2.server.resource.authentication;
 package org.springframework.security.oauth2.server.resource.authentication;
 
 
-import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
-import org.springframework.security.oauth2.jwt.Jwt;
-import org.springframework.util.StringUtils;
-
 import java.util.Arrays;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.util.StringUtils;
+
 /**
 /**
  * @author Rob Winch
  * @author Rob Winch
+ * @author Josh Cummings
  * @since 5.1
  * @since 5.1
  */
  */
-class JwtConverter {
+public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
 	private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_";
 	private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_";
 
 
 	private static final Collection<String> WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES =
 	private static final Collection<String> WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES =
 			Arrays.asList("scope", "scp");
 			Arrays.asList("scope", "scp");
 
 
 
 
-	JwtAuthenticationToken convert(Jwt jwt) {
-		Collection<GrantedAuthority> authorities =
-				this.getScopes(jwt)
+	public final AbstractAuthenticationToken convert(Jwt jwt) {
+		Collection<GrantedAuthority> authorities = extractAuthorities(jwt);
+		return new JwtAuthenticationToken(jwt, authorities);
+	}
+
+	protected Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
+		return this.getScopes(jwt)
 						.stream()
 						.stream()
 						.map(authority -> SCOPE_AUTHORITY_PREFIX + authority)
 						.map(authority -> SCOPE_AUTHORITY_PREFIX + authority)
 						.map(SimpleGrantedAuthority::new)
 						.map(SimpleGrantedAuthority::new)
 						.collect(Collectors.toList());
 						.collect(Collectors.toList());
-
-		return new JwtAuthenticationToken(jwt, authorities);
 	}
 	}
 
 
 	private Collection<String> getScopes(Jwt jwt) {
 	private Collection<String> getScopes(Jwt jwt) {

+ 11 - 2
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java

@@ -17,7 +17,9 @@ package org.springframework.security.oauth2.server.resource.authentication;
 
 
 import java.util.Collection;
 import java.util.Collection;
 
 
+import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.HttpStatus;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.AuthenticationException;
@@ -59,7 +61,7 @@ import org.springframework.util.Assert;
 public final class JwtAuthenticationProvider implements AuthenticationProvider {
 public final class JwtAuthenticationProvider implements AuthenticationProvider {
 	private final JwtDecoder jwtDecoder;
 	private final JwtDecoder jwtDecoder;
 
 
-	private final JwtConverter jwtConverter = new JwtConverter();
+	private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter = new JwtAuthenticationConverter();
 
 
 	private static final OAuth2Error DEFAULT_INVALID_TOKEN =
 	private static final OAuth2Error DEFAULT_INVALID_TOKEN =
 			invalidToken("An error occurred while attempting to decode the Jwt: Invalid token");
 			invalidToken("An error occurred while attempting to decode the Jwt: Invalid token");
@@ -91,7 +93,7 @@ public final class JwtAuthenticationProvider implements AuthenticationProvider {
 			throw new OAuth2AuthenticationException(invalidToken, invalidToken.getDescription(), failed);
 			throw new OAuth2AuthenticationException(invalidToken, invalidToken.getDescription(), failed);
 		}
 		}
 
 
-		JwtAuthenticationToken token = this.jwtConverter.convert(jwt);
+		AbstractAuthenticationToken token = this.jwtAuthenticationConverter.convert(jwt);
 		token.setDetails(bearer.getDetails());
 		token.setDetails(bearer.getDetails());
 
 
 		return token;
 		return token;
@@ -105,6 +107,13 @@ public final class JwtAuthenticationProvider implements AuthenticationProvider {
 		return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication);
 		return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication);
 	}
 	}
 
 
+	public void setJwtAuthenticationConverter(
+			Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter) {
+
+		Assert.notNull(jwtAuthenticationConverter, "jwtAuthenticationConverter cannot be null");
+		this.jwtAuthenticationConverter = jwtAuthenticationConverter;
+	}
+
 	private static OAuth2Error invalidToken(String message) {
 	private static OAuth2Error invalidToken(String message) {
 		try {
 		try {
 			return new BearerTokenError(
 			return new BearerTokenError(

+ 2 - 2
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManager.java

@@ -36,7 +36,7 @@ import reactor.core.publisher.Mono;
  * @since 5.1
  * @since 5.1
  */
  */
 public class JwtReactiveAuthenticationManager implements ReactiveAuthenticationManager {
 public class JwtReactiveAuthenticationManager implements ReactiveAuthenticationManager {
-	private final JwtConverter jwtConverter = new JwtConverter();
+	private final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
 
 
 	private final ReactiveJwtDecoder jwtDecoder;
 	private final ReactiveJwtDecoder jwtDecoder;
 
 
@@ -52,7 +52,7 @@ public class JwtReactiveAuthenticationManager implements ReactiveAuthenticationM
 				.cast(BearerTokenAuthenticationToken.class)
 				.cast(BearerTokenAuthenticationToken.class)
 				.map(BearerTokenAuthenticationToken::getToken)
 				.map(BearerTokenAuthenticationToken::getToken)
 				.flatMap(this.jwtDecoder::decode)
 				.flatMap(this.jwtDecoder::decode)
-				.map(this.jwtConverter::convert)
+				.map(this.jwtAuthenticationConverter::convert)
 				.cast(Authentication.class)
 				.cast(Authentication.class)
 				.onErrorMap(JwtException.class, this::onError);
 				.onErrorMap(JwtException.class, this::onError);
 	}
 	}

+ 128 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java

@@ -0,0 +1,128 @@
+/*
+ * 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.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.assertj.core.util.Maps;
+import org.junit.Test;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+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;
+
+/**
+ * Tests for {@link JwtAuthenticationConverter}
+ *
+ * @author Josh Cummings
+ */
+public class JwtAuthenticationConverterTests {
+	JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
+
+	@Test
+	public void convertWhenTokenHasScopeAttributeThenTranslatedToAuthorities() {
+		Jwt jwt = this.jwt(Collections.singletonMap("scope", "message:read message:write"));
+
+		AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt);
+		Collection<GrantedAuthority> authorities = authentication.getAuthorities();
+
+		assertThat(authorities).containsExactly(
+				new SimpleGrantedAuthority("SCOPE_message:read"),
+				new SimpleGrantedAuthority("SCOPE_message:write"));
+	}
+
+	@Test
+	public void convertWhenTokenHasEmptyScopeAttributeThenTranslatedToNoAuthorities() {
+		Jwt jwt = this.jwt(Collections.singletonMap("scope", ""));
+
+		AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt);
+
+		Collection<GrantedAuthority> authorities = authentication.getAuthorities();
+
+		assertThat(authorities).containsExactly();
+	}
+
+	@Test
+	public void convertWhenTokenHasScpAttributeThenTranslatedToAuthorities() {
+		Jwt jwt = this.jwt(Collections.singletonMap("scp", Arrays.asList("message:read", "message:write")));
+
+		AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt);
+
+		Collection<GrantedAuthority> authorities = authentication.getAuthorities();
+
+		assertThat(authorities).containsExactly(
+				new SimpleGrantedAuthority("SCOPE_message:read"),
+				new SimpleGrantedAuthority("SCOPE_message:write"));
+	}
+
+	@Test
+	public void convertWhenTokenHasEmptyScpAttributeThenTranslatedToNoAuthorities() {
+		Jwt jwt = this.jwt(Maps.newHashMap("scp", Arrays.asList()));
+
+		AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt);
+
+		Collection<GrantedAuthority> authorities = authentication.getAuthorities();
+
+		assertThat(authorities).containsExactly();
+	}
+
+	@Test
+	public void convertWhenTokenHasBothScopeAndScpThenScopeAttributeIsTranslatedToAuthorities() {
+		Map<String, Object> claims = new HashMap<>();
+		claims.put("scp", Arrays.asList("message:read", "message:write"));
+		claims.put("scope", "missive:read missive:write");
+		Jwt jwt = this.jwt(claims);
+
+		AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt);
+
+		Collection<GrantedAuthority> authorities = authentication.getAuthorities();
+
+		assertThat(authorities).containsExactly(
+				new SimpleGrantedAuthority("SCOPE_missive:read"),
+				new SimpleGrantedAuthority("SCOPE_missive:write"));
+	}
+
+	@Test
+	public void convertWhenTokenHasEmptyScopeAndNonEmptyScpThenScopeAttributeIsTranslatedToNoAuthorities() {
+		Map<String, Object> claims = new HashMap<>();
+		claims.put("scp", Arrays.asList("message:read", "message:write"));
+		claims.put("scope", "");
+		Jwt jwt = this.jwt(claims);
+
+		AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt);
+
+		Collection<GrantedAuthority> authorities = authentication.getAuthorities();
+
+		assertThat(authorities).containsExactly();
+	}
+
+	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);
+	}
+}

+ 24 - 110
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java

@@ -16,21 +16,18 @@
 package org.springframework.security.oauth2.server.resource.authentication;
 package org.springframework.security.oauth2.server.resource.authentication;
 
 
 import java.time.Instant;
 import java.time.Instant;
-import java.util.Arrays;
-import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 import java.util.function.Predicate;
 import java.util.function.Predicate;
 
 
-import org.assertj.core.util.Maps;
 import org.junit.Before;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 import org.mockito.junit.MockitoJUnitRunner;
 
 
-import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.core.convert.converter.Converter;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
 import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
 import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.Jwt;
@@ -41,6 +38,7 @@ import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes
 
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.when;
 
 
 /**
 /**
@@ -50,6 +48,9 @@ import static org.mockito.Mockito.when;
  */
  */
 @RunWith(MockitoJUnitRunner.class)
 @RunWith(MockitoJUnitRunner.class)
 public class JwtAuthenticationProviderTests {
 public class JwtAuthenticationProviderTests {
+	@Mock
+	Converter<Jwt, JwtAuthenticationToken> jwtAuthenticationConverter;
+
 	@Mock
 	@Mock
 	JwtDecoder jwtDecoder;
 	JwtDecoder jwtDecoder;
 
 
@@ -59,6 +60,7 @@ public class JwtAuthenticationProviderTests {
 	public void setup() {
 	public void setup() {
 		this.provider =
 		this.provider =
 				new JwtAuthenticationProvider(this.jwtDecoder);
 				new JwtAuthenticationProvider(this.jwtDecoder);
+		this.provider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
 	}
 	}
 
 
 	@Test
 	@Test
@@ -70,6 +72,7 @@ public class JwtAuthenticationProviderTests {
 		Jwt jwt = this.jwt(claims);
 		Jwt jwt = this.jwt(claims);
 
 
 		when(this.jwtDecoder.decode("token")).thenReturn(jwt);
 		when(this.jwtDecoder.decode("token")).thenReturn(jwt);
+		when(this.jwtAuthenticationConverter.convert(jwt)).thenReturn(new JwtAuthenticationToken(jwt));
 
 
 		JwtAuthenticationToken authentication =
 		JwtAuthenticationToken authentication =
 				(JwtAuthenticationToken) this.provider.authenticate(token);
 				(JwtAuthenticationToken) this.provider.authenticate(token);
@@ -89,122 +92,33 @@ public class JwtAuthenticationProviderTests {
 	}
 	}
 
 
 	@Test
 	@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() {
+	public void authenticateWhenDecoderThrowsIncompatibleErrorMessageThenWrapsWithGenericOne() {
 		BearerTokenAuthenticationToken token = this.authentication();
 		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();
+		when(this.jwtDecoder.decode(token.getToken())).thenThrow(new JwtException("with \"invalid\" chars"));
 
 
-		assertThat(authorities).containsExactly(
-				new SimpleGrantedAuthority("SCOPE_missive:read"),
-				new SimpleGrantedAuthority("SCOPE_missive:write"));
+		assertThatCode(() -> this.provider.authenticate(token))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.hasFieldOrPropertyWithValue(
+						"error.description",
+						"An error occurred while attempting to decode the Jwt: Invalid token");
 	}
 	}
 
 
 	@Test
 	@Test
-	public void authenticateWhenTokenHasEmptyScopeAndNonEmptyScpThenScopeAttributeIsTranslatedToNoAuthorities() {
+	public void authenticateWhenConverterReturnsAuthenticationThenProviderPropagatesIt() {
 		BearerTokenAuthenticationToken token = this.authentication();
 		BearerTokenAuthenticationToken token = this.authentication();
+		Object details = mock(Object.class);
+		token.setDetails(details);
 
 
-		Map<String, Object> claims = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write"));
-		claims.put("scope", "");
-		Jwt jwt = this.jwt(claims);
+		Jwt jwt = this.jwt(Collections.singletonMap("some", "value"));
+		JwtAuthenticationToken authentication = new JwtAuthenticationToken(jwt);
 
 
 		when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt);
 		when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt);
+		when(this.jwtAuthenticationConverter.convert(jwt)).thenReturn(authentication);
 
 
-		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");
+		assertThat(this.provider.authenticate(token))
+				.isEqualTo(authentication)
+				.hasFieldOrPropertyWithValue("details", details);
 	}
 	}
 
 
 	@Test
 	@Test