diff --git a/core/edr-core/src/main/java/org/eclipse/tractusx/edc/edr/core/manager/EdrManagerImpl.java b/core/edr-core/src/main/java/org/eclipse/tractusx/edc/edr/core/manager/EdrManagerImpl.java index db89aaefe..091f4e67f 100644 --- a/core/edr-core/src/main/java/org/eclipse/tractusx/edc/edr/core/manager/EdrManagerImpl.java +++ b/core/edr-core/src/main/java/org/eclipse/tractusx/edc/edr/core/manager/EdrManagerImpl.java @@ -183,7 +183,6 @@ private ContractRequest createContractRequest(NegotiateEdrRequest request) { .counterPartyAddress(request.getConnectorAddress()) .contractOffer(request.getOffer()) .protocol(request.getProtocol()) - .providerId(request.getConnectorId()) .callbackAddresses(callbacks).build(); } diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceExtension.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceExtension.java index 8f2355b3d..553ea6665 100644 --- a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceExtension.java +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceExtension.java @@ -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; @@ -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 @@ -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; } diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImpl.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImpl.java index 005b2bed3..11769cd62 100644 --- a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImpl.java +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImpl.java @@ -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; @@ -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; @@ -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 privateKeySupplier, Monitor monitor, - String refreshEndpoint, int tokenExpiryToleranceSeconds) { + String refreshEndpoint, + int tokenExpiryToleranceSeconds, + Vault vault, + ObjectMapper objectMapper) { this.tokenValidationService = tokenValidationService; this.publicKeyResolver = publicKeyResolver; this.accessTokenDataStore = accessTokenDataStore; @@ -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), @@ -121,7 +132,8 @@ public DataPlaneTokenRefreshServiceImpl(Clock clock, TokenValidationService toke public Result 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(); @@ -144,17 +156,11 @@ public Result 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() ? @@ -180,26 +186,33 @@ public Result 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 @@ -254,10 +267,19 @@ private Result resolveToken(String token, List 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) { } + } diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/RefreshToken.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/RefreshToken.java new file mode 100644 index 000000000..8e5291e9e --- /dev/null +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/RefreshToken.java @@ -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) { + +} diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/AuthTokenAudienceRule.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/AuthTokenAudienceRule.java new file mode 100644 index 000000000..3fce42497 --- /dev/null +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/AuthTokenAudienceRule.java @@ -0,0 +1,57 @@ +/* + * 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; + +/** + * Validates that the {@code iss} claim of a token is equal to the {@code audience} property found on the {@link org.eclipse.edc.connector.dataplane.spi.AccessTokenData} + * that is associated with that token (using the {@code jti} claim). + */ +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 checkRule(@NotNull ClaimToken claimToken, @Nullable Map 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())); + } +} diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/RefreshTokenMustExistRule.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/RefreshTokenMustExistRule.java deleted file mode 100644 index ccf42c31d..000000000 --- a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/RefreshTokenMustExistRule.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.AccessTokenData; -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; -import java.util.Optional; - -import static org.eclipse.tractusx.edc.dataplane.tokenrefresh.core.DataPlaneTokenRefreshServiceImpl.ACCESS_TOKEN_CLAIM; -import static org.eclipse.tractusx.edc.dataplane.tokenrefresh.core.DataPlaneTokenRefreshServiceImpl.REFRESH_TOKEN_PROPERTY; - -public class RefreshTokenMustExistRule implements TokenValidationRule { - private static final String AUDIENCE_PROPERTY = "audience"; - private final AccessTokenDataStore accessTokenDataStore; - private final String refreshToken; - - public RefreshTokenMustExistRule(AccessTokenDataStore accessTokenDataStore, String refreshToken) { - this.accessTokenDataStore = accessTokenDataStore; - this.refreshToken = refreshToken; - } - - @Override - public Result checkRule(@NotNull ClaimToken toVerify, @Nullable Map additional) { - var oldAccessToken = toVerify.getStringClaim(ACCESS_TOKEN_CLAIM); - var tokenId = toVerify.getStringClaim(JWTClaimNames.JWT_ID); - var issuer = toVerify.getStringClaim(JWTClaimNames.ISSUER); - return Optional.ofNullable(accessTokenDataStore.getById(tokenId)) - .map(accessTokenData -> checkExists(accessTokenData, refreshToken, issuer)) - .orElse(Result.failure("No AccessTokenData entry found for token-ID '%s'.".formatted(tokenId))); - } - - private Result checkExists(AccessTokenData accessTokenData, String refreshToken, String issuer) { - var storedRefreshToken = accessTokenData.additionalProperties().getOrDefault(REFRESH_TOKEN_PROPERTY, null); - if (!(storedRefreshToken instanceof String)) { - return Result.failure("Property '%s' expected to be String but was %s".formatted(REFRESH_TOKEN_PROPERTY, storedRefreshToken == null ? "null" : storedRefreshToken.getClass())); - } - if (!refreshToken.equals(storedRefreshToken)) { - return Result.failure("Provided refresh token does not match the stored refresh token."); - } - var audience = accessTokenData.additionalProperties().getOrDefault(AUDIENCE_PROPERTY, null); - if (!(audience instanceof String)) { - return Result.failure("Property '%s' expected to be String but was %s".formatted(AUDIENCE_PROPERTY, audience == null ? "null" : audience.getClass())); - } - - if (!audience.equals(issuer)) { - return Result.failure("Principal '%s' is not authorized to refresh this token.".formatted(issuer)); - } - - return Result.success(); - } -} diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/RefreshTokenValidationRule.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/RefreshTokenValidationRule.java new file mode 100644 index 000000000..b6eb4a1bb --- /dev/null +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/RefreshTokenValidationRule.java @@ -0,0 +1,75 @@ +/* + * 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.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jwt.JWTClaimNames; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.security.Vault; +import org.eclipse.edc.token.spi.TokenValidationRule; +import org.eclipse.tractusx.edc.dataplane.tokenrefresh.core.RefreshToken; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +import static org.eclipse.edc.spi.result.Result.failure; +import static org.eclipse.edc.spi.result.Result.success; + +/** + * Validates that the refresh token information associated with a token's ID ({@code jti}), that is stored in the {@link Vault} + * matches a refresh token string. The refresh token in question is passed into the CTor. + */ +public class RefreshTokenValidationRule implements TokenValidationRule { + private final Vault vault; + private final String incomingRefreshToken; + private final ObjectMapper objectMapper; + + public RefreshTokenValidationRule(Vault vault, String incomingRefreshToken, ObjectMapper objectMapper) { + this.vault = vault; + this.incomingRefreshToken = incomingRefreshToken; + this.objectMapper = objectMapper; + } + + @Override + public Result checkRule(@NotNull ClaimToken toVerify, @Nullable Map additional) { + var tokenId = toVerify.getStringClaim(JWTClaimNames.JWT_ID); + + var storedRefreshTokenJson = vault.resolveSecret(tokenId); + if (storedRefreshTokenJson == null) { + return failure("No refresh token with the ID '%s' was found in the vault.".formatted(tokenId)); + } + return parse(storedRefreshTokenJson) + .compose(rt -> incomingRefreshToken.equals(rt.refreshToken()) ? + success() : + failure("Provided refresh token does not match the stored refresh token.")); + } + + private Result parse(String storedRefreshTokenJson) { + try { + return success(objectMapper.readValue(storedRefreshTokenJson, RefreshToken.class)); + } catch (JsonProcessingException e) { + return failure("Failed to parse stored secret: " + e.getMessage()); + } + } + +} diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImplComponentTest.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImplComponentTest.java index a8d5b72e9..d8261e852 100644 --- a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImplComponentTest.java +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImplComponentTest.java @@ -19,6 +19,7 @@ package org.eclipse.tractusx.edc.dataplane.tokenrefresh.core; +import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; @@ -29,6 +30,7 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import org.eclipse.edc.connector.core.store.CriterionOperatorRegistryImpl; +import org.eclipse.edc.connector.core.vault.InMemoryVault; import org.eclipse.edc.connector.dataplane.framework.store.InMemoryAccessTokenDataStore; import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver; import org.eclipse.edc.junit.annotations.ComponentTest; @@ -37,7 +39,6 @@ import org.eclipse.edc.spi.iam.TokenParameters; import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.spi.types.domain.DataAddress; -import org.eclipse.edc.spi.types.domain.transfer.FlowType; import org.eclipse.edc.token.JwtGenerationService; import org.eclipse.edc.token.TokenValidationServiceImpl; import org.junit.jupiter.api.BeforeEach; @@ -51,10 +52,6 @@ import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.edc.connector.dataplane.framework.iam.DataPlaneAuthorizationServiceImpl.CLAIM_AGREEMENT_ID; -import static org.eclipse.edc.connector.dataplane.framework.iam.DataPlaneAuthorizationServiceImpl.CLAIM_ASSET_ID; -import static org.eclipse.edc.connector.dataplane.framework.iam.DataPlaneAuthorizationServiceImpl.CLAIM_FLOW_TYPE; -import static org.eclipse.edc.connector.dataplane.framework.iam.DataPlaneAuthorizationServiceImpl.CLAIM_PROCESS_ID; import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -91,7 +88,9 @@ void setup() throws JOSEException { () -> privateKey, mock(), TEST_REFRESH_ENDPOINT, - 1); + 1, + new InMemoryVault(mock()), + new ObjectMapper()); when(didPkResolverMock.resolveKey(eq(consumerKey.getKeyID()))).thenReturn(Result.success(consumerKey.toPublicKey())); when(didPkResolverMock.resolveKey(eq(providerKey.getKeyID()))).thenReturn(Result.success(providerKey.toPublicKey())); @@ -105,9 +104,6 @@ void obtainToken() { assertThat(edr).isSucceeded(); // assert access token contents assertThat(asClaims(edr.getContent().getToken())) - .containsKey("asset_id") - .containsKey("process_id") - .containsKey("agreement_id") .containsEntry("iss", PROVIDER_BPN) .containsEntry("sub", PROVIDER_BPN) .containsEntry("aud", List.of(CONSUMER_BPN)) @@ -123,10 +119,8 @@ void obtainToken() { var storedData = tokenDataStore.getById(tokenId); assertThat(storedData).isNotNull(); assertThat(storedData.additionalProperties()) + .hasSize(2) .containsEntry("audience", CONSUMER_DID) - .containsEntry("refreshEndpoint", TEST_REFRESH_ENDPOINT) - .containsKey("refreshToken") - .containsKey("expiresIn") .containsEntry("authType", "bearer"); } @@ -150,6 +144,10 @@ void refresh_success() throws JOSEException { assertThat(tokenResponse).withFailMessage(tokenResponse::getFailureDetail).isSucceeded() .satisfies(tr -> assertThat(tr.refreshToken()).isNotNull()) .satisfies(tr -> assertThat(tr.accessToken()).isNotNull()); + + assertThat(tokenDataStore.getById(tokenId).additionalProperties()) + .hasSize(2) + .doesNotContainKey("refreshToken"); } @DisplayName("Verify that a refresh token can only be refreshed by the original recipient") @@ -307,10 +305,6 @@ private TokenParameters.Builder tokenParamsBuilder(String id) { .claims(JwtRegisteredClaimNames.SUBJECT, PROVIDER_BPN) .claims(JwtRegisteredClaimNames.EXPIRATION_TIME, Instant.now().plusSeconds(60).getEpochSecond()) .claims(JwtRegisteredClaimNames.ISSUED_AT, Instant.now().getEpochSecond()) - .claims(CLAIM_AGREEMENT_ID, "test-agreement-id") - .claims(CLAIM_ASSET_ID, "test-asset-id") - .claims(CLAIM_PROCESS_ID, "test-process-id") - .claims(CLAIM_FLOW_TYPE, FlowType.PULL.toString()) .header("kid", providerKey.getKeyID()); } diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImplTest.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImplTest.java index f57d674ea..1d3b58e2f 100644 --- a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImplTest.java +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImplTest.java @@ -19,6 +19,7 @@ package org.eclipse.tractusx.edc.dataplane.tokenrefresh.core; +import com.fasterxml.jackson.databind.ObjectMapper; import org.eclipse.edc.connector.dataplane.spi.AccessTokenData; import org.eclipse.edc.connector.dataplane.spi.store.AccessTokenDataStore; import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver; @@ -59,7 +60,9 @@ class DataPlaneTokenRefreshServiceImplTest { private final TokenValidationService tokenValidationService = mock(); private final DidPublicKeyResolver didPublicKeyResolver = mock(); - private final DataPlaneTokenRefreshServiceImpl accessTokenService = new DataPlaneTokenRefreshServiceImpl(Clock.systemUTC(), tokenValidationService, didPublicKeyResolver, accessTokenDataStore, tokenGenService, mock(), mock(), "https://example.com", 1); + private final DataPlaneTokenRefreshServiceImpl accessTokenService = new DataPlaneTokenRefreshServiceImpl(Clock.systemUTC(), tokenValidationService, didPublicKeyResolver, accessTokenDataStore, tokenGenService, mock(), mock(), "https://example.com", 1, + mock(), new ObjectMapper()); + @Test void obtainToken() { diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/TestFunctions.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/TestFunctions.java new file mode 100644 index 000000000..cba1980cb --- /dev/null +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/TestFunctions.java @@ -0,0 +1,34 @@ +/* + * + * 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; + +import org.eclipse.edc.spi.iam.ClaimToken; + +public class TestFunctions { + public static ClaimToken createToken(String id) { + return ClaimToken.Builder.newInstance() + .claim("access_token", "test-access-token") + .claim("jti", id) + .claim("iss", "did:web:bob") + .build(); + } +} diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/AuthTokenAudienceRuleTest.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/AuthTokenAudienceRuleTest.java new file mode 100644 index 000000000..635e0ee71 --- /dev/null +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/AuthTokenAudienceRuleTest.java @@ -0,0 +1,80 @@ +/* + * 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 org.eclipse.edc.connector.dataplane.spi.AccessTokenData; +import org.eclipse.edc.connector.dataplane.spi.store.AccessTokenDataStore; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.eclipse.tractusx.edc.dataplane.tokenrefresh.core.TestFunctions.createToken; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AuthTokenAudienceRuleTest { + + private static final String TEST_TOKEN_ID = "token-id"; + private static final Object TEST_REFRESH_TOKEN = "refresh-token"; + private final AccessTokenDataStore store = mock(); + private final AuthTokenAudienceRule rule = new AuthTokenAudienceRule(store); + + @Test + void checkRule_issuerDoesNotMatchAudience() { + when(store.getById(TEST_TOKEN_ID)).thenReturn(new AccessTokenData(TEST_TOKEN_ID, + ClaimToken.Builder.newInstance().build(), + DataAddress.Builder.newInstance().type("test-type").build(), + Map.of("audience", "did:web:alice"))); + + assertThat(rule.checkRule(createToken(TEST_TOKEN_ID), Map.of())) + .isFailed() + .detail() + .isEqualTo("Principal 'did:web:bob' is not authorized to refresh this token."); + } + + @Test + void checkRule_audienceNotString() { + when(store.getById(TEST_TOKEN_ID)).thenReturn(new AccessTokenData(TEST_TOKEN_ID, + ClaimToken.Builder.newInstance().build(), + DataAddress.Builder.newInstance().type("test-type").build(), + Map.of("audience", 42L))); + + assertThat(rule.checkRule(createToken(TEST_TOKEN_ID), Map.of())) + .isFailed() + .detail() + .isEqualTo("Property 'audience' was expected to be java.lang.String but was class java.lang.Long."); + } + + @Test + void checkRule_audienceNotPresent() { + when(store.getById(TEST_TOKEN_ID)).thenReturn(new AccessTokenData(TEST_TOKEN_ID, + ClaimToken.Builder.newInstance().build(), + DataAddress.Builder.newInstance().type("test-type").build(), + Map.of())); + + assertThat(rule.checkRule(createToken(TEST_TOKEN_ID), Map.of())) + .isFailed() + .detail() + .isEqualTo("Property 'audience' was expected to be java.lang.String but was null."); + } +} \ No newline at end of file diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/RefreshTokenMustExistRuleTest.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/RefreshTokenMustExistRuleTest.java deleted file mode 100644 index e3e50d04e..000000000 --- a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/RefreshTokenMustExistRuleTest.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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 org.eclipse.edc.connector.dataplane.spi.AccessTokenData; -import org.eclipse.edc.connector.dataplane.spi.store.AccessTokenDataStore; -import org.eclipse.edc.spi.iam.ClaimToken; -import org.eclipse.edc.spi.types.domain.DataAddress; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class RefreshTokenMustExistRuleTest { - - private static final String TEST_TOKEN_ID = "test-jti"; - private static final String TEST_REFRESH_TOKEN = "test-refresh-token"; - private final AccessTokenDataStore accessTokenData = mock(); - private final RefreshTokenMustExistRule rule = new RefreshTokenMustExistRule(accessTokenData, TEST_REFRESH_TOKEN); - - @Test - void checkRule_noAccessTokenDataEntryFound() { - when(accessTokenData.getById(TEST_TOKEN_ID)).thenReturn(null); - - assertThat(rule.checkRule(createToken(), Map.of())) - .isFailed() - .detail() - .isEqualTo("No AccessTokenData entry found for token-ID '%s'.".formatted(TEST_TOKEN_ID)); - } - - @Test - void checkRule_noRefreshTokenStored() { - when(accessTokenData.getById(TEST_TOKEN_ID)).thenReturn(new AccessTokenData(TEST_TOKEN_ID, - ClaimToken.Builder.newInstance().build(), - DataAddress.Builder.newInstance().type("test-type").build(), - Map.of("foo", "var"))); - - assertThat(rule.checkRule(createToken(), Map.of())) - .isFailed() - .detail() - .isEqualTo("Property 'refreshToken' expected to be String but was null"); - } - - @Test - void checkRule_refreshTokenNotString() { - when(accessTokenData.getById(TEST_TOKEN_ID)).thenReturn(new AccessTokenData(TEST_TOKEN_ID, - ClaimToken.Builder.newInstance().build(), - DataAddress.Builder.newInstance().type("test-type").build(), - Map.of("refreshToken", 42L))); - - assertThat(rule.checkRule(createToken(), Map.of())) - .isFailed() - .detail() - .isEqualTo("Property 'refreshToken' expected to be String but was class java.lang.Long"); - } - - @Test - void checkRule_refreshTokenDoesNotMatch() { - when(accessTokenData.getById(TEST_TOKEN_ID)).thenReturn(new AccessTokenData(TEST_TOKEN_ID, - ClaimToken.Builder.newInstance().build(), - DataAddress.Builder.newInstance().type("test-type").build(), - Map.of("refreshToken", "this-is-not-equal"))); - - assertThat(rule.checkRule(createToken(), Map.of())) - .isFailed() - .detail() - .isEqualTo("Provided refresh token does not match the stored refresh token."); - } - - @Test - void checkRule_issuerDoesNotMatchAudience() { - when(accessTokenData.getById(TEST_TOKEN_ID)).thenReturn(new AccessTokenData(TEST_TOKEN_ID, - ClaimToken.Builder.newInstance().build(), - DataAddress.Builder.newInstance().type("test-type").build(), - Map.of("refreshToken", TEST_REFRESH_TOKEN, "audience", "did:web:alice"))); - - assertThat(rule.checkRule(createToken(), Map.of())) - .isFailed() - .detail() - .isEqualTo("Principal 'did:web:bob' is not authorized to refresh this token."); - } - - @Test - void checkRule_success() { - when(accessTokenData.getById(TEST_TOKEN_ID)).thenReturn(new AccessTokenData(TEST_TOKEN_ID, - ClaimToken.Builder.newInstance().build(), - DataAddress.Builder.newInstance().type("test-type").build(), - Map.of("refreshToken", TEST_REFRESH_TOKEN, "audience", "did:web:bob"))); - - assertThat(rule.checkRule(createToken(), Map.of())) - .isSucceeded(); - } - - private ClaimToken createToken() { - return ClaimToken.Builder.newInstance() - .claim("access_token", "test-access-token") - .claim("jti", TEST_TOKEN_ID) - .claim("iss", "did:web:bob") - .build(); - } -} \ No newline at end of file diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/RefreshTokenValidationRuleTest.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/RefreshTokenValidationRuleTest.java new file mode 100644 index 000000000..60022ac35 --- /dev/null +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/rules/RefreshTokenValidationRuleTest.java @@ -0,0 +1,114 @@ +/* + * 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.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.edc.spi.security.Vault; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.eclipse.tractusx.edc.dataplane.tokenrefresh.core.TestFunctions.createToken; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class RefreshTokenValidationRuleTest { + + private static final String TEST_TOKEN_ID = "test-jti"; + private static final String TEST_REFRESH_TOKEN = "test-refresh-token"; + private final Vault vault = mock(); + private final RefreshTokenValidationRule rule = new RefreshTokenValidationRule(vault, TEST_REFRESH_TOKEN, new ObjectMapper()); + + @Test + void checkRule_noAccessTokenDataEntryFound() { + when(vault.resolveSecret(TEST_TOKEN_ID)).thenReturn(null); + + assertThat(rule.checkRule(createToken(TEST_TOKEN_ID), Map.of())) + .isFailed() + .detail() + .isEqualTo("No refresh token with the ID '%s' was found in the vault.".formatted(TEST_TOKEN_ID)); + } + + @Test + void checkRule_noRefreshTokenStored() { + when(vault.resolveSecret(TEST_TOKEN_ID)).thenReturn(null); + + assertThat(rule.checkRule(createToken(TEST_TOKEN_ID), Map.of())) + .isFailed() + .detail() + .isEqualTo("No refresh token with the ID 'test-jti' was found in the vault."); + } + + @Test + void checkRule_refreshTokenNotString() { + when(vault.resolveSecret(TEST_TOKEN_ID)).thenReturn( + """ + { + "refreshToken": 42 + } + """); + + assertThat(rule.checkRule(createToken(TEST_TOKEN_ID), Map.of())) + .isFailed() + .detail() + .isEqualTo("Provided refresh token does not match the stored refresh token."); + } + + @Test + void checkRule_refreshTokenDoesNotMatch() { + when(vault.resolveSecret(TEST_TOKEN_ID)).thenReturn( + """ + { + "refreshToken": "someRefreshToken" + } + """); + + assertThat(rule.checkRule(createToken(TEST_TOKEN_ID), Map.of())) + .isFailed() + .detail() + .isEqualTo("Provided refresh token does not match the stored refresh token."); + } + + @Test + void checkRule_success() { + when(vault.resolveSecret(TEST_TOKEN_ID)).thenReturn( + """ + { + "refreshToken": "%s" + } + """.formatted(TEST_REFRESH_TOKEN)); + + assertThat(rule.checkRule(createToken(TEST_TOKEN_ID), Map.of())) + .isSucceeded(); + } + + @Test + void checkRule_invalidJson() { + when(vault.resolveSecret(TEST_TOKEN_ID)).thenReturn( + "nope-thats-not-json"); + + assertThat(rule.checkRule(createToken(TEST_TOKEN_ID), Map.of())) + .isFailed() + .detail() + .startsWith("Failed to parse stored secret"); + } + +} \ No newline at end of file