Skip to content

Commit

Permalink
Token exchange: more flexible subject / actor configuration (#8812)
Browse files Browse the repository at this point in the history
  • Loading branch information
adutra authored Jun 13, 2024
1 parent a763674 commit 8726452
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 131 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 @@ -15,11 +15,21 @@
*/
package org.projectnessie.client.auth.oauth2;

import jakarta.annotation.Nullable;
import java.time.Instant;
import org.immutables.value.Value;

@Value.Immutable
public interface AccessToken extends Token {

/** The type of the token issued, typically "Bearer". */
String getTokenType();

static AccessToken of(String payload, String tokenType, @Nullable Instant expirationTime) {
return ImmutableAccessToken.builder()
.payload(payload)
.tokenType(tokenType)
.expirationTime(expirationTime)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@
*/
package org.projectnessie.client.auth.oauth2;

import jakarta.annotation.Nullable;
import java.time.Instant;
import org.immutables.value.Value;

@Value.Immutable
public interface RefreshToken extends Token {}
public interface RefreshToken extends Token {

static RefreshToken of(String payload, @Nullable Instant expirationTime) {
return ImmutableRefreshToken.builder().payload(payload).expirationTime(expirationTime).build();
}
}
Loading

0 comments on commit 8726452

Please sign in to comment.