From 45d047418b14d5e2011436e594fce2021e315439 Mon Sep 17 00:00:00 2001 From: Brian Demers Date: Thu, 27 Jan 2022 11:08:10 -0500 Subject: [PATCH] Idea: Allow for AuthoritiesProvider to be used for Resource Servers NOTE: This doesn't work, I was just putting thoughts down, and it ended up being a be more complicated: TODO: * There is probably a better Spring Security abstraction for all of this * Need to check to see if returning a `Converter` bean will inject correctly everywhere (including with native images) * Need to implement something similar for opaque tokens * Need to add tests that will make sure scope authorities and claim/group authorities are merged correctly * The new method in AuthoritiesProvder's return type doesn't match the other very related method in this interface (and it still doesn't cover the opaque token use case) Related: #160 --- .../boot/oauth/AuthoritiesProvider.java | 12 +++++ ... => OktaJwtGrantedAuthorityConverter.java} | 14 ++--- .../boot/oauth/OktaOAuth2Configurer.java | 7 +-- .../OktaOAuth2ResourceServerAutoConfig.java | 11 +--- ...th2ResourceServerHttpServerAutoConfig.java | 21 +++++--- .../boot/oauth/ResourceServerConfig.java | 53 +++++++++++++++++++ .../oauth/AutoConfigConditionalTest.groovy | 6 +-- ...taJwtGrantedAuthorityConverterTest.groovy} | 9 ++-- 8 files changed, 97 insertions(+), 36 deletions(-) rename oauth2/src/main/java/com/okta/spring/boot/oauth/{OktaJwtAuthenticationConverter.java => OktaJwtGrantedAuthorityConverter.java} (63%) create mode 100644 oauth2/src/main/java/com/okta/spring/boot/oauth/ResourceServerConfig.java rename oauth2/src/test/groovy/com/okta/spring/boot/oauth/{OktaJwtAuthenticationConverterTest.groovy => OktaJwtGrantedAuthorityConverterTest.groovy} (80%) diff --git a/oauth2/src/main/java/com/okta/spring/boot/oauth/AuthoritiesProvider.java b/oauth2/src/main/java/com/okta/spring/boot/oauth/AuthoritiesProvider.java index ad1a83d1a..3b224d3cc 100644 --- a/oauth2/src/main/java/com/okta/spring/boot/oauth/AuthoritiesProvider.java +++ b/oauth2/src/main/java/com/okta/spring/boot/oauth/AuthoritiesProvider.java @@ -20,8 +20,10 @@ import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; 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 java.util.Collection; +import java.util.Collections; /** * Allows for custom {@link GrantedAuthority}s to be added to the current OAuth Principal. Multiple implementations @@ -46,4 +48,14 @@ public interface AuthoritiesProvider { default Collection getAuthorities(OidcUser user, OidcUserRequest userRequest) { return getAuthorities((OAuth2User) user, userRequest); } + + /** + * Returns collections of authorities based on the contents of a JWT or other Bearer token. + * @param token a bearer token + * @return A collections of authorities based on the contents of a JWT or other Bearer token. + * @since 2.2.0 + */ + default Collection getAuthorities(Jwt token) { + return Collections.emptySet(); + } } diff --git a/oauth2/src/main/java/com/okta/spring/boot/oauth/OktaJwtAuthenticationConverter.java b/oauth2/src/main/java/com/okta/spring/boot/oauth/OktaJwtGrantedAuthorityConverter.java similarity index 63% rename from oauth2/src/main/java/com/okta/spring/boot/oauth/OktaJwtAuthenticationConverter.java rename to oauth2/src/main/java/com/okta/spring/boot/oauth/OktaJwtGrantedAuthorityConverter.java index 759fd152e..eea79b9c9 100644 --- a/oauth2/src/main/java/com/okta/spring/boot/oauth/OktaJwtAuthenticationConverter.java +++ b/oauth2/src/main/java/com/okta/spring/boot/oauth/OktaJwtGrantedAuthorityConverter.java @@ -15,27 +15,23 @@ */ package com.okta.spring.boot.oauth; +import org.springframework.core.convert.converter.Converter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import java.util.Collection; import java.util.HashSet; -final class OktaJwtAuthenticationConverter extends JwtAuthenticationConverter { +final class OktaJwtGrantedAuthorityConverter implements Converter> { private final String groupClaim; - public OktaJwtAuthenticationConverter(String groupClaim) { + public OktaJwtGrantedAuthorityConverter(String groupClaim) { this.groupClaim = groupClaim; } @Override - protected Collection extractAuthorities(Jwt jwt) { - - Collection result = new HashSet<>(super.extractAuthorities(jwt)); - result.addAll(TokenUtil.tokenClaimsToAuthorities(jwt.getClaims(), groupClaim)); - - return result; + public Collection convert(Jwt jwt) { + return new HashSet<>(TokenUtil.tokenClaimsToAuthorities(jwt.getClaims(), groupClaim)); } } \ No newline at end of file diff --git a/oauth2/src/main/java/com/okta/spring/boot/oauth/OktaOAuth2Configurer.java b/oauth2/src/main/java/com/okta/spring/boot/oauth/OktaOAuth2Configurer.java index 701d4f1a9..389d1403c 100644 --- a/oauth2/src/main/java/com/okta/spring/boot/oauth/OktaOAuth2Configurer.java +++ b/oauth2/src/main/java/com/okta/spring/boot/oauth/OktaOAuth2Configurer.java @@ -28,6 +28,7 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.web.client.RestTemplate; import java.lang.reflect.Field; @@ -83,7 +84,7 @@ public void init(HttpSecurity http) throws Exception { if (getJwtConfigurer(oAuth2ResourceServerConfigurer).isPresent()) { log.debug("JWT configurer is set in OAuth resource server configuration. " + "JWT validation will be configured."); - configureResourceServerForJwtValidation(http, oktaOAuth2Properties); + configureResourceServerForJwtValidation(http, context.getBean(JwtAuthenticationConverter.class)); } else if (getOpaqueTokenConfigurer(oAuth2ResourceServerConfigurer).isPresent()) { log.debug("Opaque Token configurer is set in OAuth resource server configuration. " + "Opaque Token validation/introspection will be configured."); @@ -151,9 +152,9 @@ private void configureLogin(HttpSecurity http, OktaOAuth2Properties oktaOAuth2Pr } } - private void configureResourceServerForJwtValidation(HttpSecurity http, OktaOAuth2Properties oktaOAuth2Properties) throws Exception { + private void configureResourceServerForJwtValidation(HttpSecurity http, JwtAuthenticationConverter jwtAuthenticationConverter) throws Exception { http.oauth2ResourceServer() - .jwt().jwtAuthenticationConverter(new OktaJwtAuthenticationConverter(oktaOAuth2Properties.getGroupsClaim())); + .jwt().jwtAuthenticationConverter(jwtAuthenticationConverter); } private void configureResourceServerForOpaqueTokenValidation(HttpSecurity http, OktaOAuth2Properties oktaOAuth2Properties) throws Exception { diff --git a/oauth2/src/main/java/com/okta/spring/boot/oauth/OktaOAuth2ResourceServerAutoConfig.java b/oauth2/src/main/java/com/okta/spring/boot/oauth/OktaOAuth2ResourceServerAutoConfig.java index db3ce7b90..d4c55900e 100644 --- a/oauth2/src/main/java/com/okta/spring/boot/oauth/OktaOAuth2ResourceServerAutoConfig.java +++ b/oauth2/src/main/java/com/okta/spring/boot/oauth/OktaOAuth2ResourceServerAutoConfig.java @@ -28,6 +28,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.client.support.BasicAuthenticationInterceptor; import org.springframework.http.converter.FormHttpMessageConverter; @@ -38,9 +39,7 @@ import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -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.JwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; import org.springframework.web.client.RestTemplate; @@ -56,17 +55,11 @@ @AutoConfigureBefore(OAuth2ResourceServerAutoConfiguration.class) @ConditionalOnClass(JwtAuthenticationToken.class) @ConditionalOnOktaResourceServerProperties +@Import(ResourceServerConfig.class) @EnableConfigurationProperties(OktaOAuth2Properties.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) class OktaOAuth2ResourceServerAutoConfig { - @Bean - public JwtAuthenticationConverter jwtAuthenticationConverter(OktaOAuth2Properties oktaOAuth2Properties) { - OktaJwtAuthenticationConverter converter = new OktaJwtAuthenticationConverter(oktaOAuth2Properties.getGroupsClaim()); - converter.setJwtGrantedAuthoritiesConverter(new JwtGrantedAuthoritiesConverter()); - return converter; - } - @Bean @ConditionalOnMissingBean JwtDecoder jwtDecoder(OAuth2ResourceServerProperties oAuth2ResourceServerProperties, diff --git a/oauth2/src/main/java/com/okta/spring/boot/oauth/ReactiveOktaOAuth2ResourceServerHttpServerAutoConfig.java b/oauth2/src/main/java/com/okta/spring/boot/oauth/ReactiveOktaOAuth2ResourceServerHttpServerAutoConfig.java index 4b18a0c09..b38ed6bd2 100644 --- a/oauth2/src/main/java/com/okta/spring/boot/oauth/ReactiveOktaOAuth2ResourceServerHttpServerAutoConfig.java +++ b/oauth2/src/main/java/com/okta/spring/boot/oauth/ReactiveOktaOAuth2ResourceServerHttpServerAutoConfig.java @@ -24,31 +24,39 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.convert.converter.Converter; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; +import java.util.Collection; + @Configuration @ConditionalOnOktaResourceServerProperties @AutoConfigureAfter(ReactiveOktaOAuth2ResourceServerAutoConfig.class) @EnableConfigurationProperties({OktaOAuth2Properties.class, OAuth2ResourceServerProperties.class}) +@Import(ResourceServerConfig.class) @ConditionalOnClass({ EnableWebFluxSecurity.class, BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class }) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) class ReactiveOktaOAuth2ResourceServerHttpServerAutoConfig { @Bean - BeanPostProcessor oktaOAuth2ResourceServerBeanPostProcessor(OktaOAuth2Properties oktaOAuth2Properties) { - return new OktaOAuth2ResourceServerBeanPostProcessor(oktaOAuth2Properties); + BeanPostProcessor oktaOAuth2ResourceServerBeanPostProcessor(Converter> converter) { + return new OktaOAuth2ResourceServerBeanPostProcessor(converter); } static class OktaOAuth2ResourceServerBeanPostProcessor implements BeanPostProcessor { - private final OktaOAuth2Properties oktaOAuth2Properties; + private final Converter> converter; - OktaOAuth2ResourceServerBeanPostProcessor(OktaOAuth2Properties oktaOAuth2Properties) { - this.oktaOAuth2Properties = oktaOAuth2Properties; + OktaOAuth2ResourceServerBeanPostProcessor(Converter> converter) { + this.converter = converter; } @Override @@ -57,7 +65,8 @@ public Object postProcessAfterInitialization(Object bean, String beanName) { final ServerHttpSecurity http = (ServerHttpSecurity) bean; http.oauth2ResourceServer().jwt() .jwtAuthenticationConverter(new ReactiveJwtAuthenticationConverterAdapter( - new OktaJwtAuthenticationConverter(oktaOAuth2Properties.getGroupsClaim()))); + jwt -> new JwtAuthenticationToken(jwt, converter.convert(jwt)) + )); } return bean; } diff --git a/oauth2/src/main/java/com/okta/spring/boot/oauth/ResourceServerConfig.java b/oauth2/src/main/java/com/okta/spring/boot/oauth/ResourceServerConfig.java new file mode 100644 index 000000000..5263d7002 --- /dev/null +++ b/oauth2/src/main/java/com/okta/spring/boot/oauth/ResourceServerConfig.java @@ -0,0 +1,53 @@ +/* + * Copyright 2022-Present Okta, Inc. + * + * 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 com.okta.spring.boot.oauth; + +import com.okta.spring.boot.oauth.config.OktaOAuth2Properties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.DelegatingJwtGrantedAuthoritiesConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; + +import java.util.ArrayList; +import java.util.Collection; + +@Configuration +class ResourceServerConfig { + + @Bean + JwtAuthenticationConverter jwtAuthenticationConverter(Converter> converter) { + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(converter); + return jwtAuthenticationConverter; + } + + @Bean + Converter> jwtConverter(OktaOAuth2Properties oktaOAuth2Properties, Collection authoritiesProviders) { + + Collection>> converters = new ArrayList<>(); + converters.add(new JwtGrantedAuthoritiesConverter()); + converters.add(new OktaJwtGrantedAuthorityConverter(oktaOAuth2Properties.getGroupsClaim())); + authoritiesProviders.stream() + .map(provider -> (Converter>) provider::getAuthorities) + .forEach(converters::add); + + return new DelegatingJwtGrantedAuthoritiesConverter(converters); + } +} diff --git a/oauth2/src/test/groovy/com/okta/spring/boot/oauth/AutoConfigConditionalTest.groovy b/oauth2/src/test/groovy/com/okta/spring/boot/oauth/AutoConfigConditionalTest.groovy index 32aa7876d..b61a98adc 100644 --- a/oauth2/src/test/groovy/com/okta/spring/boot/oauth/AutoConfigConditionalTest.groovy +++ b/oauth2/src/test/groovy/com/okta/spring/boot/oauth/AutoConfigConditionalTest.groovy @@ -180,7 +180,7 @@ class AutoConfigConditionalTest implements HttpMock { .run { context -> assertThat(context).hasSingleBean(OktaOAuth2ResourceServerAutoConfig) assertThat(context).hasSingleBean(JwtDecoder) - assertThat(context).hasSingleBean(OktaJwtAuthenticationConverter) + assertThat(context).hasSingleBean(OktaJwtGrantedAuthorityConverter) assertThat(context).doesNotHaveBean(OktaOAuth2AutoConfig) assertThat(context).doesNotHaveBean(ReactiveOktaOAuth2AutoConfig) assertThat(context).doesNotHaveBean(ReactiveOktaOAuth2ResourceServerAutoConfig) @@ -304,7 +304,7 @@ class AutoConfigConditionalTest implements HttpMock { assertThat(context).hasSingleBean(OktaOAuth2ResourceServerAutoConfig) assertThat(context).hasSingleBean(JwtDecoder) - assertThat(context).hasSingleBean(OktaJwtAuthenticationConverter) + assertThat(context).hasSingleBean(OktaJwtGrantedAuthorityConverter) assertThat(context).hasSingleBean(OAuth2ClientProperties) assertThat(context).hasSingleBean(OktaOAuth2Properties) assertThat(context).hasSingleBean(OktaOAuth2AutoConfig) @@ -431,7 +431,7 @@ class AutoConfigConditionalTest implements HttpMock { assertThat(context).hasSingleBean(OktaOAuth2ResourceServerAutoConfig) assertThat(context).hasSingleBean(JwtDecoder) - assertThat(context).hasSingleBean(OktaJwtAuthenticationConverter) + assertThat(context).hasSingleBean(OktaJwtGrantedAuthorityConverter) assertThat(context).hasSingleBean(OAuth2ClientProperties) assertThat(context).hasSingleBean(OktaOAuth2Properties) assertThat(context).hasSingleBean(OktaOAuth2AutoConfig) diff --git a/oauth2/src/test/groovy/com/okta/spring/boot/oauth/OktaJwtAuthenticationConverterTest.groovy b/oauth2/src/test/groovy/com/okta/spring/boot/oauth/OktaJwtGrantedAuthorityConverterTest.groovy similarity index 80% rename from oauth2/src/test/groovy/com/okta/spring/boot/oauth/OktaJwtAuthenticationConverterTest.groovy rename to oauth2/src/test/groovy/com/okta/spring/boot/oauth/OktaJwtGrantedAuthorityConverterTest.groovy index 1c89f5b30..9a7beac5e 100644 --- a/oauth2/src/test/groovy/com/okta/spring/boot/oauth/OktaJwtAuthenticationConverterTest.groovy +++ b/oauth2/src/test/groovy/com/okta/spring/boot/oauth/OktaJwtGrantedAuthorityConverterTest.groovy @@ -25,7 +25,7 @@ import static org.hamcrest.Matchers.hasItems import static org.hamcrest.MatcherAssert.assertThat import static org.hamcrest.Matchers.hasSize -class OktaJwtAuthenticationConverterTest { +class OktaJwtGrantedAuthorityConverterTest { @Test void extractAuthorities_simpleTest() { @@ -36,11 +36,8 @@ class OktaJwtAuthenticationConverterTest { myGroups: ["g1", "g2"] ]) - def authorities = new OktaJwtAuthenticationConverter("myGroups").extractAuthorities(jwt) + def authorities = new OktaJwtGrantedAuthorityConverter("myGroups").convert(jwt) assertThat authorities, hasItems( - new SimpleGrantedAuthority("SCOPE_one"), - new SimpleGrantedAuthority("SCOPE_two"), - new SimpleGrantedAuthority("SCOPE_three"), new SimpleGrantedAuthority("g1"), new SimpleGrantedAuthority("g2")) } @@ -49,7 +46,7 @@ class OktaJwtAuthenticationConverterTest { void extractAuthorities_emptyTest() { def jwt = new Jwt("foo", Instant.now(), Instant.now().plusMillis(1000L), [simple: "value"], [simple: "value"]) // these maps must not be empty - def authorities = new OktaJwtAuthenticationConverter("myGroups").extractAuthorities(jwt) + def authorities = new OktaJwtGrantedAuthorityConverter("myGroups").convert(jwt) assertThat authorities, hasSize(0) } } \ No newline at end of file