Skip to content

Commit

Permalink
fix: get DID for participantContextId
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Jun 5, 2024
1 parent a3568fd commit 26557fb
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer;
import org.eclipse.edc.identityhub.spi.keypair.KeyPairService;
import org.eclipse.edc.identityhub.spi.model.IdentityHubConstants;
import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.store.CredentialStore;
import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.CredentialStatusCheckService;
Expand Down Expand Up @@ -120,6 +121,8 @@ public class CoreServicesExtension implements ServiceExtension {

@Inject
private LocalPublicKeyService fallbackService;
@Inject
private ParticipantContextService participantContextService;

@Override
public String name() {
Expand All @@ -136,7 +139,7 @@ public void initialize(ServiceExtensionContext context) {
@Provider
public AccessTokenVerifier createAccessTokenVerifier(ServiceExtensionContext context) {
var keyResolver = new KeyPairResourcePublicKeyResolver(store, keyParserRegistry, context.getMonitor(), fallbackService);
return new AccessTokenVerifierImpl(tokenValidationService, keyResolver, tokenValidationRulesRegistry, context.getMonitor(), publicKeyResolver);
return new AccessTokenVerifierImpl(tokenValidationService, keyResolver, tokenValidationRulesRegistry, context.getMonitor(), publicKeyResolver, participantContextService);
}

@Provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package org.eclipse.edc.identityhub.accesstoken.verification;

import org.eclipse.edc.identityhub.publickey.KeyPairResourcePublicKeyResolver;
import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
import org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames;
import org.eclipse.edc.keys.spi.PublicKeyResolver;
Expand Down Expand Up @@ -47,14 +48,16 @@ public class AccessTokenVerifierImpl implements AccessTokenVerifier {
private final TokenValidationRulesRegistry tokenValidationRulesRegistry;
private final Monitor monitor;
private final PublicKeyResolver publicKeyResolver;
private final ParticipantContextService participantContextService;

public AccessTokenVerifierImpl(TokenValidationService tokenValidationService, KeyPairResourcePublicKeyResolver localPublicKeyService, TokenValidationRulesRegistry tokenValidationRulesRegistry, Monitor monitor,
PublicKeyResolver publicKeyResolver) {
PublicKeyResolver publicKeyResolver, ParticipantContextService participantContextService) {
this.tokenValidationService = tokenValidationService;
this.localPublicKeyService = localPublicKeyService;
this.tokenValidationRulesRegistry = tokenValidationRulesRegistry;
this.monitor = monitor;
this.publicKeyResolver = publicKeyResolver;
this.participantContextService = participantContextService;
}

@Override
Expand All @@ -74,7 +77,15 @@ public Result<List<String>> verify(String token, String participantId) {
if (aud == null || aud.isEmpty()) {
return Result.failure("Mandatory claim 'aud' on 'token' was null.");
}
return aud.contains(participantId) ? Result.success() : Result.failure("Participant Context ID must match 'aud' claim in 'access_token'");
var participantDidResult = participantContextService.getParticipantContext(participantId);

if (participantDidResult.failed()) {
return Result.failure(participantDidResult.getFailureDetail());
}
var pcDid = participantDidResult.getContent().getDid();
return aud.contains(pcDid) ?
Result.success() :
Result.failure("The DID associated with the Participant Context ID of this request ('%s') must match 'aud' claim in 'access_token' (%s).".formatted(pcDid, aud));
};

TokenValidationRule subClaimsMatch = (at, additional) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@
import com.nimbusds.jwt.SignedJWT;
import org.eclipse.edc.identityhub.accesstoken.rules.ClaimIsPresentRule;
import org.eclipse.edc.identityhub.publickey.KeyPairResourcePublicKeyResolver;
import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext;
import org.eclipse.edc.junit.annotations.ComponentTest;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.result.ServiceResult;
import org.eclipse.edc.token.TokenValidationRulesRegistryImpl;
import org.eclipse.edc.token.TokenValidationServiceImpl;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -45,6 +48,7 @@
import static org.eclipse.edc.identityhub.accesstoken.verification.AccessTokenConstants.TOKEN_CLAIM;
import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
Expand All @@ -54,7 +58,10 @@
class AccessTokenVerifierImplComponentTest {

public static final String STS_PUBLIC_KEY_ID = "sts-key-123";
public static final String PARTICIPANT_CONTEXT_ID = "did:web:test_participant";
public static final String PARTICIPANT_DID = "did:web:test_participant";
private final Monitor monitor = mock();
private final ParticipantContextService participantContextService = mock();
private AccessTokenVerifierImpl verifier;
private KeyPair stsKeyPair; // this is used to sign the acces token
private KeyPair providerKeyPair; // this is used to sign the incoming SI token
Expand All @@ -80,23 +87,24 @@ void setUp() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException
var resolverMock = mock(KeyPairResourcePublicKeyResolver.class);
when(resolverMock.resolveKey(anyString(), anyString())).thenReturn(Result.success(stsKeyPair.getPublic()));

verifier = new AccessTokenVerifierImpl(tokenValidationService, resolverMock, ruleRegistry, monitor, (id) -> Result.success(providerKeyPair.getPublic()));
when(participantContextService.getParticipantContext(anyString())).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance().did(PARTICIPANT_DID).participantId(PARTICIPANT_CONTEXT_ID).apiTokenAlias("foobar").build()));
verifier = new AccessTokenVerifierImpl(tokenValidationService, resolverMock, ruleRegistry, monitor, (id) -> Result.success(providerKeyPair.getPublic()), participantContextService);
}

@Test
void selfIssuedTokenNotVerified() {
var spoofedKey = generator.generateKeyPair().getPrivate();

var selfIssuedIdToken = createSignedJwt(spoofedKey, new JWTClaimsSet.Builder().claim("foo", "bar").jwtID(UUID.randomUUID().toString()).build());
assertThat(verifier.verify(selfIssuedIdToken, "did:web:test_participant")).isFailed()
assertThat(verifier.verify(selfIssuedIdToken, PARTICIPANT_CONTEXT_ID)).isFailed()
.detail().isEqualTo("Token verification failed");

}

@Test
void selfIssuedToken_noAccessTokenClaim() {
var selfIssuedIdToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder()/* missing: claims("access_token", "....") */.build());
assertThat(verifier.verify(selfIssuedIdToken, "did:web:test_participant")).isFailed()
assertThat(verifier.verify(selfIssuedIdToken, PARTICIPANT_CONTEXT_ID)).isFailed()
.detail().isEqualTo("Required claim 'token' not present on token.");
}

Expand All @@ -107,17 +115,52 @@ void selfIssuedToken_noAccessTokenAudienceClaim() {
.build());
var selfIssuedIdToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("token", accessToken)
.build());
assertThat(verifier.verify(selfIssuedIdToken, "did:web:test_participant")).isFailed()
assertThat(verifier.verify(selfIssuedIdToken, PARTICIPANT_CONTEXT_ID)).isFailed()
.detail().isEqualTo("Mandatory claim 'aud' on 'token' was null.");
}


