Skip to content

Commit

Permalink
add more e2e
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Mar 11, 2024
1 parent 25fab99 commit 7db594d
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
import java.util.function.Supplier;
import java.util.stream.Stream;

import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.AUDIENCE;

/**
* This implementation of the {@link DataPlaneTokenRefreshService} validates an incoming authentication token.
*/
Expand All @@ -58,7 +60,10 @@ public class DataPlaneTokenRefreshServiceImpl implements DataPlaneTokenRefreshSe
public static final String TOKEN_ID_CLAIM = "jti";
public static final String REFRESH_TOKEN_PROPERTY = "refreshToken";
private static final Long DEFAULT_EXPIRY_IN_SECONDS = 60 * 5L;
private final List<TokenValidationRule> tokenValidationRules = List.of(new IssuerEqualsSubjectRule(), new ClaimIsPresentRule(ACCESS_TOKEN_CLAIM), new ClaimIsPresentRule(TOKEN_ID_CLAIM));
private final List<TokenValidationRule> authenticationTokenValidationRules = List.of(new IssuerEqualsSubjectRule(),
new ClaimIsPresentRule(AUDIENCE), // we don't check the contents, only it is present
new ClaimIsPresentRule(ACCESS_TOKEN_CLAIM),
new ClaimIsPresentRule(TOKEN_ID_CLAIM));
private final TokenValidationService tokenValidationService;
private final DidPublicKeyResolver publicKeyResolver;
private final AccessTokenDataStore accessTokenDataStore;
Expand All @@ -71,7 +76,10 @@ public class DataPlaneTokenRefreshServiceImpl implements DataPlaneTokenRefreshSe
public DataPlaneTokenRefreshServiceImpl(TokenValidationService tokenValidationService,
DidPublicKeyResolver publicKeyResolver,
AccessTokenDataStore accessTokenDataStore,
TokenGenerationService tokenGenerationService, Supplier<PrivateKey> privateKeySupplier, Monitor monitor, String refreshEndpoint) {
TokenGenerationService tokenGenerationService,
Supplier<PrivateKey> privateKeySupplier,
Monitor monitor,
String refreshEndpoint) {
this.tokenValidationService = tokenValidationService;
this.publicKeyResolver = publicKeyResolver;
this.accessTokenDataStore = accessTokenDataStore;
Expand Down Expand Up @@ -100,7 +108,7 @@ public DataPlaneTokenRefreshServiceImpl(TokenValidationService tokenValidationSe
@Override
public Result<TokenResponse> refreshToken(String refreshToken, String authenticationToken) {

var allRules = new ArrayList<>(tokenValidationRules);
var allRules = new ArrayList<>(authenticationTokenValidationRules);
allRules.add(new RefreshTokenMustExistRule(accessTokenDataStore, refreshToken));

authenticationToken = authenticationToken.replace("Bearer", "").trim();
Expand Down Expand Up @@ -184,7 +192,7 @@ public Result<TokenRepresentation> obtainToken(TokenParameters tokenParameters,

@Override
public Result<AccessTokenData> resolve(String token) {
return resolveToken(token, tokenValidationRules);
return resolveToken(token, authenticationTokenValidationRules);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,29 @@ void refresh_whenNoAccessTokenClaim() throws JOSEException {
.isEqualTo("Required claim 'access_token' not present on token.");
}

@DisplayName("Verify that the equality of the 'iss' and the 'sub' claim of the authentication token")
@Test
void refresh_whenIssNotEqualToSub() throws JOSEException {
var tokenId = "test-token-id";
var edr = tokenRefreshService.obtainToken(tokenParams(tokenId), DataAddress.Builder.newInstance().type("test-type").build(), Map.of("audience", CONSUMER_DID))
.orElseThrow(f -> new RuntimeException(f.getFailureDetail()));

var accessToken = edr.getToken();
var jwsHeader = new JWSHeader.Builder(JWSAlgorithm.ES384).keyID(consumerKey.getKeyID()).build();
var claimsSet = getAuthTokenClaims(tokenId, accessToken)
.issuer(CONSUMER_DID)
.subject("violating-subject")
.build();

var signedAuthToken = new SignedJWT(jwsHeader, claimsSet);
signedAuthToken.sign(CryptoConverter.createSigner(consumerKey));
var tokenResponse = tokenRefreshService.refreshToken(edr.getAdditional().get("refreshToken").toString(), signedAuthToken.serialize());

assertThat(tokenResponse).isFailed()
.detail()
.isEqualTo("The 'iss' and 'sub' claims must be non-null and identical.");
}

private JWTClaimsSet.Builder getAuthTokenClaims(String tokenId, String accessToken) {
return new JWTClaimsSet.Builder()
.jwtID(tokenId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.eclipse.edc.spi.types.domain.transfer.FlowType;
import org.eclipse.tractusx.edc.dataplane.tokenrefresh.spi.model.TokenResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

Expand Down Expand Up @@ -99,21 +100,16 @@ void setup() throws JOSEException {
@Test
void refresh_success() {

// register generator and keys
var generator = DATAPLANE_RUNTIME.getContext().getService(PublicEndpointGeneratorService.class);
generator.addGeneratorFunction("HttpData", dataAddress -> Endpoint.url(RUNTIME_CONFIG.getPublicApi().url().toString()));

var vault = DATAPLANE_RUNTIME.getContext().getService(Vault.class);
vault.storeSecret(PROVIDER_KEY_ID, providerKey.toJSONString());

// register generator and secrets
prepareDataplaneRuntime();

var authorizationService = DATAPLANE_RUNTIME.getService(DataPlaneAuthorizationService.class);
var edr = authorizationService.createEndpointDataReference(createStartMessage("test-process-id", CONSUMER_DID))
.orElseThrow(f -> new AssertionError(f.getFailureDetail()));

var refreshToken = edr.getStringProperty("refreshToken");
var accessToken = edr.getStringProperty("authorization");
var authToken = createAuthToken(accessToken);
var authToken = createAuthToken(accessToken, consumerKey);

var tokenResponse = RUNTIME_CONFIG.getRefreshApi().baseRequest()
.queryParam("grant_type", "refresh_token")
Expand All @@ -128,32 +124,143 @@ void refresh_success() {
assertThat(tokenResponse).isNotNull();
}

@DisplayName("The sign key of the authentication token is different from the public key from the DID")
@Test
void refresh_tokenInvalidFormat() {
// will be used once the RefreshAPI is here as well
void refresh_spoofedAuthToken() throws JOSEException {
prepareDataplaneRuntime();

var authorizationService = DATAPLANE_RUNTIME.getService(DataPlaneAuthorizationService.class);
var edr = authorizationService.createEndpointDataReference(createStartMessage("test-process-id", CONSUMER_DID))
.orElseThrow(f -> new AssertionError(f.getFailureDetail()));

var refreshToken = edr.getStringProperty("refreshToken");
var accessToken = edr.getStringProperty("authorization");
var spoofedKey = new ECKeyGenerator(Curve.P_256).keyID(CONSUMER_KEY_ID).generate();
var authTokenWithSpoofedKey = createAuthToken(accessToken, spoofedKey);

RUNTIME_CONFIG.getRefreshApi().baseRequest()
.queryParam("grant_type", "refresh_token")
.queryParam("refresh_token", "foobar")
.header(AUTHORIZATION, "Bearer foobar")
.queryParam("refresh_token", refreshToken)
.header(AUTHORIZATION, "Bearer " + authTokenWithSpoofedKey)
.post("/v1/token")
.then()
.log().ifValidationFails()
.statusCode(401)
.body(containsString("Failed to decode token"));
.body(containsString("Token verification failed"));
}

private String createAuthToken(String accessToken) {
var header = new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(consumerKey.getKeyID()).build();
@DisplayName("The refresh token does not match the stored one")
@Test
void refresh_withWrongRefreshToken() {
prepareDataplaneRuntime();

var authorizationService = DATAPLANE_RUNTIME.getService(DataPlaneAuthorizationService.class);
var edr = authorizationService.createEndpointDataReference(createStartMessage("test-process-id", CONSUMER_DID))
.orElseThrow(f -> new AssertionError(f.getFailureDetail()));

var refreshToken = "invalid_refresh_token";
var accessToken = edr.getStringProperty("authorization");

RUNTIME_CONFIG.getRefreshApi().baseRequest()
.queryParam("grant_type", "refresh_token")
.queryParam("refresh_token", refreshToken)
.header(AUTHORIZATION, "Bearer " + createAuthToken(accessToken, consumerKey))
.post("/v1/token")
.then()
.log().ifValidationFails()
.statusCode(401)
.body(containsString("Provided refresh token does not match the stored refresh token."));
}


@DisplayName("The authentication token misses required claims: access_token")
@Test
void refresh_invalidAuthenticationToken_missingAccessToken() {
prepareDataplaneRuntime();

var authorizationService = DATAPLANE_RUNTIME.getService(DataPlaneAuthorizationService.class);
var edr = authorizationService.createEndpointDataReference(createStartMessage("test-process-id", CONSUMER_DID))
.orElseThrow(f -> new AssertionError(f.getFailureDetail()));

var refreshToken = edr.getStringProperty("refreshToken");
var accessToken = edr.getStringProperty("authorization");

var claims = new JWTClaimsSet.Builder()
/* missing: .claim("access_token", accessToken)*/
.issuer(CONSUMER_DID)
.subject(CONSUMER_DID)
.audience("did:web:bob")
.jwtID(getJwtId(accessToken))
.build();
var authToken = createJwt(consumerKey, claims);

RUNTIME_CONFIG.getRefreshApi().baseRequest()
.queryParam("grant_type", "refresh_token")
.queryParam("refresh_token", refreshToken)
.header(AUTHORIZATION, "Bearer " + authToken)
.post("/v1/token")
.then()
.log().ifValidationFails()
.statusCode(401)
.body(containsString("Required claim 'access_token' not present on token."));
}

@DisplayName("The authentication token misses required claims: audience")
@Test
void refresh_invalidAuthenticationToken_missingAudience() {
prepareDataplaneRuntime();

var authorizationService = DATAPLANE_RUNTIME.getService(DataPlaneAuthorizationService.class);
var edr = authorizationService.createEndpointDataReference(createStartMessage("test-process-id", CONSUMER_DID))
.orElseThrow(f -> new AssertionError(f.getFailureDetail()));

var refreshToken = edr.getStringProperty("refreshToken");
var accessToken = edr.getStringProperty("authorization");

var claims = new JWTClaimsSet.Builder()
.claim("access_token", accessToken)
.issuer(CONSUMER_DID)
.subject(CONSUMER_DID)
/* missing: .audience("did:web:bob")*/
.jwtID(getJwtId(accessToken))
.build();
var authToken = createJwt(consumerKey, claims);

RUNTIME_CONFIG.getRefreshApi().baseRequest()
.queryParam("grant_type", "refresh_token")
.queryParam("refresh_token", refreshToken)
.header(AUTHORIZATION, "Bearer " + authToken)
.post("/v1/token")
.then()
.log().ifValidationFails()
.statusCode(401)
.body(containsString("Required claim 'aud' not present on token."));
}

private void prepareDataplaneRuntime() {
var generator = DATAPLANE_RUNTIME.getContext().getService(PublicEndpointGeneratorService.class);
generator.addGeneratorFunction("HttpData", dataAddress -> Endpoint.url(RUNTIME_CONFIG.getPublicApi().url().toString()));

var vault = DATAPLANE_RUNTIME.getContext().getService(Vault.class);
vault.storeSecret(PROVIDER_KEY_ID, providerKey.toJSONString());
}

private String createAuthToken(String accessToken, ECKey signerKey) {
var claims = new JWTClaimsSet.Builder()
.claim("access_token", accessToken)
.issuer(CONSUMER_DID)
.subject(CONSUMER_DID)
.audience("did:web:bob")
.jwtID(getJwtId(accessToken))
.build();
return createJwt(signerKey, claims);
}

private String createJwt(ECKey signerKey, JWTClaimsSet claims) {
var header = new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(consumerKey.getKeyID()).build();
var jwt = new SignedJWT(header, claims);
try {
jwt.sign(new ECDSASigner(consumerKey));
jwt.sign(new ECDSASigner(signerKey));
return jwt.serialize();
} catch (JOSEException e) {
throw new RuntimeException(e);
Expand Down

0 comments on commit 7db594d

Please sign in to comment.