Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for audience parameter #600

Draft
wants to merge 2 commits into
base: send-audience
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 73 additions & 14 deletions oauth2/src/main/java/com/okta/spring/boot/oauth/Okta.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/
package com.okta.spring.boot.oauth;

import org.springframework.beans.factory.annotation.Value;
import com.okta.spring.boot.oauth.config.OktaOAuth2Properties;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand All @@ -24,6 +24,7 @@
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.server.DefaultServerOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
Expand Down Expand Up @@ -102,12 +103,6 @@ public static ServerHttpSecurity configureOAuth2WithPkce(ServerHttpSecurity http
authorizationRequestResolver.setAuthorizationRequestCustomizer(withPkce());
// enable oauth2 login that uses PKCE
http.oauth2Login().authorizationRequestResolver(authorizationRequestResolver);
// enable passing the audience parameter
http.oauth2Login(oauth2 -> oauth2
.authorizationRequestResolver(
authorizeRequestResolver(clientRegistrationRepository)
)
);

return http;
}
Expand Down Expand Up @@ -135,6 +130,61 @@ public static HttpSecurity configureOAuth2WithPkce(HttpSecurity http, ClientRegi
return http;
}

/**
* Configures the {@code http} with an OAuth2 Login, that sends an audience parameter with the authorize request.
*
* @param http the HttpSecurity to configure
* @param clientRegistrationRepository the repository bean, this should be injected into the calling method.
* @param properties the OktaOAuth2Properties bean, this should be injected into the calling method.
* @return the {@code http} to allow method chaining
*/
public static ServerHttpSecurity configureOAuth2WithAudience(ServerHttpSecurity http,
ReactiveClientRegistrationRepository clientRegistrationRepository,
OktaOAuth2Properties properties) throws Exception {
// Don't add audience parameter if empty or default
if (isDefaultAudience(properties)) {
return http;
}
http.oauth2Login(oauth2 -> oauth2
.authorizationRequestResolver(
reactiveAuthzRequestResolver(clientRegistrationRepository, properties.getAudience())
)
);

return http;
}

private static boolean isDefaultAudience(OktaOAuth2Properties properties) {
String audience = properties.getAudience();
return audience == null || audience.isEmpty() || audience.equals("api://default");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that api://default will never be sent if it's the audience. I tried to make it so it only happens when it's Auth0, but was unable to figure out how to do it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it should not be a problem sending it for Okta too.

}

/**
* Configures the {@code http} with an OAuth2 Login, that sends an audience parameter with the authorize request.
*
* @param http the HttpSecurity to configure
* @param clientRegistrationRepository the repository bean, this should be injected into the calling method.
* @param properties the OktaOAuth2Properties bean, this should be injected into the calling method.
* @return the {@code http} to allow method chaining
*/
public static HttpSecurity configureOAuth2WithAudience(HttpSecurity http,
ClientRegistrationRepository clientRegistrationRepository,
OktaOAuth2Properties properties) throws Exception {
// Don't add audience parameter if empty or default
if (isDefaultAudience(properties)) {
return http;
}
http.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization
.authorizationRequestResolver(
mvcAuthzRequestResolver(clientRegistrationRepository, properties.getAudience())
)
)
);

return http;
}

private static RequestMatcher textRequestMatcher() {
return new MediaTypeRequestMatcher(new HeaderContentNegotiationStrategy(), MediaType.TEXT_PLAIN);
}
Expand Down Expand Up @@ -162,22 +212,31 @@ static String statusAsString(HttpStatus status) {
return status.value() + " " + status.getReasonPhrase();
}

@Value("${okta.oauth2.audience:}")
private static String audience;

private static ServerOAuth2AuthorizationRequestResolver authorizeRequestResolver(
ReactiveClientRegistrationRepository clientRegistrationRepository) {
private static ServerOAuth2AuthorizationRequestResolver reactiveAuthzRequestResolver(
ReactiveClientRegistrationRepository clientRegistrationRepository, String audience) {

DefaultServerOAuth2AuthorizationRequestResolver authorizationRequestResolver =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you get the authorizationRequestResolver if it already exists? If I could figure that out, I could probably solve the chaining problem where the audience parameters overwrite the PKCE parameters.

new DefaultServerOAuth2AuthorizationRequestResolver(
clientRegistrationRepository);
authorizationRequestResolver.setAuthorizationRequestCustomizer(
authorizeRequestCustomizer());
authorizationRequestCustomizer(audience));

return authorizationRequestResolver;
}

