Skip to content

Commit

Permalink
OAUth2Client token exchange: more flexible subject / actor configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
adutra committed Jun 13, 2024
1 parent 4b6c2b3 commit 1ef6df7
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,18 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ACTOR_TOKEN;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ACTOR_TOKEN_TYPE;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ENABLED;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN_TYPE;
import static org.projectnessie.client.auth.oauth2.GrantType.AUTHORIZATION_CODE;
import static org.projectnessie.client.auth.oauth2.GrantType.CLIENT_CREDENTIALS;
import static org.projectnessie.client.auth.oauth2.GrantType.DEVICE_CODE;
import static org.projectnessie.client.auth.oauth2.GrantType.PASSWORD;
import static org.projectnessie.client.auth.oauth2.TokenExchangeConfig.CURRENT_ACCESS_TOKEN;
import static org.projectnessie.client.auth.oauth2.TokenExchangeConfig.CURRENT_REFRESH_TOKEN;
import static org.projectnessie.client.auth.oauth2.TypedToken.URN_ID_TOKEN;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand All @@ -33,6 +41,7 @@
import java.net.URI;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
Expand Down Expand Up @@ -385,20 +394,25 @@ void testOAuth2ClientTokenExchangeDelegation1() {
new OAuth2Client(clientConfig("Private1", true, true).grantType(PASSWORD).build())) {
subjectToken = subjectClient.fetchNewTokens().getAccessToken();
}
TokenExchangeConfig tokenExchangeConfig =
TokenExchangeConfig.builder()
.enabled(true)
.subjectToken(TypedToken.fromAccessToken(subjectToken))
.actorTokenProvider(
(accessToken, refreshToken) -> TypedToken.fromAccessToken(accessToken))
.build();
Map<String, String> config =
ImmutableMap.of(
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ENABLED,
"true",
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN,
subjectToken.getPayload(),
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN_TYPE,
URN_ID_TOKEN.toString(),
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ACTOR_TOKEN,
CURRENT_ACCESS_TOKEN);
try (OAuth2Client client =
new OAuth2Client(
clientConfig("Private2", true, true)
.tokenExchangeConfig(tokenExchangeConfig)
.build())) {
new OAuth2Client(
clientConfig("Private2", true, true)
.tokenExchangeConfig(TokenExchangeConfig.fromConfigSupplier(config::get))
.build());
HttpClient validatingClient = validatingHttpClient("Private2").build()) {
Tokens tokens = client.fetchNewTokens();
soft.assertThat(tokens.getAccessToken()).isNotNull();
tryUseAccessToken(validatingClient, tokens.getAccessToken());
}
}

Expand All @@ -413,20 +427,25 @@ void testOAuth2ClientTokenExchangeDelegation2() {
new OAuth2Client(clientConfig("Private1", true, true).grantType(PASSWORD).build())) {
actorToken = client.fetchNewTokens().getAccessToken();
}
TokenExchangeConfig tokenExchangeConfig =
TokenExchangeConfig.builder()
.enabled(true)
.subjectTokenProvider(
(accessToken, refreshToken) -> TypedToken.fromAccessToken(accessToken))
.actorToken(TypedToken.fromAccessToken(actorToken))
.build();
Map<String, String> config =
ImmutableMap.of(
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ENABLED,
"true",
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN,
CURRENT_ACCESS_TOKEN,
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ACTOR_TOKEN,
actorToken.getPayload(),
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ACTOR_TOKEN_TYPE,
URN_ID_TOKEN.toString());
try (OAuth2Client client =
new OAuth2Client(
clientConfig("Private2", true, true)
.tokenExchangeConfig(tokenExchangeConfig)
.build())) {
new OAuth2Client(
clientConfig("Private2", true, true)
.tokenExchangeConfig(TokenExchangeConfig.fromConfigSupplier(config::get))
.build());
HttpClient validatingClient = validatingHttpClient("Private2").build()) {
Tokens tokens = client.fetchNewTokens();
soft.assertThat(tokens.getAccessToken()).isNotNull();
tryUseAccessToken(validatingClient, tokens.getAccessToken());
}
}