@Test
void accessToken_audClaimDoesNotBelongToParticipant() {
var accessToken = createSignedJwt(stsKeyPair.getPrivate(), new JWTClaimsSet.Builder()
.claim("scope", "foobar")
.audience(PARTICIPANT_DID)
.build());
var selfIssuedIdToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("token", accessToken)
.build());
when(participantContextService.getParticipantContext(eq(PARTICIPANT_CONTEXT_ID))).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance()
.did("did:web:someone_else")
.participantId(PARTICIPANT_CONTEXT_ID)
.apiTokenAlias("foobar")
.build()));

assertThat(verifier.verify(selfIssuedIdToken, PARTICIPANT_CONTEXT_ID)).isFailed()
.detail()
.isEqualTo("The DID associated with the Participant Context ID of this request ('did:web:someone_else') must match 'aud' claim in 'access_token' ([%s]).".formatted(PARTICIPANT_DID));
}

@Test
void accessToken_participantServiceError() {
var accessToken = createSignedJwt(stsKeyPair.getPrivate(), new JWTClaimsSet.Builder()
.claim("scope", "foobar")
.audience(PARTICIPANT_DID)
.build());
var selfIssuedIdToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("token", accessToken)
.build());
when(participantContextService.getParticipantContext(eq(PARTICIPANT_CONTEXT_ID))).thenReturn(ServiceResult.notFound("foobar not found barbaz"));

assertThat(verifier.verify(selfIssuedIdToken, PARTICIPANT_CONTEXT_ID)).isFailed()
.detail()
.isEqualTo("foobar not found barbaz");
}

