Reactive Jwt Authentication Converter Support

Fixes: gh-5092
This commit is contained in:
Josh Cummings 2018-10-05 15:41:12 -06:00 committed by Rob Winch
parent 01b47a8b2f
commit 22bd8f1c1f
5 changed files with 277 additions and 3 deletions

View File

@ -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;
}

View File

@ -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

View File

@ -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());

View File

@ -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);
}
}

View File

@ -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);
}
}