Expand All @@ -436,21 +455,23 @@ void testOAuth2ClientTokenExchangeDelegation2() {
*/
@Test
void testOAuth2ClientTokenExchangeDelegation3() {
TokenExchangeConfig tokenExchangeConfig =
TokenExchangeConfig.builder()
.enabled(true)
.subjectTokenProvider(
(accessToken, refreshToken) -> TypedToken.fromAccessToken(accessToken))
.actorTokenProvider(
(accessToken, refreshToken) -> TypedToken.fromAccessToken(accessToken))
.build();
Map<String, String> config =
ImmutableMap.of(
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ENABLED,
"true",
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN,
CURRENT_REFRESH_TOKEN,
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ACTOR_TOKEN,
CURRENT_ACCESS_TOKEN);
try (OAuth2Client client =
new OAuth2Client(
clientConfig("Private1", true, true)
.tokenExchangeConfig(tokenExchangeConfig)
.build())) {
new OAuth2Client(
clientConfig("Private1", true, true)
.tokenExchangeConfig(TokenExchangeConfig.fromConfigSupplier(config::get))
.build());
HttpClient validatingClient = validatingHttpClient("Private2").build()) {
Tokens tokens = client.fetchNewTokens();
soft.assertThat(tokens.getAccessToken()).isNotNull();
tryUseAccessToken(validatingClient, tokens.getAccessToken());
}
}

Expand All @@ -465,18 +486,23 @@ void testOAuth2ClientTokenExchangeImpersonation1() {
new OAuth2Client(clientConfig("Private1", true, true).grantType(PASSWORD).build())) {
subjectToken = subjectClient.fetchNewTokens().getAccessToken();
}
TokenExchangeConfig tokenExchangeConfig =
TokenExchangeConfig.builder()
.enabled(true)
.subjectToken(TypedToken.fromAccessToken(subjectToken))
.build();
Map<String, String> config =
ImmutableMap.of(
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ENABLED,
"true",
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN,
subjectToken.getPayload(),
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN_TYPE,
URN_ID_TOKEN.toString());
try (OAuth2Client client =
new OAuth2Client(
clientConfig("Private2", true, true)
.tokenExchangeConfig(tokenExchangeConfig)
.build())) {
new OAuth2Client(
clientConfig("Private2", true, true)
.tokenExchangeConfig(TokenExchangeConfig.fromConfigSupplier(config::get))
.build());
HttpClient validatingClient = validatingHttpClient("Private2").build()) {
Tokens tokens = client.fetchNewTokens();
soft.assertThat(tokens.getAccessToken()).isNotNull();
tryUseAccessToken(validatingClient, tokens.getAccessToken());
}
}

