Skip to content

Commit

Permalink
Idea: Allow for AuthoritiesProvider to be used for Resource Servers
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bdemers committed Jan 27, 2022
1 parent a351eac commit 45d0474
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -46,4 +48,14 @@ public interface AuthoritiesProvider {
default Collection<? extends GrantedAuthority> 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<GrantedAuthority> getAuthorities(Jwt token) {
return Collections.emptySet();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Jwt, Collection<GrantedAuthority>> {

private final String groupClaim;

public OktaJwtAuthenticationConverter(String groupClaim) {
public OktaJwtGrantedAuthorityConverter(String groupClaim) {
this.groupClaim = groupClaim;
}

@Override
protected Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {

Collection<GrantedAuthority> result = new HashSet<>(super.extractAuthorities(jwt));
result.addAll(TokenUtil.tokenClaimsToAuthorities(jwt.getClaims(), groupClaim));

return result;
public Collection<GrantedAuthority> convert(Jwt jwt) {
return new HashSet<>(TokenUtil.tokenClaimsToAuthorities(jwt.getClaims(), groupClaim));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.");
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Jwt, Collection<GrantedAuthority>> converter) {
return new OktaOAuth2ResourceServerBeanPostProcessor(converter);
}

static class OktaOAuth2ResourceServerBeanPostProcessor implements BeanPostProcessor {

private final OktaOAuth2Properties oktaOAuth2Properties;
private final Converter<Jwt, Collection<GrantedAuthority>> converter;

OktaOAuth2ResourceServerBeanPostProcessor(OktaOAuth2Properties oktaOAuth2Properties) {
this.oktaOAuth2Properties = oktaOAuth2Properties;
OktaOAuth2ResourceServerBeanPostProcessor(Converter<Jwt, Collection<GrantedAuthority>> converter) {
this.converter = converter;
}

@Override
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Jwt, Collection<GrantedAuthority>> converter) {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(converter);
return jwtAuthenticationConverter;
}

@Bean
Converter<Jwt, Collection<GrantedAuthority>> jwtConverter(OktaOAuth2Properties oktaOAuth2Properties, Collection<AuthoritiesProvider> authoritiesProviders) {

Collection<Converter<Jwt, Collection<GrantedAuthority>>> converters = new ArrayList<>();
converters.add(new JwtGrantedAuthoritiesConverter());
converters.add(new OktaJwtGrantedAuthorityConverter(oktaOAuth2Properties.getGroupsClaim()));
authoritiesProviders.stream()
.map(provider -> (Converter<Jwt, Collection<GrantedAuthority>>) provider::getAuthorities)
.forEach(converters::add);

return new DelegatingJwtGrantedAuthoritiesConverter(converters);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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"))
}
Expand All @@ -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)
}
}

0 comments on commit 45d0474

Please sign in to comment.