소스 검색

Reactive Jwt Authentication Converter Support

Fixes: gh-5092
Josh Cummings 6 년 전
부모
커밋
22bd8f1c1f

+ 33 - 1
config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

@@ -38,8 +38,10 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.core.Ordered;
 import org.springframework.core.ResolvableType;
 import org.springframework.core.annotation.AnnotationAwareOrderComparator;
+import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.MediaType;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager;
@@ -70,9 +72,12 @@ import org.springframework.security.oauth2.client.web.server.ServerOAuth2Authori
 import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter;
 import org.springframework.security.oauth2.core.oidc.user.OidcUser;
 import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
 import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
+import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
 import org.springframework.security.oauth2.server.resource.web.access.server.BearerTokenServerAccessDeniedHandler;
 import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint;
 import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter;
@@ -900,6 +905,8 @@ public class ServerHttpSecurity {
 		public class JwtSpec {
 			private ReactiveAuthenticationManager authenticationManager;
 			private ReactiveJwtDecoder jwtDecoder;
+			private Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter
+					= new ReactiveJwtAuthenticationConverterAdapter(new JwtAuthenticationConverter());
 
 			private BearerTokenServerWebExchangeMatcher bearerTokenServerWebExchangeMatcher =
 					new BearerTokenServerWebExchangeMatcher();
@@ -915,6 +922,21 @@ public class ServerHttpSecurity {
 				return this;
 			}
 
+			/**
+			 * Configures the {@link Converter} to use for converting a {@link Jwt} into
+			 * an {@link AbstractAuthenticationToken}.
+			 *
+			 * @param jwtAuthenticationConverter the converter to use
+			 * @return the {@code JwtSpec} for additional configuration
+			 * @since 5.1.1
+			 */
+			public JwtSpec jwtAuthenticationConverter
+					(Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter) {
+				Assert.notNull(jwtAuthenticationConverter, "jwtAuthenticationConverter cannot be null");
+				this.jwtAuthenticationConverter = jwtAuthenticationConverter;
+				return this;
+			}
+
 			/**
 			 * Configures the {@link ReactiveJwtDecoder} to use
 			 * @param jwtDecoder the decoder to use
@@ -976,14 +998,24 @@ public class ServerHttpSecurity {
 				return this.jwtDecoder;
 			}
 
+			protected Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>>
+					getJwtAuthenticationConverter() {
+
+				return this.jwtAuthenticationConverter;
+			}
+
 			private ReactiveAuthenticationManager getAuthenticationManager() {
 				if (this.authenticationManager != null) {
 					return this.authenticationManager;
 				}
 
 				ReactiveJwtDecoder jwtDecoder = getJwtDecoder();
-				ReactiveAuthenticationManager authenticationManager =
+				Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter =
+						getJwtAuthenticationConverter();
+				JwtReactiveAuthenticationManager authenticationManager =
 						new JwtReactiveAuthenticationManager(jwtDecoder);
+				authenticationManager.setJwtAuthenticationConverter(jwtAuthenticationConverter);
+
 				return authenticationManager;
 			}
 

+ 54 - 0
config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java

@@ -23,7 +23,10 @@ import java.security.interfaces.RSAPublicKey;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.RSAPublicKeySpec;
 import java.time.Instant;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import javax.annotation.PreDestroy;
 
 import okhttp3.mockwebserver.MockResponse;
@@ -39,15 +42,21 @@ import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
 import org.springframework.security.config.test.SpringTestRule;
 import org.springframework.security.core.Authentication;
+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.jose.jws.JwsAlgorithms;
 import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
+import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
 import org.springframework.security.web.server.SecurityWebFilterChain;
 import org.springframework.test.context.junit4.SpringRunner;
 import org.springframework.test.web.reactive.server.WebTestClient;
@@ -213,6 +222,16 @@ public class OAuth2ResourceServerSpecTests {
 				.expectStatus().isForbidden();
 	}
 
+	@Test
+	public void getWhenSignedAndCustomConverterThenConverts() {
+		this.spring.register(CustomJwtAuthenticationConverterConfig.class, RootController.class).autowire();
+
+		this.client.get()
+				.headers(headers -> headers.setBearerAuth(this.messageReadToken))
+				.exchange()
+				.expectStatus().isOk();
+	}
+
 	@Test
 	public void getJwtDecoderWhenBeanWiredAndDslWiredThenDslTakesPrecedence() {
 		GenericWebApplicationContext context = autowireWebServerGenericWebApplicationContext();
@@ -386,6 +405,41 @@ public class OAuth2ResourceServerSpecTests {
 		}
 	}
 
+	@EnableWebFlux
+	@EnableWebFluxSecurity
+	static class CustomJwtAuthenticationConverterConfig {
+		@Bean
+		SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeExchange()
+					.anyExchange().hasAuthority("message:read")
+					.and()
+				.oauth2ResourceServer()
+					.jwt()
+						.jwtAuthenticationConverter(jwtAuthenticationConverter())
+						.publicKey(publicKey());
+			// @formatter:on
+
+			return http.build();
+		}
+
+		@Bean
+		Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter() {
+			JwtAuthenticationConverter converter = new JwtAuthenticationConverter() {
+				@Override
+				protected Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
+					String[] claims = ((String) jwt.getClaims().get("scope")).split(" ");
+					return Stream.of(claims).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
+				}
+			};
+
+			return new ReactiveJwtAuthenticationConverterAdapter(converter);
+		}
+	}
+
+
+
 	@RestController
 	static class RootController {
 		@GetMapping

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

@@ -18,11 +18,14 @@ package org.springframework.security.oauth2.server.resource.authentication;
 
 import reactor.core.publisher.Mono;
 
+import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.HttpStatus;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.core.Authentication;
 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.JwtException;
 import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
 import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
@@ -37,7 +40,8 @@ import org.springframework.util.Assert;
  * @since 5.1
  */
 public final class JwtReactiveAuthenticationManager implements ReactiveAuthenticationManager {
-	private final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
+	private Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter
+			= new ReactiveJwtAuthenticationConverterAdapter(new JwtAuthenticationConverter());
 
 	private final ReactiveJwtDecoder jwtDecoder;
 
@@ -53,11 +57,23 @@ public final class JwtReactiveAuthenticationManager implements ReactiveAuthentic
 				.cast(BearerTokenAuthenticationToken.class)
 				.map(BearerTokenAuthenticationToken::getToken)
 				.flatMap(this.jwtDecoder::decode)
-				.map(this.jwtAuthenticationConverter::convert)
+				.flatMap(this.jwtAuthenticationConverter::convert)
 				.cast(Authentication.class)
 				.onErrorMap(JwtException.class, this::onError);
 	}
 
+	/**
+	 * Use the given {@link Converter} for converting a {@link Jwt} into an {@link AbstractAuthenticationToken}.
+	 *
+	 * @param jwtAuthenticationConverter the {@link Converter} to use
+	 */
+	public void setJwtAuthenticationConverter(
+			Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter) {
+
+		Assert.notNull(jwtAuthenticationConverter, "jwtAuthenticationConverter cannot be null");
+		this.jwtAuthenticationConverter = jwtAuthenticationConverter;
+	}
+
 	private OAuth2AuthenticationException onError(JwtException e) {
 		OAuth2Error invalidRequest = invalidToken(e.getMessage());
 		return new OAuth2AuthenticationException(invalidRequest, e.getMessage());

+ 43 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterAdapter.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.authentication;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.util.Assert;
+
+/**
+ * A reactive {@link Converter} for adapting a non-blocking imperative {@link Converter}
+ *
+ * @author Josh Cummings
+ * @since 5.1.1
+ */
+public class ReactiveJwtAuthenticationConverterAdapter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
+	private final JwtAuthenticationConverter delegate;
+
+	public ReactiveJwtAuthenticationConverterAdapter(JwtAuthenticationConverter delegate) {
+		Assert.notNull(delegate, "delegate cannot be null");
+		this.delegate = delegate;
+	}
+
+	public final Mono<AbstractAuthenticationToken> convert(Jwt jwt) {
+		return Mono.just(jwt).map(this.delegate::convert);
+	}
+}

+ 129 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterAdapterTests.java

@@ -0,0 +1,129 @@
+/*
+ * 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.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 ReactiveJwtAuthenticationConverterAdapter}
+ *
+ * @author Josh Cummings
+ */
+public class ReactiveJwtAuthenticationConverterAdapterTests {
+	JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
+	ReactiveJwtAuthenticationConverterAdapter jwtAuthenticationConverter =
+			new ReactiveJwtAuthenticationConverterAdapter(converter);
+
+	@Test
+	public void convertWhenTokenHasScopeAttributeThenTranslatedToAuthorities() {
+		Jwt jwt = this.jwt(Collections.singletonMap("scope", "message:read message:write"));
+
+		AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt).block();
+		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).block();
+
+		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).block();
+
+		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(Collections.singletonMap("scp", Arrays.asList()));
+
+		AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt).block();
+
+		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).block();
+
+		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).block();
+
+		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);
+	}
+}