@Test
void accessToken_notVerified() {
var spoofedKey = generator.generateKeyPair().getPrivate();
var accessToken = createSignedJwt(spoofedKey, new JWTClaimsSet.Builder().claim("scope", "foobar").claim("foo", "bar").build());
var siToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("token", accessToken).build());

assertThat(verifier.verify(siToken, "did:web:test_participant")).isFailed()
assertThat(verifier.verify(siToken, PARTICIPANT_CONTEXT_ID)).isFailed()
.detail().isEqualTo("Token verification failed");
}

Expand All @@ -126,12 +169,12 @@ void accessToken_noScopeClaim() {
var accessToken = createSignedJwt(stsKeyPair.getPrivate(), new JWTClaimsSet.Builder()
/* missing: .claim("scope", "foobar") */
.claim("foo", "bar")
.audience("did:web:test_participant")
.audience(PARTICIPANT_CONTEXT_ID)
.build());
var siToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("token", accessToken)
.build());

assertThat(verifier.verify(siToken, "did:web:test_participant")).isFailed()
assertThat(verifier.verify(siToken, PARTICIPANT_CONTEXT_ID)).isFailed()
.detail().isEqualTo("Required claim 'scope' not present on token.");
}

Expand All @@ -145,23 +188,24 @@ void accessToken_noAudClaim() {
var siToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("token", accessToken)
.build());

assertThat(verifier.verify(siToken, "did:web:test_participant")).isFailed()
assertThat(verifier.verify(siToken, PARTICIPANT_CONTEXT_ID)).isFailed()
.detail().isEqualTo("Mandatory claim 'aud' on 'token' was null.");
}

@Test
void assertWarning_whenSubjectClaimsMismatch() {
var accessToken = createSignedJwt(stsKeyPair.getPrivate(), new JWTClaimsSet.Builder()
.claim("scope", "foobar")
.audience("did:web:test_participant")
.audience(PARTICIPANT_CONTEXT_ID)
.subject("test-subject")
.build());
var siToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("token", accessToken).subject("mismatching-subject").build());

assertThat(verifier.verify(siToken, "did:web:test_participant")).isSucceeded();
assertThat(verifier.verify(siToken, PARTICIPANT_CONTEXT_ID)).isSucceeded();
verify(monitor).warning(startsWith("ID token [sub] claim is not equal to [token.sub]"));
}


