Skip to content

Commit dc6937c

Browse files
authored
Merge pull request #158 from shift7-ch/feature/token-endpoint
Use token exchange proxy via POST `/api/storage/s3-token`
2 parents ee1d7c8 + 355206b commit dc6937c

18 files changed

+3165
-2635
lines changed

hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public AttributedList<Path> list(final Path directory, final ListProgressListene
9696
}
9797
throw e;
9898
}
99-
final Host bookmark = vaultService.getStorageBackend(protocols, configDto, vaultDto.getId(),
99+
final Host bookmark = vaultService.getStorageBackend(protocols, session, configDto, vaultDto.getId(),
100100
vaultMetadata.storage(), tokens);
101101
log.debug("Configured {} for vault {}", bookmark, vaultDto);
102102
final Session<?> storage = SessionFactory.create(bookmark, trust, key);

hub/src/main/java/cloud/katta/protocols/hub/serializer/HubConfigDtoDeserializer.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import com.dd.plist.NSDictionary;
1414

1515
import static ch.cyberduck.core.Profile.*;
16-
import static cloud.katta.protocols.s3.S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_CLIENT_ID;
1716

1817
public class HubConfigDtoDeserializer extends ProxyDeserializer<NSDictionary> {
1918

@@ -33,9 +32,6 @@ public <L> List<L> listForKey(final String key) {
3332
switch(key) {
3433
case PROPERTIES_KEY:
3534
final List<String> properties = new ArrayList<>(super.listForKey(key));
36-
if(dto.getKeycloakClientIdCryptomatorVaults() != null) {
37-
properties.add(String.format("%s=%s", OAUTH_TOKENEXCHANGE_CLIENT_ID, dto.getKeycloakClientIdCryptomatorVaults()));
38-
}
3935
return (List<L>) properties;
4036
case SCOPES_KEY:
4137
return (List<L>) Collections.singletonList("openid");

hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ public <L> List<L> listForKey(final String key) {
5656
if(dto.getStsDurationSeconds() != null) {
5757
properties.add(String.format("%s=%s", S3AssumeRoleProtocol.S3_ASSUMEROLE_DURATIONSECONDS, dto.getStsDurationSeconds().toString()));
5858
}
59-
properties.add(String.format("%s=%s", S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_CLIENT_SECRET, ""));
6059
}
6160
log.debug("Return properties {} from {}", properties, dto);
6261
return (List<L>) properties;

hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleProtocol.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ public class S3AssumeRoleProtocol extends S3Protocol {
1515

1616
// Token exchange
1717
public static final String OAUTH_TOKENEXCHANGE = "oauth.tokenexchange";
18-
public static final String OAUTH_TOKENEXCHANGE_CLIENT_ID = "oauth.tokenexchange.client_id";
19-
public static final String OAUTH_TOKENEXCHANGE_CLIENT_SECRET = "oauth.tokenexchange.audience.client_secret";
20-
public static final String OAUTH_TOKENEXCHANGE_ADDITIONAL_SCOPES = "oauth.tokenexchange.additional_scopes";
18+
public static final String OAUTH_TOKENEXCHANGE_VAULT = "oauth.tokenexchange.vault";
19+
public static final String OAUTH_TOKENEXCHANGE_BASEPATH = "oauth.tokenexchange.basepath";
2120

2221
// STS assume role with web identity from Cyberduck core (AWS + MinIO)
2322
public static final String S3_ASSUMEROLE_ROLEARN = "s3.assumerole.rolearn";

hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public void refresh() {
114114
if(StringUtils.isNotBlank(preferences.getProperty("s3.assumerole.tag"))) {
115115
request.setTags(Collections.singletonList(new Tag()
116116
.withKey(preferences.getProperty("s3.assumerole.tag"))
117-
.withValue(preferences.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_ADDITIONAL_SCOPES))));
117+
.withValue(preferences.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT))));
118118
}
119119
try {
120120
log.debug("Use request {}", request);

hub/src/main/java/cloud/katta/protocols/s3/TokenExchangeRequestInterceptor.java

Lines changed: 18 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,66 +5,49 @@
55
package cloud.katta.protocols.s3;
66

77
import ch.cyberduck.core.Credentials;
8-
import ch.cyberduck.core.DefaultIOExceptionMappingService;
98
import ch.cyberduck.core.Host;
109
import ch.cyberduck.core.LoginCallback;
1110
import ch.cyberduck.core.OAuthTokens;
1211
import ch.cyberduck.core.exception.BackgroundException;
1312
import ch.cyberduck.core.exception.LoginCanceledException;
1413
import ch.cyberduck.core.exception.LoginFailureException;
15-
import ch.cyberduck.core.http.DefaultHttpResponseExceptionMappingService;
1614
import ch.cyberduck.core.oauth.OAuth2RequestInterceptor;
17-
import ch.cyberduck.core.oauth.OAuthExceptionMappingService;
1815
import ch.cyberduck.core.preferences.HostPreferences;
1916
import ch.cyberduck.core.preferences.PreferencesReader;
2017

21-
import org.apache.commons.lang3.StringUtils;
2218
import org.apache.http.client.HttpClient;
2319
import org.apache.logging.log4j.LogManager;
2420
import org.apache.logging.log4j.Logger;
2521

26-
import java.io.IOException;
27-
import java.util.ArrayList;
2822
import java.util.Arrays;
2923
import java.util.List;
3024

25+
import cloud.katta.client.ApiException;
26+
import cloud.katta.client.api.StorageResourceApi;
27+
import cloud.katta.client.model.AccessTokenResponse;
28+
import cloud.katta.protocols.hub.HubSession;
29+
import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService;
3130
import com.auth0.jwt.JWT;
3231
import com.auth0.jwt.exceptions.JWTDecodeException;
3332
import com.auth0.jwt.interfaces.DecodedJWT;
34-
import com.google.api.client.auth.oauth2.TokenRequest;
35-
import com.google.api.client.auth.oauth2.TokenResponse;
36-
import com.google.api.client.auth.oauth2.TokenResponseException;
37-
import com.google.api.client.http.GenericUrl;
38-
import com.google.api.client.http.HttpResponseException;
39-
import com.google.api.client.http.apache.v2.ApacheHttpTransport;
40-
import com.google.api.client.json.gson.GsonFactory;
41-
42-
import static cloud.katta.protocols.s3.S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE;
4333

4434
/**
4535
* Exchange OIDC token to scoped token using OAuth 2.0 Token Exchange. Used for S3-STS in Katta.
4636
*/
4737
public class TokenExchangeRequestInterceptor extends OAuth2RequestInterceptor {
4838
private static final Logger log = LogManager.getLogger(TokenExchangeRequestInterceptor.class);
4939

50-
// https://datatracker.ietf.org/doc/html/rfc8693#name-request
51-
public static final String OAUTH_GRANT_TYPE_TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange";
52-
public static final String OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_CLIENT_ID = "client_id";
53-
public static final String OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_CLIENT_SECRET = "client_secret";
54-
public static final String OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_SUBJECT_TOKEN = "subject_token";
55-
public static final String OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_SUBJECT_TOKEN_TYPE = "subject_token_type";
56-
public static final String OAUTH_TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token";
57-
// https://openid.net/specs/openid-connect-core-1_0.html
40+
/**
41+
* The party to which the ID Token was issued
42+
* <a href="https://openid.net/specs/openid-connect-core-1_0.html">...</a>
43+
*/
5844
public static final String OIDC_AUTHORIZED_PARTY = "azp";
5945

60-
6146
private final Host bookmark;
62-
private final HttpClient client;
6347

6448
public TokenExchangeRequestInterceptor(final HttpClient client, final Host bookmark, final LoginCallback prompt) throws LoginCanceledException {
6549
super(client, bookmark, prompt);
6650
this.bookmark = bookmark;
67-
this.client = client;
6851
}
6952

7053
@Override
@@ -82,49 +65,26 @@ public OAuthTokens refresh(final OAuthTokens previous) throws BackgroundExceptio
8265
*
8366
* @param previous Input tokens retrieved to exchange at the token endpoint
8467
* @return New tokens
85-
* @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_CLIENT_ID
86-
* @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_ADDITIONAL_SCOPES
68+
* @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_VAULT
69+
* @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_BASEPATH
8770
*/
8871
public OAuthTokens exchange(final OAuthTokens previous) throws BackgroundException {
8972
log.info("Exchange tokens {} for {}", previous, bookmark);
90-
final TokenRequest request = new TokenRequest(
91-
new ApacheHttpTransport(client),
92-
new GsonFactory(),
93-
new GenericUrl(bookmark.getProtocol().getOAuthTokenUrl()),
94-
OAUTH_GRANT_TYPE_TOKEN_EXCHANGE
95-
);
96-
request.set(OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_CLIENT_ID, bookmark.getProtocol().getOAuthClientId());
9773
final PreferencesReader preferences = new HostPreferences(bookmark);
98-
if(!StringUtils.isEmpty(preferences.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_CLIENT_ID))) {
99-
request.set(OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_CLIENT_ID, preferences.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_CLIENT_ID));
100-
request.set(OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_CLIENT_SECRET, preferences.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_CLIENT_SECRET));
101-
}
102-
request.set(OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_SUBJECT_TOKEN, previous.getAccessToken());
103-
request.set(OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_SUBJECT_TOKEN_TYPE, OAUTH_TOKEN_TYPE_ACCESS_TOKEN);
104-
final ArrayList<String> scopes = new ArrayList<>(bookmark.getProtocol().getOAuthScopes());
105-
if(!StringUtils.isEmpty(preferences.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_ADDITIONAL_SCOPES))) {
106-
scopes.addAll(Arrays.asList(preferences.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_ADDITIONAL_SCOPES).split(" ")));
107-
}
108-
request.setScopes(scopes);
109-
log.debug("Token exchange request {} for {}", request, bookmark);
74+
final HubSession hub = bookmark.getProtocol().getFeature(HubSession.class);
75+
log.debug("Exchange token with hub {}", hub);
76+
final StorageResourceApi api = new StorageResourceApi(hub.getClient());
11077
try {
111-
final TokenResponse tokenExchangeResponse = request.execute();
78+
AccessTokenResponse tokenExchangeResponse = api.apiStorageS3TokenPost(preferences.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT));
11279
// N.B. token exchange with Id token does not work!
11380
final OAuthTokens tokens = new OAuthTokens(tokenExchangeResponse.getAccessToken(),
11481
tokenExchangeResponse.getRefreshToken(),
115-
System.currentTimeMillis() + tokenExchangeResponse.getExpiresInSeconds() * 1000);
82+
System.currentTimeMillis() + tokenExchangeResponse.getExpiresIn() * 1000);
11683
log.debug("Received exchanged token {} for {}", tokens, bookmark);
11784
return tokens;
11885
}
119-
catch(TokenResponseException e) {
120-
throw new OAuthExceptionMappingService().map(e);
121-
}
122-
catch(HttpResponseException e) {
123-
throw new DefaultHttpResponseExceptionMappingService().map(new org.apache.http.client
124-
.HttpResponseException(e.getStatusCode(), e.getStatusMessage()));
125-
}
126-
catch(IOException e) {
127-
throw new DefaultIOExceptionMappingService().map(e);
86+
catch(ApiException e) {
87+
throw new HubExceptionMappingService().map(e);
12888
}
12989
}
13090