private static OAuth2AuthorizationRequestResolver mvcAuthzRequestResolver(
ClientRegistrationRepository clientRegistrationRepository, String audience) {

DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
new DefaultOAuth2AuthorizationRequestResolver(
clientRegistrationRepository, "/oauth2/authorization");
authorizationRequestResolver.setAuthorizationRequestCustomizer(
authorizationRequestCustomizer(audience));

return authorizationRequestResolver;
}

private static Consumer<OAuth2AuthorizationRequest.Builder> authorizeRequestCustomizer() {
private static Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer(String audience) {
return customizer -> customizer
.additionalParameters(params -> params.put("audience", audience));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,12 @@ OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService(
static class OAuth2SecurityFilterChainConfiguration {

@Bean
SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception {
SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository, OktaOAuth2Properties oktaOAuth2Properties) throws Exception {
// as of Spring Security 5.4 the default chain uses oauth2Login OR a JWT resource server (NOT both)
// this does the same as both defaults merged together (and provides the previous behavior)
http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
Okta.configureOAuth2WithPkce(http, clientRegistrationRepository);
Okta.configureOAuth2WithAudience(http, clientRegistrationRepository, oktaOAuth2Properties);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The audience overrides the PKCE parameters added on the previous line. Is there a way to chain these together so both parameters are added?

http.oauth2Client();
http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import com.okta.spring.boot.oauth.config.OktaOAuth2Properties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.ApplicationContext;
Expand All @@ -30,25 +29,17 @@
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.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.web.client.RestTemplate;

import java.lang.reflect.Field;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Optional;
import java.util.function.Consumer;

import static com.okta.commons.lang.Strings.isEmpty;

final class OktaOAuth2Configurer extends AbstractHttpConfigurer<OktaOAuth2Configurer, HttpSecurity> {

@Value("${okta.oauth2.audience:}")
private String audience;

private static final Logger log = LoggerFactory.getLogger(OktaOAuth2Configurer.class);

@SuppressWarnings("rawtypes")
Expand All @@ -60,6 +51,7 @@ public void init(HttpSecurity http) throws Exception {
// make sure OktaOAuth2Properties are available
if (!context.getBeansOfType(OktaOAuth2Properties.class).isEmpty()) {
OktaOAuth2Properties oktaOAuth2Properties = context.getBean(OktaOAuth2Properties.class);

// Auth Code Flow Config

// if OAuth2ClientProperties bean is not available do NOT configure
Expand All @@ -71,8 +63,7 @@ public void init(HttpSecurity http) throws Exception {
&& !isEmpty(propertiesProvider.getIssuerUri())
&& !isEmpty(propertiesRegistration.getClientId())) {
// configure Okta user services
ClientRegistrationRepository clientRegistrationRepository = context.getBean(ClientRegistrationRepository.class);
configureLogin(http, oktaOAuth2Properties, context.getEnvironment(), clientRegistrationRepository);
configureLogin(http, oktaOAuth2Properties, context.getEnvironment());

// check for RP-Initiated logout
if (!context.getBeansOfType(OidcClientInitiatedLogoutSuccessHandler.class).isEmpty()) {
Expand Down Expand Up @@ -171,22 +162,14 @@ private void unsetJwtConfigurer(OAuth2ResourceServerConfigurer oAuth2ResourceSer
});
}

private void configureLogin(HttpSecurity http, OktaOAuth2Properties oktaOAuth2Properties, Environment environment,
ClientRegistrationRepository clientRegistrationRepository) throws Exception {
private void configureLogin(HttpSecurity http, OktaOAuth2Properties oktaOAuth2Properties, Environment environment) throws Exception {

RestTemplate restTemplate = OktaOAuth2ResourceServerAutoConfig.restTemplate(oktaOAuth2Properties);

http.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization
.authorizationRequestResolver(authorizationRequestResolver(clientRegistrationRepository)))
.tokenEndpoint(token -> token
.accessTokenResponseClient(accessTokenResponseClient(restTemplate)))
);
http.oauth2Login()
.tokenEndpoint()
.accessTokenResponseClient(accessTokenResponseClient(restTemplate));

String audienceProperty = environment.getProperty("okta.oauth2.audience");
if (audienceProperty != null) {
audience = audienceProperty;
}
String redirectUriProperty = environment.getProperty("spring.security.oauth2.client.registration.okta.redirect-uri");
if (redirectUriProperty != null) {
// remove `{baseUrl}` pattern, if present, as Spring will solve this on its own
Expand Down Expand Up @@ -222,19 +205,4 @@ private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> acc

return accessTokenResponseClient;
}

private OAuth2AuthorizationRequestResolver authorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
new DefaultOAuth2AuthorizationRequestResolver(
clientRegistrationRepository, "/oauth2/authorization");
authorizationRequestResolver.setAuthorizationRequestCustomizer(
authorizationRequestCustomizer());

return authorizationRequestResolver;
}

private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() {
return customizer -> customizer
.additionalParameters(params -> params.put("audience", audience));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@

import com.okta.spring.boot.oauth.config.OktaOAuth2Properties;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
Expand Down Expand Up @@ -52,9 +50,6 @@
@Import(AuthorityProvidersConfig.class)
class ReactiveOktaOAuth2AutoConfig {

@Value("${okta.oauth2.audience:}")
private String audience;

@Bean
@ConditionalOnMissingBean
ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService(Collection<AuthoritiesProvider> authoritiesProviders) {
Expand All @@ -69,13 +64,13 @@ OidcReactiveOAuth2UserService oidcUserService(Collection<AuthoritiesProvider> au
}

@Bean
@ConditionalOnBean(ReactiveJwtDecoder.class)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this was necessary to invoke this configuration when no SecurityConfiguration exists.

@ConditionalOnMissingBean(SecurityWebFilterChain.class)
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder jwtDecoder, ReactiveClientRegistrationRepository clientRegistrationRepository) {
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder jwtDecoder, ReactiveClientRegistrationRepository clientRegistrationRepository, OktaOAuth2Properties properties) throws Exception {
// as of Spring Security 5.4 the default chain uses oauth2Login OR a JWT resource server (NOT both)
// this does the same as both defaults merged together (and provides the previous behavior)
http.authorizeExchange().anyExchange().authenticated();
Okta.configureOAuth2WithPkce(http, clientRegistrationRepository);
Okta.configureOAuth2WithAudience(http, clientRegistrationRepository, properties);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line clobbers the previous lines added parameters. We need to figure out a way to chain them together.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arvindkrishnakumar-okta It seems like we could create the DefaultServerOAuth2AuthorizationRequestResolver before passing it to Okta's static methods, but that would change the method signature for configureOAuth2WithPkce(). I believe that would be a breaking change and require a major version bump. Do you have any other ideas?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mraible I think that is the only way out here to resolve the clobbering.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to overload the method with new signature and deprecate the current one?

http.oauth2Client();
http.oauth2ResourceServer((server) -> customDecoder(server, jwtDecoder));
return http.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,15 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, SpringAp
environment.getPropertySources().addLast(oktaOpaqueTokenPropertySource(environment, oidcMetadata));
}
environment.getPropertySources().addLast(oktaRedirectUriPropertySource(environment));
environment.getPropertySources().addLast(otkaForcePkcePropertySource(environment, oidcMetadata));
environment.getPropertySources().addLast(oktaForcePkcePropertySource(environment, oidcMetadata));

if (application != null) {
// This is required as EnvironmentPostProcessors are run before logging system is initialized
application.addInitializers(ctx -> log.replayTo(OktaOAuth2PropertiesMappingEnvironmentPostProcessor.class));
}
}

private PropertySource<?> otkaForcePkcePropertySource(ConfigurableEnvironment environment, OIDCMetadata oidcMetadata) {
private PropertySource<?> oktaForcePkcePropertySource(ConfigurableEnvironment environment, OIDCMetadata oidcMetadata) {
Map<String, Object> props = new HashMap<>();
props.put("spring.security.oauth2.client.registration.okta.client-authentication-method", oidcMetadata.getClientAuthenticationMethod());

Expand Down