Skip to content

Commit

Permalink
OAuth client: add support for custom request parameters (#10154)
Browse files Browse the repository at this point in the history
  • Loading branch information
adutra authored Jan 2, 2025
1 parent 5ecbc16 commit aa137f8
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 6 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ as necessary. Empty sections will not end in the release notes.

### New Features

- When using OAuth authentication, the Nessie client now supports including extra parameters in
requests to the token endpoint. This is useful for passing custom parameters that are not covered
by the standard OAuth 2.0 specification. See the [Nessie
documentation](https://projectnessie.org/tools/client_config/#authentication-settings) for
details.

### Changes

### Deprecations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.net.Socket;
import java.net.URI;
import java.security.cert.X509Certificate;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
Expand Down Expand Up @@ -153,6 +154,8 @@ private static OAuth2ClientConfig.Builder clientConfig(String clientId) throws E
.addScope(clientId.equals("nessie-private-cc") ? "profile" : "offline_access")
.authorizationCodeFlowWebServerPort(NESSIE_CALLBACK_PORT)
.issuerUrl(issuerUrl)
// Should be ignored
.extraRequestParameters(Map.of("param1", "value1", "custom param 2", "custom value 2"))
.sslContext(insecureSslContext());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,8 @@ private static OAuth2ClientConfig.Builder clientConfig(
.password("s3cr3t")
// Otherwise Keycloak complains about missing scope, but still issues tokens
.addScope("openid")
// Should be ignored
.extraRequestParameters(Map.of("param1", "value1", "custom param 2", "custom value 2"))
.defaultAccessTokenLifespan(Duration.ofSeconds(10))
.defaultRefreshTokenLifespan(Duration.ofSeconds(15))
.refreshSafetyWindow(Duration.ofSeconds(5))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,29 @@ public final class NessieConfigConstants {
public static final String CONF_NESSIE_OAUTH2_CLIENT_SECRET =
"nessie.authentication.oauth2.client-secret";

/**
* Extra parameters to include in each request to the token endpoint. This is useful for custom
* parameters that are not covered by the standard OAuth2.0 specification. Optional, defaults to
* empty.
*
* <p>The format of this field is a comma-separated list of key-value pairs, separated by an equal
* sign. The values must NOT be URL-encoded. Example:
*
* <pre>{@code
* nessie.authentication.oauth2.extra-params = "custom_param1=custom_value1,custom_param2=custom_value2"
* }</pre>
*
* For example, Auth0 requires the {@code audience} parameter to be set to the API identifier.
* This can be done by setting the following configuration:
*
* <pre>{@code
* nessie.authentication.oauth2.extra-params = "audience=https://nessie-catalog/api"
* }</pre>
*/
@ConfigItem(section = "OAuth2 Authentication")
public static final String CONF_NESSIE_OAUTH2_EXTRA_PARAMS =
"nessie.authentication.oauth2.extra-params";

/**
* Username to use when authenticating against the OAuth2 server. Required if using OAuth2
* authentication and "password" grant type, ignored otherwise.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ abstract class AbstractFlow implements Flow {

<REQ extends TokenRequestBase, RESP extends TokenResponseBase> Tokens invokeTokenEndpoint(
TokenRequestBase.Builder<REQ> request, Class<? extends RESP> responseClass) {
request.extraParameters(config.getExtraRequestParameters());
getScope().ifPresent(request::scope);
maybeAddClientId(request);
return invokeEndpoint(getResolvedTokenEndpoint(), request.build(), responseClass)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_DEVICE_AUTH_ENDPOINT;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_DEVICE_CODE_FLOW_POLL_INTERVAL;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_DEVICE_CODE_FLOW_TIMEOUT;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_EXTRA_PARAMS;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_GRANT_TYPE;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_ISSUER_URL;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_PASSWORD;
Expand All @@ -49,7 +50,9 @@
import java.net.URI;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
Expand Down Expand Up @@ -143,11 +146,36 @@ static OAuth2AuthenticatorConfig fromConfigSupplier(Function<String, String> con
CONF_NESSIE_OAUTH2_DEVICE_CODE_FLOW_POLL_INTERVAL,
builder::deviceCodeFlowPollInterval,
Duration::parse);
applyConfigOption(
config,
CONF_NESSIE_OAUTH2_EXTRA_PARAMS,
builder::extraRequestParameters,
OAuth2AuthenticatorConfig::parseExtraParams);
builder.tokenExchangeConfig(TokenExchangeConfig.fromConfigSupplier(config));
builder.impersonationConfig(ImpersonationConfig.fromConfigSupplier(config));
return builder.build();
}

static Map<String, String> parseExtraParams(String text) {
if (text == null || text.isBlank()) {
return Map.of();
}
Map<String, String> map = new HashMap<>();
for (String s : text.split(",")) {
String[] a = s.split("=", 2);
String key = a[0].trim();
String value = a.length > 1 ? a[1].trim() : "";
if (map.put(key, value) != null) {
throw new IllegalArgumentException(
String.format(
"OAuth2 authentication has configuration errors and could not be initialized: "
+ "extra parameter '%s' is defined multiple times (%s)",
key, CONF_NESSIE_OAUTH2_EXTRA_PARAMS));
}
}
return map;
}

/**
* The root URL of the OpenID Connect identity issuer provider, which will be used for discovering
* supported endpoints and their locations.
Expand Down Expand Up @@ -252,6 +280,12 @@ default Optional<String> getScope() {
*/
List<String> getScopes();

/**
* Additional parameters to be included in the request. This is useful for custom parameters that
* are not covered by the standard OAuth2.0 specification.
*/
Map<String, String> getExtraRequestParameters();

@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
@Value.Default
Expand Down Expand Up @@ -470,6 +504,9 @@ default Builder scope(String scope) {
@CanIgnoreReturnValue
Builder scopes(Iterable<String> scopes);

@CanIgnoreReturnValue
Builder extraRequestParameters(Map<String, ? extends String> extraRequestParameters);

@Deprecated
@CanIgnoreReturnValue
Builder tokenExchangeEnabled(boolean tokenExchangeEnabled);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Consumer;
Expand Down Expand Up @@ -420,7 +421,7 @@ && getAuthorizationCodeFlowWebServerPort().getAsInt() <= 65535,
CONF_NESSIE_OAUTH2_GRANT_TYPE_TOKEN_EXCHANGE);
if (!violations.isEmpty()) {
throw new IllegalArgumentException(
"OAuth2 authentication is missing some parameters and could not be initialized: "
"OAuth2 authentication has configuration errors and could not be initialized: "
+ join(", ", violations));
}
}
Expand Down Expand Up @@ -498,6 +499,9 @@ default Builder password(String password) {
@Override
Builder scopes(Iterable<String> scopes);

@CanIgnoreReturnValue
Builder extraRequestParameters(Map<String, ? extends String> extraRequestParameters);

@Override
Builder tokenExchangeConfig(TokenExchangeConfig tokenExchangeConfig);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
*/
package org.projectnessie.client.auth.oauth2;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Map;
import javax.annotation.Nullable;

/**
Expand Down Expand Up @@ -63,11 +65,21 @@ interface TokenRequestBase {
@JsonProperty("scope")
String getScope();

/**
* Additional parameters to be included in the request. This is useful for custom parameters that
* are not covered by the standard OAuth2.0 specification.
*/
@JsonAnyGetter
Map<String, String> extraParameters();

interface Builder<T extends TokenRequestBase> {

@CanIgnoreReturnValue
Builder<T> scope(String scope);

@CanIgnoreReturnValue
Builder<T> extraParameters(Map<String, ? extends String> extraParameters);

T build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,24 @@ void testBadRequest() throws Exception {
}
}

@Test
void testExtraRequestParameters() throws Exception {

try (HttpTestServer server = new HttpTestServer(handler(), true)) {

OAuth2ClientConfig config =
configBuilder(server, false)
.grantType(GrantType.CLIENT_CREDENTIALS)
.extraRequestParameters(Map.of("key1", "value1", "key2", "value2"))
.build();

try (OAuth2Client client = new OAuth2Client(config)) {
Tokens tokens = client.fetchNewTokens();
checkInitialResponse(tokens, false);
}
}
}

private class TestRequestHandler implements HttpTestServer.RequestHandler {

private volatile boolean deviceAuthorized = false;
Expand Down Expand Up @@ -631,6 +649,10 @@ private void handleTokenEndpoint(HttpServletRequest req, HttpServletResponse res
if (grantType.equals(GrantType.CLIENT_CREDENTIALS.canonicalName())) {
request = OBJECT_MAPPER.convertValue(data, ClientCredentialsTokenRequest.class);
soft.assertThat(request.getScope()).isEqualTo("test");
if (request.extraParameters().size() > 1) {
soft.assertThat(request.extraParameters())
.contains(entry("key1", "value1"), entry("key2", "value2"));
}
response = getClientCredentialsTokensResponse();
} else if (grantType.equals(GrantType.PASSWORD.canonicalName())) {
request = OBJECT_MAPPER.convertValue(data, PasswordTokenRequest.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_DEVICE_AUTH_ENDPOINT;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_DEVICE_CODE_FLOW_POLL_INTERVAL;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_DEVICE_CODE_FLOW_TIMEOUT;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_EXTRA_PARAMS;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_GRANT_TYPE;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_IMPERSONATION_CLIENT_ID;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_IMPERSONATION_CLIENT_SECRET;
Expand Down Expand Up @@ -76,7 +77,7 @@ void testCheck(OAuth2ClientConfig.Builder config, List<String> expected) {
assertThatIllegalArgumentException()
.isThrownBy(config::build)
.withMessage(
"OAuth2 authentication is missing some parameters and could not be initialized: "
"OAuth2 authentication has configuration errors and could not be initialized: "
+ join(", ", expected));
}

Expand Down Expand Up @@ -312,12 +313,12 @@ static Stream<Arguments> testFromConfig() {
ImmutableMap.of(
CONF_NESSIE_OAUTH2_TOKEN_ENDPOINT, "https://example.com/token",
CONF_NESSIE_OAUTH2_CLIENT_SECRET, "s3cr3t",
CONF_NESSIE_OAUTH2_REFRESH_SAFETY_WINDOW, "PT10S",
CONF_NESSIE_OAUTH2_DEFAULT_ACCESS_TOKEN_LIFESPAN, "PT30S",
CONF_NESSIE_OAUTH2_CLIENT_SCOPES, "test"),
CONF_NESSIE_OAUTH2_CLIENT_ID, "client1",
CONF_NESSIE_OAUTH2_EXTRA_PARAMS, "key1=value1,key1=value2"),
null,
new IllegalArgumentException(
"OAuth2 authentication is missing some parameters and could not be initialized: client ID must not be empty (nessie.authentication.oauth2.client-id)")),
"OAuth2 authentication has configuration errors and could not be initialized: "
+ "extra parameter 'key1' is defined multiple times (nessie.authentication.oauth2.extra-params)")),
Arguments.of(
ImmutableMap.builder()
.put(CONF_NESSIE_OAUTH2_ISSUER_URL, "https://example.com/")
Expand Down Expand Up @@ -357,6 +358,9 @@ static Stream<Arguments> testFromConfig() {
.put(
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ACTOR_TOKEN_TYPE,
TypedToken.URN_JWT.toString())
.put(
CONF_NESSIE_OAUTH2_EXTRA_PARAMS,
" extra1 = param1 , extra2=param 2 , extra3= , = ")
.build(),
OAuth2ClientConfig.builder()
.issuerUrl(URI.create("https://example.com/"))
Expand All @@ -369,6 +373,8 @@ static Stream<Arguments> testFromConfig() {
.username("Alice")
.password("s3cr3t")
.addScope("test")
.extraRequestParameters(
ImmutableMap.of("extra1", "param1", "extra2", "param 2", "extra3", "", "", ""))
.defaultAccessTokenLifespan(Duration.ofSeconds(30))
.defaultRefreshTokenLifespan(Duration.ofSeconds(30))
.refreshSafetyWindow(Duration.ofSeconds(10))
Expand Down

0 comments on commit aa137f8

Please sign in to comment.