Skip to content

Commit

Permalink
feat: store refresh token in vault
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Mar 14, 2024
1 parent bd6f22d commit 1f3aa4c
Show file tree
Hide file tree
Showing 13 changed files with 450 additions and 242 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ private ContractRequest createContractRequest(NegotiateEdrRequest request) {
.counterPartyAddress(request.getConnectorAddress())
.contractOffer(request.getOffer())
.protocol(request.getProtocol())
.providerId(request.getConnectorId())
.callbackAddresses(callbacks).build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@
import org.eclipse.edc.runtime.metamodel.annotation.Provider;
import org.eclipse.edc.runtime.metamodel.annotation.Setting;
import org.eclipse.edc.spi.security.PrivateKeyResolver;
import org.eclipse.edc.spi.security.Vault;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.types.TypeManager;
import org.eclipse.edc.token.JwtGenerationService;
import org.eclipse.edc.token.spi.TokenValidationService;
import org.eclipse.tractusx.edc.dataplane.tokenrefresh.spi.DataPlaneTokenRefreshService;
Expand Down Expand Up @@ -57,6 +59,11 @@ public class DataPlaneTokenRefreshServiceExtension implements ServiceExtension {
private PrivateKeyResolver privateKeyResolver;
@Inject
private Clock clock;
@Inject
private Vault vault;
@Inject
private TypeManager typeManager;

private DataPlaneTokenRefreshServiceImpl tokenRefreshService;

@Override
Expand All @@ -80,7 +87,8 @@ public DataPlaneTokenRefreshService createRefreshTokenService(ServiceExtensionCo
private DataPlaneTokenRefreshServiceImpl getTokenRefreshService(ServiceExtensionContext context) {
if (tokenRefreshService == null) {
var epsilon = context.getConfig().getInteger(TOKEN_EXPIRY_TOLERANCE_SECONDS_PROPERTY, DEFAULT_TOKEN_EXPIRY_TOLERANCE_SECONDS);
tokenRefreshService = new DataPlaneTokenRefreshServiceImpl(clock, tokenValidationService, didPkResolver, accessTokenDataStore, new JwtGenerationService(), getPrivateKeySupplier(context), context.getMonitor(), null, epsilon);
tokenRefreshService = new DataPlaneTokenRefreshServiceImpl(clock, tokenValidationService, didPkResolver, accessTokenDataStore, new JwtGenerationService(), getPrivateKeySupplier(context), context.getMonitor(), null,
epsilon, vault, typeManager.getMapper());
}
return tokenRefreshService;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

package org.eclipse.tractusx.edc.dataplane.tokenrefresh.core;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.edc.connector.dataplane.spi.AccessTokenData;
import org.eclipse.edc.connector.dataplane.spi.iam.DataPlaneAccessTokenService;
import org.eclipse.edc.connector.dataplane.spi.store.AccessTokenDataStore;
Expand All @@ -29,15 +31,17 @@
import org.eclipse.edc.spi.iam.TokenRepresentation;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.security.Vault;
import org.eclipse.edc.spi.types.domain.DataAddress;
import org.eclipse.edc.token.rules.ExpirationIssuedAtValidationRule;
import org.eclipse.edc.token.spi.TokenDecorator;
import org.eclipse.edc.token.spi.TokenGenerationService;
import org.eclipse.edc.token.spi.TokenValidationRule;
import org.eclipse.edc.token.spi.TokenValidationService;
import org.eclipse.tractusx.edc.dataplane.tokenrefresh.core.rules.AuthTokenAudienceRule;
import org.eclipse.tractusx.edc.dataplane.tokenrefresh.core.rules.ClaimIsPresentRule;
import org.eclipse.tractusx.edc.dataplane.tokenrefresh.core.rules.IssuerEqualsSubjectRule;
import org.eclipse.tractusx.edc.dataplane.tokenrefresh.core.rules.RefreshTokenMustExistRule;
import org.eclipse.tractusx.edc.dataplane.tokenrefresh.core.rules.RefreshTokenValidationRule;
import org.eclipse.tractusx.edc.dataplane.tokenrefresh.spi.DataPlaneTokenRefreshService;
import org.eclipse.tractusx.edc.dataplane.tokenrefresh.spi.model.TokenResponse;

Expand Down Expand Up @@ -74,15 +78,20 @@ public class DataPlaneTokenRefreshServiceImpl implements DataPlaneTokenRefreshSe
private final Monitor monitor;
private final String refreshEndpoint;
private final Clock clock;
private final Vault vault;
private final ObjectMapper objectMapper;


public DataPlaneTokenRefreshServiceImpl(Clock clock, TokenValidationService tokenValidationService,
public DataPlaneTokenRefreshServiceImpl(Clock clock,
TokenValidationService tokenValidationService,
DidPublicKeyResolver publicKeyResolver,
AccessTokenDataStore accessTokenDataStore,
TokenGenerationService tokenGenerationService,
Supplier<PrivateKey> privateKeySupplier,
Monitor monitor,
String refreshEndpoint, int tokenExpiryToleranceSeconds) {
String refreshEndpoint,
int tokenExpiryToleranceSeconds,
Vault vault,
ObjectMapper objectMapper) {
this.tokenValidationService = tokenValidationService;
this.publicKeyResolver = publicKeyResolver;
this.accessTokenDataStore = accessTokenDataStore;
Expand All @@ -91,6 +100,8 @@ public DataPlaneTokenRefreshServiceImpl(Clock clock, TokenValidationService toke
this.monitor = monitor;
this.refreshEndpoint = refreshEndpoint;
this.clock = clock;
this.vault = vault;
this.objectMapper = objectMapper;
authenticationTokenValidationRules = List.of(new IssuerEqualsSubjectRule(),
new ClaimIsPresentRule(AUDIENCE), // we don't check the contents, only it is present
new ClaimIsPresentRule(ACCESS_TOKEN_CLAIM),
Expand Down Expand Up @@ -121,7 +132,8 @@ public DataPlaneTokenRefreshServiceImpl(Clock clock, TokenValidationService toke
public Result<TokenResponse> refreshToken(String refreshToken, String authenticationToken) {

var allRules = new ArrayList<>(authenticationTokenValidationRules);
allRules.add(new RefreshTokenMustExistRule(accessTokenDataStore, refreshToken));
allRules.add(new RefreshTokenValidationRule(vault, refreshToken, objectMapper));
allRules.add(new AuthTokenAudienceRule(accessTokenDataStore));

authenticationToken = authenticationToken.replace("Bearer", "").trim();

Expand All @@ -144,17 +156,11 @@ public Result<TokenResponse> refreshToken(String refreshToken, String authentica
return Result.failure("Failed to regenerate access/refresh token pair: %s".formatted(errors));
}

// additional token information will be added to the TokenRepresentation, which will be returned to the caller
// note: can't use DBI (double-bracket initialization) here, because SonarCloud will complain about it
var accessTokenAdditional = new HashMap<>(existingAccessTokenData.additionalProperties());
accessTokenAdditional.put(REFRESH_TOKEN_PROPERTY, newRefreshToken.getContent());
accessTokenAdditional.put("expiresIn", DEFAULT_EXPIRY_IN_SECONDS);
accessTokenAdditional.put("refreshEndpoint", refreshEndpoint);
accessTokenAdditional.put("authType", "bearer");
storeRefreshToken(existingAccessTokenData.id(), new RefreshToken(newRefreshToken.getContent(), DEFAULT_EXPIRY_IN_SECONDS, refreshEndpoint));

// the ClaimToken is created based solely on the TokenParameters. The additional information (refresh token...) is persisted separately
var claimToken = ClaimToken.Builder.newInstance().claims(newTokenParams.getClaims()).build();
var accessTokenData = new AccessTokenData(existingAccessTokenData.id(), claimToken, existingAccessTokenData.dataAddress(), accessTokenAdditional);
var accessTokenData = new AccessTokenData(existingAccessTokenData.id(), claimToken, existingAccessTokenData.dataAddress(), existingAccessTokenData.additionalProperties());

var storeResult = accessTokenDataStore.update(accessTokenData);
return storeResult.succeeded() ?
Expand All @@ -180,26 +186,33 @@ public Result<TokenRepresentation> obtainToken(TokenParameters tokenParameters,
return Result.failure("Could not generate access token: %s".formatted(accessTokenResult.getFailureDetail()));
}

// additional token information will be added to the TokenRepresentation, which will be returned to the caller
// the edrAdditionalData contains the refresh token, which is NOT supposed to be put in the DB
// note: can't use DBI (double-bracket initialization) here, because SonarCloud will complain about it
var accessTokenAdditional = new HashMap<>(additionalTokenData);
accessTokenAdditional.put("refreshToken", refreshTokenResult.getContent().tokenRepresentation().getToken());
accessTokenAdditional.put("expiresIn", DEFAULT_EXPIRY_IN_SECONDS);
accessTokenAdditional.put("refreshEndpoint", refreshEndpoint);
accessTokenAdditional.put("authType", "bearer");
var additionalDataForStorage = new HashMap<>(additionalTokenData);
additionalDataForStorage.put("authType", "bearer");

// the ClaimToken is created based solely on the TokenParameters. The additional information (refresh token...) is persisted separately
var claimToken = ClaimToken.Builder.newInstance().claims(tokenParameters.getClaims()).build();
var accessTokenData = new AccessTokenData(accessTokenResult.getContent().id(), claimToken, backendDataAddress, additionalDataForStorage);
var storeResult = accessTokenDataStore.store(accessTokenData);

storeRefreshToken(accessTokenResult.getContent().id(), new RefreshToken(refreshTokenResult.getContent().tokenRepresentation().getToken(),
DEFAULT_EXPIRY_IN_SECONDS, refreshEndpoint));

// the refresh token information must be returned in the EDR
var edrAdditionalData = new HashMap<>(additionalTokenData);
edrAdditionalData.put("refreshToken", refreshTokenResult.getContent().tokenRepresentation().getToken());
edrAdditionalData.put("expiresIn", DEFAULT_EXPIRY_IN_SECONDS);
edrAdditionalData.put("refreshEndpoint", refreshEndpoint);

var accessToken = TokenRepresentation.Builder.newInstance()
var edrTokenRepresentation = TokenRepresentation.Builder.newInstance()
.token(accessTokenResult.getContent().tokenRepresentation().getToken()) // the access token
.additional(accessTokenAdditional) //contains additional properties and the refresh token
.additional(edrAdditionalData) //contains additional properties and the refresh token
.expiresIn(DEFAULT_EXPIRY_IN_SECONDS) //todo: needed?
.build();

// the ClaimToken is created based solely on the TokenParameters. The additional information (refresh token...) is persisted separately
var claimToken = ClaimToken.Builder.newInstance().claims(tokenParameters.getClaims()).build();
var accessTokenData = new AccessTokenData(accessTokenResult.getContent().id(), claimToken, backendDataAddress, accessTokenAdditional);

var storeResult = accessTokenDataStore.store(accessTokenData);
return storeResult.succeeded() ? Result.success(accessToken) : Result.failure(storeResult.getFailureMessages());
return storeResult.succeeded() ? Result.success(edrTokenRepresentation) : Result.failure(storeResult.getFailureMessages());
}

@Override
Expand Down Expand Up @@ -254,10 +267,19 @@ private Result<AccessTokenData> resolveToken(String token, List<TokenValidationR
Result.success(existingAccessToken);
}

private Result<Void> storeRefreshToken(String id, RefreshToken refreshToken) {
try {
return vault.storeSecret(id, objectMapper.writeValueAsString(refreshToken));
} catch (JsonProcessingException e) {
return Result.failure(e.getMessage());
}
}

/**
* container object for a TokenRepresentation and an ID
*/
private record TokenRepresentationWithId(String id, TokenRepresentation tokenRepresentation) {

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
*/

package org.eclipse.tractusx.edc.dataplane.tokenrefresh.core;

public record RefreshToken(String refreshToken, Long expiresIn, String refreshEndpoint) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

package org.eclipse.tractusx.edc.dataplane.tokenrefresh.core.rules;

import com.nimbusds.jwt.JWTClaimNames;
import org.eclipse.edc.connector.dataplane.spi.store.AccessTokenDataStore;
import org.eclipse.edc.spi.iam.ClaimToken;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.token.spi.TokenValidationRule;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Map;

public class AuthTokenAudienceRule implements TokenValidationRule {
private static final String AUDIENCE_PROPERTY = "audience";
private final AccessTokenDataStore store;

public AuthTokenAudienceRule(AccessTokenDataStore store) {
this.store = store;
}

@Override
public Result<Void> checkRule(@NotNull ClaimToken claimToken, @Nullable Map<String, Object> map) {
var issuer = claimToken.getStringClaim(JWTClaimNames.ISSUER);
var tokenId = claimToken.getStringClaim(JWTClaimNames.JWT_ID);

var accessTokenData = store.getById(tokenId);
var expectedAudience = accessTokenData.additionalProperties().getOrDefault(AUDIENCE_PROPERTY, null);
if (expectedAudience instanceof String expectedAud) {
return expectedAud.equals(issuer) ? Result.success() : Result.failure("Principal '%s' is not authorized to refresh this token.".formatted(issuer));
}

return Result.failure("Property '%s' was expected to be java.lang.String but was %s.".formatted(AUDIENCE_PROPERTY, expectedAudience == null ? null : expectedAudience.getClass()));
}
}

This file was deleted.

Loading

0 comments on commit 1f3aa4c

Please sign in to comment.