Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: audience check with participant id or did #270

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import org.eclipse.edc.iam.identitytrust.transform.to.JsonObjectToPresentationQueryTransformer;
import org.eclipse.edc.identityhub.api.v1.PresentationApiController;
import org.eclipse.edc.identityhub.api.validation.PresentationQueryValidator;
import org.eclipse.edc.identityhub.spi.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.generator.VerifiablePresentationService;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
Expand Down Expand Up @@ -61,8 +60,6 @@ public class PresentationApiExtension implements ServiceExtension {
private JsonLd jsonLd;
@Inject
private TypeManager typeManager;
@Inject
private ParticipantContextService participantContextService;

@Override
public String name() {
Expand All @@ -75,7 +72,7 @@ public void initialize(ServiceExtensionContext context) {
validatorRegistry.register(PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY, new PresentationQueryValidator());


var controller = new PresentationApiController(validatorRegistry, typeTransformer, credentialResolver, accessTokenVerifier, verifiablePresentationService, context.getMonitor(), participantContextService);
var controller = new PresentationApiController(validatorRegistry, typeTransformer, credentialResolver, accessTokenVerifier, verifiablePresentationService, context.getMonitor());

var jsonLdMapper = typeManager.getMapper(JSON_LD);
webService.registerResource(RESOLUTION_CONTEXT, new ObjectMapperProvider(jsonLdMapper));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import org.eclipse.edc.identityhub.spi.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.generator.VerifiablePresentationService;
import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQueryMessage;
Expand All @@ -47,7 +45,6 @@
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.eclipse.edc.identityhub.spi.ParticipantContextId.onEncoded;
import static org.eclipse.edc.identitytrust.model.credentialservice.PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY;
import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper;

@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
Expand All @@ -60,17 +57,15 @@ public class PresentationApiController implements PresentationApi {
private final AccessTokenVerifier accessTokenVerifier;
private final VerifiablePresentationService verifiablePresentationService;
private final Monitor monitor;
private final ParticipantContextService participantContextService;

public PresentationApiController(JsonObjectValidatorRegistry validatorRegistry, TypeTransformerRegistry transformerRegistry, CredentialQueryResolver queryResolver,
AccessTokenVerifier accessTokenVerifier, VerifiablePresentationService verifiablePresentationService, Monitor monitor, ParticipantContextService participantContextService) {
AccessTokenVerifier accessTokenVerifier, VerifiablePresentationService verifiablePresentationService, Monitor monitor) {
this.validatorRegistry = validatorRegistry;
this.transformerRegistry = transformerRegistry;
this.queryResolver = queryResolver;
this.accessTokenVerifier = accessTokenVerifier;
this.verifiablePresentationService = verifiablePresentationService;
this.monitor = monitor;
this.participantContextService = participantContextService;
}


Expand All @@ -91,11 +86,6 @@ public Response queryPresentation(@PathParam("participantId") String participant
return notImplemented();
}

// verify that the participant actually exists
participantContextService.getParticipantContext(participantContextId)
.orElseThrow(exceptionMapper(ParticipantContext.class, participantContextId));


// verify and validate the requestor's SI token
var issuerScopes = accessTokenVerifier.verify(token, participantContextId).orElseThrow(f -> new AuthenticationFailedException("ID token verification failed: %s".formatted(f.getFailureDetail())));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@
import jakarta.json.Json;
import jakarta.json.JsonObject;
import org.eclipse.edc.identityhub.api.v1.PresentationApiController;
import org.eclipse.edc.identityhub.spi.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.generator.VerifiablePresentationService;
import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.resolution.QueryResult;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
Expand All @@ -32,7 +30,6 @@
import org.eclipse.edc.junit.annotations.ApiTest;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.result.ServiceResult;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;
import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry;
import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase;
Expand Down Expand Up @@ -78,10 +75,6 @@ class PresentationApiControllerTest extends RestControllerTestBase {
private final CredentialQueryResolver queryResolver = mock();
private final AccessTokenVerifier accessTokenVerifier = mock();
private final VerifiablePresentationService generator = mock();
private final ParticipantContextService participantContextService = mock(a -> ServiceResult.success(ParticipantContext.Builder.newInstance()
.participantId(a.getArgument(0).toString())
.apiTokenAlias("test-alias")
.build()));

@Test
void query_tokenNotPresent_shouldReturn401() {
Expand Down Expand Up @@ -194,7 +187,7 @@ void query_success() {

@Override
protected PresentationApiController controller() {
return new PresentationApiController(validatorRegistryMock, typeTransformerRegistry, queryResolver, accessTokenVerifier, generator, mock(), participantContextService);
return new PresentationApiController(validatorRegistryMock, typeTransformerRegistry, queryResolver, accessTokenVerifier, generator, mock());
}

private String generateJwt() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.eclipse.edc.identityhub.core.creators.JwtPresentationGenerator;
import org.eclipse.edc.identityhub.core.creators.LdpPresentationGenerator;
import org.eclipse.edc.identityhub.spi.KeyPairService;
import org.eclipse.edc.identityhub.spi.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer;
import org.eclipse.edc.identityhub.spi.generator.PresentationCreatorRegistry;
import org.eclipse.edc.identityhub.spi.generator.VerifiablePresentationService;
Expand Down Expand Up @@ -114,6 +115,9 @@ public class CoreServicesExtension implements ServiceExtension {
@Inject
private KeyPairService keyPairService;

@Inject
private ParticipantContextService participantContextService;

@Override
public String name() {
return NAME;
Expand All @@ -129,7 +133,7 @@ public void initialize(ServiceExtensionContext context) {

@Provider
public AccessTokenVerifier createAccessTokenVerifier(ServiceExtensionContext context) {
return new AccessTokenVerifierImpl(tokenValidationService, createPublicKey(context), tokenValidationRulesRegistry, context.getMonitor(), publicKeyResolver);
return new AccessTokenVerifierImpl(tokenValidationService, createPublicKey(context), tokenValidationRulesRegistry, participantContextService, context.getMonitor(), publicKeyResolver);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

package org.eclipse.edc.identityhub.token.verification;

import org.eclipse.edc.identityhub.spi.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
import org.eclipse.edc.spi.iam.PublicKeyResolver;
import org.eclipse.edc.spi.monitor.Monitor;
Expand All @@ -31,6 +32,7 @@

import static com.nimbusds.jwt.JWTClaimNames.AUDIENCE;
import static com.nimbusds.jwt.JWTClaimNames.SUBJECT;
import static java.lang.String.format;
import static org.eclipse.edc.identityhub.DefaultServicesExtension.ACCESS_TOKEN_CLAIM;
import static org.eclipse.edc.identityhub.DefaultServicesExtension.ACCESS_TOKEN_SCOPE_CLAIM;
import static org.eclipse.edc.identityhub.DefaultServicesExtension.IATP_ACCESS_TOKEN_CONTEXT;
Expand All @@ -50,10 +52,16 @@ public class AccessTokenVerifierImpl implements AccessTokenVerifier {
private final Monitor monitor;
private final PublicKeyResolver publicKeyResolver;

public AccessTokenVerifierImpl(TokenValidationService tokenValidationService, Supplier<PublicKey> publicKeySupplier, TokenValidationRulesRegistry tokenValidationRulesRegistry, Monitor monitor,
private final ParticipantContextService participantContextService;

public AccessTokenVerifierImpl(TokenValidationService tokenValidationService, Supplier<PublicKey> publicKeySupplier,
TokenValidationRulesRegistry tokenValidationRulesRegistry,
ParticipantContextService participantContextService,
Monitor monitor,
PublicKeyResolver publicKeyResolver) {
this.tokenValidationService = tokenValidationService;
this.tokenValidationRulesRegistry = tokenValidationRulesRegistry;
this.participantContextService = participantContextService;
this.monitor = monitor;
this.stsPublicKey = publicKeySupplier;
this.publicKeyResolver = publicKeyResolver;
Expand All @@ -62,6 +70,15 @@ public AccessTokenVerifierImpl(TokenValidationService tokenValidationService, Su
@Override
public Result<List<String>> verify(String token, String participantId) {
Objects.requireNonNull(participantId, "Participant ID is mandatory.");

var participantResult = participantContextService.getParticipantContext(participantId);

if (participantResult.failed()) {
return Result.failure(format("Participant with id: %s not found", participantId));
}

var participant = participantResult.getContent();

var res = tokenValidationService.validate(token, publicKeyResolver, tokenValidationRulesRegistry.getRules(IATP_SELF_ISSUED_TOKEN_CONTEXT));
if (res.failed()) {
return res.mapTo();
Expand All @@ -76,7 +93,11 @@ public Result<List<String>> verify(String token, String participantId) {
if (aud == null || aud.isEmpty()) {
return Result.failure("Mandatory claim 'aud' on 'access_token' was null.");
}
return aud.contains(participantId) ? Result.success() : Result.failure("Participant Context ID must match 'aud' claim in 'access_token'");
if (aud.contains(participant.getParticipantId()) || aud.contains(participant.getDid())) {
return Result.success();
} else {
return Result.failure("Participant Context ID or DID must match 'aud' claim in 'access_token'");
}
};

TokenValidationRule subClaimsMatch = (at, additional) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import org.eclipse.edc.identityhub.spi.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext;
import org.eclipse.edc.identityhub.token.rules.ClaimIsPresentRule;
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 @@ -46,16 +49,22 @@
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ComponentTest
class AccessTokenVerifierImplComponentTest {

public static final String DID_WEB_TEST_PARTICIPANT = "did:web:test_participant";
private final Monitor monitor = 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
private KeyPairGenerator generator;

private ParticipantContextService participantContextService = mock();

private ParticipantContext participantContext = mock();

@BeforeEach
void setUp() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
generator = KeyPairGenerator.getInstance("EC");
Expand All @@ -73,23 +82,24 @@ void setUp() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException
var scopeIsPresentRule = new ClaimIsPresentRule(ACCESS_TOKEN_SCOPE_CLAIM);
ruleRegistry.addRule(IATP_ACCESS_TOKEN_CONTEXT, scopeIsPresentRule);

verifier = new AccessTokenVerifierImpl(tokenValidationService, stsKeyPair::getPublic, ruleRegistry, monitor, (id) -> Result.success(providerKeyPair.getPublic()));
when(participantContextService.getParticipantContext(DID_WEB_TEST_PARTICIPANT)).thenReturn(ServiceResult.success(participantContext));
verifier = new AccessTokenVerifierImpl(tokenValidationService, stsKeyPair::getPublic, ruleRegistry, participantContextService, monitor, (id) -> Result.success(providerKeyPair.getPublic()));
}

@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, DID_WEB_TEST_PARTICIPANT)).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, DID_WEB_TEST_PARTICIPANT)).isFailed()
.detail().isEqualTo("Required claim 'access_token' not present on token.");
}

Expand All @@ -100,7 +110,7 @@ void selfIssuedToken_noAccessTokenAudienceClaim() {
.build());
var selfIssuedIdToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("access_token", accessToken)
.build());
assertThat(verifier.verify(selfIssuedIdToken, "did:web:test_participant")).isFailed()
assertThat(verifier.verify(selfIssuedIdToken, DID_WEB_TEST_PARTICIPANT)).isFailed()
.detail().isEqualTo("Mandatory claim 'aud' on 'access_token' was null.");
}

Expand All @@ -110,20 +120,23 @@ void accessToken_notVerified() {
var accessToken = createSignedJwt(spoofedKey, new JWTClaimsSet.Builder().claim("scope", "foobar").claim("foo", "bar").build());
var siToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("access_token", accessToken).build());

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

@Test
void accessToken_noScopeClaim() {
var accessToken = createSignedJwt(stsKeyPair.getPrivate(), new JWTClaimsSet.Builder()/* missing: .claim("scope", "foobar") */
.claim("foo", "bar")
.audience("did:web:test_participant")
.audience(DID_WEB_TEST_PARTICIPANT)
.build());

when(participantContext.getParticipantId()).thenReturn(DID_WEB_TEST_PARTICIPANT);

var siToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("access_token", accessToken)
.build());

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

Expand All @@ -137,20 +150,38 @@ void accessToken_noAudClaim() {
var siToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("access_token", accessToken)
.build());

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

@Test
void accessToken_whenAudClaimMismatch() {
var participantId = "participantID";

var accessToken = createSignedJwt(stsKeyPair.getPrivate(), new JWTClaimsSet.Builder()
.claim("scope", "foobar")
.claim("foo", "bar")
.audience(participantId)
.build());
var siToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("access_token", accessToken)
.build());

assertThat(verifier.verify(siToken, DID_WEB_TEST_PARTICIPANT)).isFailed()
.detail().isEqualTo("Participant Context ID or DID must match 'aud' claim in 'access_token'");
}

@Test
void assertWarning_whenSubjectClaimsMismatch() {
var accessToken = createSignedJwt(stsKeyPair.getPrivate(), new JWTClaimsSet.Builder()
.claim("scope", "foobar")
.audience("did:web:test_participant")
.audience(DID_WEB_TEST_PARTICIPANT)
.subject("test-subject")
.build());
when(participantContext.getParticipantId()).thenReturn(DID_WEB_TEST_PARTICIPANT);

var siToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("access_token", accessToken).subject("mismatching-subject").build());

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

Expand Down
Loading
Loading