@@ -133,12 +93,6 @@ public Credentials validate() throws BackgroundException {
13393
final Credentials credentials = super.validate();
13494
final OAuthTokens tokens = credentials.getOauth();
13595
final String accessToken = tokens.getAccessToken();
136-
final PreferencesReader preferences = new HostPreferences(bookmark);
137-
final String tokenExchangeClientId = preferences.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_CLIENT_ID);
138-
if(StringUtils.isEmpty(tokenExchangeClientId)) {
139-
log.warn("Found {} empty, although {} is set to {} - misconfiguration?", S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_CLIENT_ID, OAUTH_TOKENEXCHANGE, preferences.getBoolean(OAUTH_TOKENEXCHANGE));
140-
return credentials;
141-
}
14296
try {
14397
final DecodedJWT jwt = JWT.decode(accessToken);
14498

hub/src/main/java/cloud/katta/workflows/CreateVaultService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public void createVault(final UserKeys userKeys, final StorageProfileDtoWrapper
134134

135135
final OAuthTokens tokens = keychain.findOAuthTokens(hubSession.getHost());
136136
final Host bookmark = new VaultServiceImpl(vaultResource, storageProfileResource).getStorageBackend(ProtocolFactory.get(),
137-
configResource.apiConfigGet(), vaultDto.getId(), metadataPayload.storage(), tokens);
137+
hubSession, configResource.apiConfigGet(), vaultDto.getId(), metadataPayload.storage(), tokens);
138138
if(storageProfileWrapper.getProtocol() == Protocol.S3) {
139139
// permanent: template upload into existing bucket from client (not backend)
140140
templateUploadService.uploadTemplate(bookmark, metadataPayload, storageDto, hashedRootDirId);

hub/src/main/java/cloud/katta/workflows/VaultService.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@
55
package cloud.katta.workflows;
66

77
import ch.cyberduck.core.Host;
8-
9-
import java.util.UUID;
10-
118
import ch.cyberduck.core.OAuthTokens;
129
import ch.cyberduck.core.ProtocolFactory;
1310

11+
import java.util.UUID;
12+
1413
import cloud.katta.client.ApiException;
1514
import cloud.katta.client.model.ConfigDto;
1615
import cloud.katta.crypto.UserKeys;
1716
import cloud.katta.crypto.uvf.UvfAccessTokenPayload;
1817
import cloud.katta.crypto.uvf.UvfMetadataPayload;
1918
import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto;
19+
import cloud.katta.protocols.hub.HubSession;
2020
import cloud.katta.workflows.exceptions.AccessException;
2121
import cloud.katta.workflows.exceptions.SecurityFailure;
2222

@@ -47,13 +47,15 @@ public interface VaultService {
4747

4848
/**
4949
* Prepares (virtual) bookmark for vault to access its configured storage backend.
50+
*
5051
* @param protocols Registered protocol implementations to access backend storage
52+
* @param hub Hub API Connection
5153
* @param configDto Hub configuration
52-
* @param vaultId Vault ID
53-
* @param metadata Storage Backend configuration
54+
* @param vaultId Vault ID
55+
* @param metadata Storage Backend configuration
5456
* @return Configuration
5557
* @throws AccessException Unsupported backend storage protocol
56-
* @throws ApiException Server error accessing storage profile
58+
* @throws ApiException Server error accessing storage profile
5759
*/
58-
Host getStorageBackend(final ProtocolFactory protocols, final ConfigDto configDto, UUID vaultId, VaultMetadataJWEBackendDto metadata, final OAuthTokens tokens) throws AccessException, ApiException;
60+
Host getStorageBackend(final ProtocolFactory protocols, final HubSession hub, final ConfigDto configDto, UUID vaultId, VaultMetadataJWEBackendDto metadata, final OAuthTokens tokens) throws AccessException, ApiException;
5961
}

hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@
3838
import com.nimbusds.jose.jwk.OctetSequenceKey;
3939

4040
import static cloud.katta.crypto.uvf.UvfMetadataPayload.UniversalVaultFormatJWKS.memberKeyFromRawKey;
41-
import static cloud.katta.protocols.s3.S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_ADDITIONAL_SCOPES;
42-
import static cloud.katta.protocols.s3.S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN;
41+
import static cloud.katta.protocols.s3.S3AssumeRoleProtocol.*;
4342

4443
public class VaultServiceImpl implements VaultService {
4544
private static final Logger log = LogManager.getLogger(VaultServiceImpl.class);
@@ -85,7 +84,7 @@ public UvfAccessTokenPayload getVaultAccessTokenJWE(final UUID vaultId, final Us
8584
}
8685

8786
@Override
88-
public Host getStorageBackend(final ProtocolFactory protocols, final ConfigDto configDto, final UUID vaultId, final VaultMetadataJWEBackendDto vaultMetadata, final OAuthTokens tokens) throws ApiException, AccessException {
87+
public Host getStorageBackend(final ProtocolFactory protocols, final HubSession hub, final ConfigDto configDto, final UUID vaultId, final VaultMetadataJWEBackendDto vaultMetadata, final OAuthTokens tokens) throws ApiException, AccessException {
8988
if(null == protocols.forName(vaultMetadata.getProvider())) {
9089
log.debug("Load missing profile {}", vaultMetadata.getProvider());
9190
final StorageProfileDtoWrapper storageProfile = StorageProfileDtoWrapper.coerce(storageProfileResourceApi
@@ -94,8 +93,8 @@ public Host getStorageBackend(final ProtocolFactory protocols, final ConfigDto c
9493
switch(storageProfile.getProtocol()) {
9594
case S3:
9695
case S3_STS:
97-
final Profile profile = new Profile(protocols.forType(protocols.find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Protocol.Type.s3), new StorageProfileDtoWrapperDeserializer(
98-
new HubConfigDtoDeserializer(configDto), storageProfile));
96+
final Profile profile = new HubAwareProfile(hub, protocols.forType(protocols.find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Type.s3),
97+
configDto, storageProfile);
9998
log.debug("Register storage profile {}", profile);
10099
protocols.register(profile);
101100
break;
@@ -123,10 +122,30 @@ public Host getStorageBackend(final ProtocolFactory protocols, final ConfigDto c
123122
credentials.setPassword(vaultMetadata.getPassword());
124123
}
125124
if(protocol.getProperties().get(S3_ASSUMEROLE_ROLEARN) != null) {
126-
bookmark.setProperty(OAUTH_TOKENEXCHANGE_ADDITIONAL_SCOPES, vaultId.toString());
125+
bookmark.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString());
126+
bookmark.setProperty(OAUTH_TOKENEXCHANGE_BASEPATH, this.vaultResource.getApiClient().getBasePath());
127127
}
128128
// region as chosen by user upon vault creation (STS) or as retrieved from bucket (permanent)
129129
bookmark.setRegion(vaultMetadata.getRegion());
130130
return bookmark;
131131
}
132+
133+
private static final class HubAwareProfile extends Profile {
134+
private final HubSession hub;
135+
136+
public HubAwareProfile(final HubSession hub, final Protocol parent, final ConfigDto configDto, final StorageProfileDtoWrapper storageProfile) {
137+
super(parent, new StorageProfileDtoWrapperDeserializer(
138+
new HubConfigDtoDeserializer(configDto), storageProfile));
139+
this.hub = hub;
140+
}
141+
142+
@SuppressWarnings("unchecked")
143+
@Override
144+
public <T> T getFeature(final Class<T> type) {
145+
if(type == HubSession.class) {
146+
return (T) hub;
147+
}
148+
return super.getFeature(type);
149+
}
150+
}
132151
}

0 commit comments

Comments
 (0)