Expand All @@ -486,14 +512,21 @@ void testOAuth2ClientTokenExchangeImpersonation1() {
*/
@Test
void testOAuth2ClientTokenExchangeImpersonation2() {
TokenExchangeConfig tokenExchangeConfig = TokenExchangeConfig.builder().enabled(true).build();
Map<String, String> config =
ImmutableMap.of(
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ENABLED,
"true",
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN,
CURRENT_ACCESS_TOKEN);
try (OAuth2Client client =
new OAuth2Client(
clientConfig("Private2", true, true)
.tokenExchangeConfig(tokenExchangeConfig)
.build())) {
new OAuth2Client(
clientConfig("Private1", true, true)
.tokenExchangeConfig(TokenExchangeConfig.fromConfigSupplier(config::get))
.build());
HttpClient validatingClient = validatingHttpClient("Private2").build()) {
Tokens tokens = client.fetchNewTokens();
soft.assertThat(tokens.getAccessToken()).isNotNull();
tryUseAccessToken(validatingClient, tokens.getAccessToken());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,46 +388,68 @@ public final class NessieConfigConstants {
"nessie.authentication.oauth2.token-exchange.scopes";

/**
* For token exchanges only. The subject token to exchange.
* For token exchanges only. The subject token to exchange. This can take 3 kinds of values:
*
* <p>By default, the client will use its current access token as the subject token. But if this
* property is set, the client will use the static token provided here instead.
* <ul>
* <li>The value {@value
* org.projectnessie.client.auth.oauth2.TokenExchangeConfig#CURRENT_ACCESS_TOKEN}, if the
* client should use its current access token;
* <li>The value {@value
* org.projectnessie.client.auth.oauth2.TokenExchangeConfig#CURRENT_REFRESH_TOKEN}, if the
* client should use its current refresh token (if available);
* <li>An arbitrary token: in this case, the client will always use the static token provided
* here.
* </ul>
*
* The default is to use the current access token.
*/
@ConfigItem(section = "OAuth2 Authentication")
public static final String CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN =
"nessie.authentication.oauth2.token-exchange.subject-token";

/**
* For token exchanges only. The type of the subject token. By default, {@code
* urn:ietf:params:oauth:token-type:access_token}.
* For token exchanges only. The type of the subject token. Must be a valid URN. The default is
* either {@code urn:ietf:params:oauth:token-type:access_token} or {@code
* urn:ietf:params:oauth:token-type:refresh_token}, depending on the value of {@value
* #CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN}.
*
* <p>If {@value #CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN} is set, this property will be
* used to define the type of the provided subject token. If that property not set, this property
* will define the type of the access token obtained by the client – in this case, please note
* that if an incorrect token type is provided, the token exchange could fail.
* <p>If the client is configured to use its access or refresh token as the subject token, please
* note that if an incorrect token type is provided here, the token exchange could fail.
*/
@ConfigItem(section = "OAuth2 Authentication")
public static final String CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN_TYPE =
"nessie.authentication.oauth2.token-exchange.subject-token-type";

/**
* For token exchanges only. The actor token to exchange.
* For token exchanges only. The actor token to exchange. This can take 4 kinds of values:
*
* <ul>
* <li>The value {@value org.projectnessie.client.auth.oauth2.TokenExchangeConfig#NO_TOKEN}, if
* the client should not include any actor token in the exchange request;
* <li>The value {@value
* org.projectnessie.client.auth.oauth2.TokenExchangeConfig#CURRENT_ACCESS_TOKEN}, if the
* client should use its current access token;
* <li>The value {@value
* org.projectnessie.client.auth.oauth2.TokenExchangeConfig#CURRENT_REFRESH_TOKEN}, if the
* client should use its current refresh token (if available);
* <li>An arbitrary token: in this case, the client will always use the static token provided
* here.
* </ul>
*
* <p>By default, the client will not use an actor token. But if this property is set, the client
* will use the static token provided here as the actor token.
* The default is to not include any actor token.
*/
@ConfigItem(section = "OAuth2 Authentication")
public static final String CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ACTOR_TOKEN =
"nessie.authentication.oauth2.token-exchange.actor-token";

/**
* For token exchanges only. The type of the actor token. By default, {@code
* urn:ietf:params:oauth:token-type:access_token}.
* For token exchanges only. The type of the actor token. Must be a valid URN. The default is
* either {@code urn:ietf:params:oauth:token-type:access_token} or {@code
* urn:ietf:params:oauth:token-type:refresh_token}, depending on the value of {@value
* #CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ACTOR_TOKEN}.
*
* <p>If {@value #CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ACTOR_TOKEN} is set, this property will be
* used to define the type of the provided subject token. If that property not set, this property
* will define the type of the access token obtained by the client – in this case, please note
* that if an incorrect token type is provided, the token exchange could fail.
* <p>If the client is configured to use its access or refresh token as the actor token, please
* note that if an incorrect token type is provided here, the token exchange could fail.
*/
@ConfigItem(section = "OAuth2 Authentication")
public static final String CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ACTOR_TOKEN_TYPE =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_TOKEN_ENDPOINT;
import static org.projectnessie.client.auth.oauth2.OAuth2ClientConfig.applyConfigOption;
import static org.projectnessie.client.auth.oauth2.TypedToken.URN_ACCESS_TOKEN;
import static org.projectnessie.client.auth.oauth2.TypedToken.URN_REFRESH_TOKEN;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.net.URI;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.immutables.value.Value;
Expand All @@ -51,6 +51,10 @@ public interface TokenExchangeConfig {

String SCOPES_INHERIT = "\\inherit\\";

String CURRENT_ACCESS_TOKEN = "current_access_token";
String CURRENT_REFRESH_TOKEN = "current_refresh_token";
String NO_TOKEN = "no_token";

static TokenExchangeConfig fromConfigSupplier(Function<String, String> config) {
String enabled = config.apply(CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ENABLED);
if (!Boolean.parseBoolean(enabled)) {
Expand All @@ -71,39 +75,45 @@ static TokenExchangeConfig fromConfigSupplier(Function<String, String> config) {
config, CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_RESOURCE, builder::resource, URI::create);
applyConfigOption(config, CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SCOPES, builder::scope);
applyConfigOption(config, CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_AUDIENCE, builder::audience);

String subjectToken = config.apply(CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN);
String actorToken = config.apply(CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ACTOR_TOKEN);
AtomicReference<URI> subjectTokenType = new AtomicReference<>(URN_ACCESS_TOKEN);
AtomicReference<URI> actorTokenType = new AtomicReference<>(URN_ACCESS_TOKEN);
applyConfigOption(
config,
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN_TYPE,
subjectTokenType::set,
URI::create);
applyConfigOption(
config,
CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ACTOR_TOKEN_TYPE,
actorTokenType::set,
URI::create);
// If a subject token is statically provided, use it. If no subject token is provided, then let
// the client be the subject, since a subject token is always required.
if (subjectToken != null) {
builder.subjectToken(TypedToken.of(subjectToken, subjectTokenType.get()));
} else {

Optional<URI> subjectTokenType =
Optional.ofNullable(config.apply(CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_SUBJECT_TOKEN_TYPE))
.map(URI::create);
Optional<URI> actorTokenType =
Optional.ofNullable(config.apply(CONF_NESSIE_OAUTH2_TOKEN_EXCHANGE_ACTOR_TOKEN_TYPE))
.map(URI::create);

if (subjectToken == null || subjectToken.equalsIgnoreCase(CURRENT_ACCESS_TOKEN)) {
builder.subjectTokenProvider(
(accessToken, refreshToken) ->
TypedToken.of(accessToken.getPayload(), subjectTokenType.get()));
}
// If an actor token is statically provided, use it. If no actor token is provided, but a
// subject token is statically provided, then let the client be the actor; otherwise, no actor
// token should be used.
if (actorToken != null) {
builder.actorToken(TypedToken.of(actorToken, actorTokenType.get()));
} else if (subjectToken != null) {
builder.actorTokenProvider(
TypedToken.of(accessToken, subjectTokenType.orElse(URN_ACCESS_TOKEN)));
} else if (subjectToken.equalsIgnoreCase(CURRENT_REFRESH_TOKEN)) {
builder.subjectTokenProvider(
(accessToken, refreshToken) ->
TypedToken.of(accessToken.getPayload(), actorTokenType.get()));
TypedToken.of(refreshToken, subjectTokenType.orElse(URN_REFRESH_TOKEN)));
} else {
builder.subjectToken(TypedToken.of(subjectToken, subjectTokenType.orElse(URN_ACCESS_TOKEN)));
}

if (actorToken != null && !actorToken.equalsIgnoreCase(NO_TOKEN)) {
if (actorToken.equalsIgnoreCase(CURRENT_ACCESS_TOKEN)) {
builder.actorTokenProvider(
(accessToken, refreshToken) ->
TypedToken.of(accessToken, actorTokenType.orElse(URN_ACCESS_TOKEN)));
} else if (actorToken.equalsIgnoreCase(CURRENT_REFRESH_TOKEN)) {
builder.actorTokenProvider(
(accessToken, refreshToken) ->
refreshToken == null
? null
: TypedToken.of(refreshToken, actorTokenType.orElse(URN_REFRESH_TOKEN)));
} else {
builder.actorToken(TypedToken.of(actorToken, actorTokenType.orElse(URN_ACCESS_TOKEN)));
}
}

return builder.build();
}

Expand Down Expand Up @@ -222,7 +232,7 @@ default String getScope() {
@Value.Default
@Value.Auxiliary
default BiFunction<AccessToken, RefreshToken, TypedToken> getSubjectTokenProvider() {
return (accessToken, refreshToken) -> TypedToken.fromAccessToken(accessToken);
return (accessToken, refreshToken) -> TypedToken.of(accessToken);
}

/**
Expand Down
Loading

0 comments on commit 1ef6df7

Please sign in to comment.