private String createSignedJwt(PrivateKey signingKey, JWTClaimsSet claimsSet) {
try {
var signer = new ECDSASigner(signingKey, Curve.P_256);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import org.assertj.core.api.Assertions;
import org.eclipse.edc.identityhub.publickey.KeyPairResourcePublicKeyResolver;
import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService;
import org.eclipse.edc.identityhub.verifiablecredentials.testfixtures.JwtCreationUtil;
import org.eclipse.edc.identityhub.verifiablecredentials.testfixtures.VerifiableCredentialTestUtil;
import org.eclipse.edc.junit.assertions.AbstractResultAssert;
Expand All @@ -38,6 +39,7 @@

class AccessTokenVerifierImplTest {
public static final String OWN_DID = "did:web:consumer";
public static final String PARTICIPANT_CONTEXT_ID = "did:web:test_participant";
private static final String OTHER_PARTICIPANT_DID = "did:web:provider";
private final TokenValidationService tokenValidationSerivce = mock();
private final TokenValidationRulesRegistry tokenValidationRulesRegistry = mock();
Expand All @@ -47,13 +49,14 @@ class AccessTokenVerifierImplTest {
.claim("scope", "org.eclipse.edc.vc.type:AlumniCredential:read")
.build();
private final KeyPairResourcePublicKeyResolver localPublicKeyResolver = mock();
private final AccessTokenVerifierImpl verifier = new AccessTokenVerifierImpl(tokenValidationSerivce, localPublicKeyResolver, tokenValidationRulesRegistry, mock(), pkResolver);
private final ParticipantContextService participantContextService = mock();
private final AccessTokenVerifierImpl verifier = new AccessTokenVerifierImpl(tokenValidationSerivce, localPublicKeyResolver, tokenValidationRulesRegistry, mock(), pkResolver, participantContextService);

@Test
void verify_validSiToken_validAccessToken() {
when(tokenValidationSerivce.validate(anyString(), any(), anyList()))
.thenReturn(Result.success(idToken));
AbstractResultAssert.assertThat(verifier.verify(JwtCreationUtil.generateSiToken(OWN_DID, OTHER_PARTICIPANT_DID), "did:web:test_participant"))
AbstractResultAssert.assertThat(verifier.verify(JwtCreationUtil.generateSiToken(OWN_DID, OTHER_PARTICIPANT_DID), PARTICIPANT_CONTEXT_ID))
.isSucceeded()
.satisfies(strings -> Assertions.assertThat(strings).containsOnly(JwtCreationUtil.TEST_SCOPE));
verify(tokenValidationSerivce, times(2)).validate(anyString(), any(PublicKeyResolver.class), anyList());
Expand All @@ -64,7 +67,7 @@ void verify_validSiToken_validAccessToken() {
void verify_siTokenValidationFails() {
when(tokenValidationSerivce.validate(anyString(), any(), anyList()))
.thenReturn(Result.failure("test-failure"));
AbstractResultAssert.assertThat(verifier.verify(JwtCreationUtil.generateSiToken(OWN_DID, OTHER_PARTICIPANT_DID), "did:web:test_participant")).isFailed()
AbstractResultAssert.assertThat(verifier.verify(JwtCreationUtil.generateSiToken(OWN_DID, OTHER_PARTICIPANT_DID), PARTICIPANT_CONTEXT_ID)).isFailed()
.detail().contains("test-failure");
}

Expand All @@ -73,7 +76,7 @@ void verify_noAccessTokenClaim() {
when(tokenValidationSerivce.validate(anyString(), any(PublicKeyResolver.class), anyList()))
.thenReturn(Result.failure("no access token"));

AbstractResultAssert.assertThat(verifier.verify(JwtCreationUtil.generateSiToken(OWN_DID, OTHER_PARTICIPANT_DID), "did:web:test_participant")).isFailed()
AbstractResultAssert.assertThat(verifier.verify(JwtCreationUtil.generateSiToken(OWN_DID, OTHER_PARTICIPANT_DID), PARTICIPANT_CONTEXT_ID)).isFailed()
.detail().contains("no access token");
verify(tokenValidationSerivce).validate(anyString(), any(PublicKeyResolver.class), anyList());
}
Expand All @@ -85,7 +88,7 @@ void verify_accessTokenValidationFails() {
var siToken = JwtCreationUtil.generateJwt(OWN_DID, OTHER_PARTICIPANT_DID, OTHER_PARTICIPANT_DID, Map.of("client_id", OTHER_PARTICIPANT_DID, "access_token", accessToken), JwtCreationUtil.PROVIDER_KEY);

when(tokenValidationSerivce.validate(anyString(), any(), anyList())).thenReturn(Result.failure("test-failure"));
AbstractResultAssert.assertThat(verifier.verify(siToken, "did:web:test_participant")).isFailed()
AbstractResultAssert.assertThat(verifier.verify(siToken, PARTICIPANT_CONTEXT_ID)).isFailed()
.detail().isEqualTo("test-failure");
}

Expand All @@ -102,7 +105,7 @@ void verify_accessTokenDoesNotContainScopeClaim() {
when(tokenValidationSerivce.validate(anyString(), any(), anyList())).thenReturn(Result.success(idToken));
when(tokenValidationSerivce.validate(anyString(), any(), anyList())).thenReturn(Result.failure("test-failure"));

AbstractResultAssert.assertThat(verifier.verify(siToken, "did:web:test_participant"))
AbstractResultAssert.assertThat(verifier.verify(siToken, PARTICIPANT_CONTEXT_ID))
.isFailed()
.detail().contains("test-failure");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ public Result<PublicKey> resolveKey(String publicKeyId, String participantId) {
return resources.stream().findAny()
.map(kpr -> parseKey(kpr.getSerializedPublicKey()))
.orElseGet(() -> {
monitor.warning("No KeyPairResource with keyId '%s' was found in the store. Will attempt to resolve from the Vault. This could be an indication of a data inconsistency, it is recommended to revoke and regenerate keys!");
monitor.warning("No KeyPairResource with keyId '%s' was found for participant '%s' in the store. Will attempt to resolve from the Vault. ".formatted(publicKeyId, participantId) +
"This could be an indication of a data inconsistency, it is recommended to revoke and regenerate keys!");
return fallbackResolver.resolveKey(publicKeyId); // attempt to resolve from vault
});
}
Expand Down
Loading

0 comments on commit 26557fb

Please sign in to comment.