Skip to content

Commit

Permalink
added verification of incoming SI tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Oct 10, 2023
1 parent 45fe711 commit 98acd09
Show file tree
Hide file tree
Showing 12 changed files with 348 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@

package org.eclipse.edc.iam.identitytrust.core;

import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry;
import org.eclipse.edc.iam.identitytrust.IdentityAndTrustService;
import org.eclipse.edc.iam.identitytrust.validation.JwtValidatorImpl;
import org.eclipse.edc.iam.identitytrust.verification.JwtVerifierImpl;
import org.eclipse.edc.iam.identitytrust.verification.MultiFormatPresentationVerifier;
import org.eclipse.edc.identitytrust.CredentialServiceClient;
import org.eclipse.edc.identitytrust.SecureTokenService;
import org.eclipse.edc.identitytrust.validation.JwtValidator;
import org.eclipse.edc.identitytrust.verifier.PresentationVerifier;
import org.eclipse.edc.identitytrust.verification.JwtVerifier;
import org.eclipse.edc.identitytrust.verification.PresentationVerifier;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.runtime.metamodel.annotation.Provider;
Expand All @@ -44,12 +47,16 @@ public class IdentityAndTrustExtension implements ServiceExtension {
@Inject
private CredentialServiceClient credentialServiceClient;

@Inject
private DidResolverRegistry resolverRegistry;

private JwtValidator jwtValidator;
private JwtVerifier jwtVerifier;

@Provider
public IdentityService createIdentityService(ServiceExtensionContext context) {
return new IdentityAndTrustService(secureTokenService, getIssuerDid(context), presentationVerifier,
credentialServiceClient, getJwtValidator(), context.getMonitor());
credentialServiceClient, getJwtValidator(), getJwtVerifier());
}

@Provider
Expand All @@ -65,6 +72,14 @@ public PresentationVerifier createPresentationVerifier() {
return new MultiFormatPresentationVerifier();
}

@Provider
private JwtVerifier getJwtVerifier() {
if (jwtVerifier == null) {
jwtVerifier = new JwtVerifierImpl(resolverRegistry);
}
return jwtVerifier;
}

private String getIssuerDid(ServiceExtensionContext context) {
return context.getConfig().getString(ISSUER_DID_PROPERTY);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ plugins {

dependencies {
api(project(":spi:common:identity-trust-spi"))
api(project(":spi:common:identity-did-spi"))
implementation(project(":core:common:util"))
implementation(project(":extensions:common:iam:decentralized-identity:identity-did-crypto"))
implementation(libs.nimbus.jwt)
testImplementation(testFixtures(project(":spi:common:identity-trust-spi")))
testImplementation(project(":core:common:junit"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
import org.eclipse.edc.identitytrust.model.VerifiableCredential;
import org.eclipse.edc.identitytrust.validation.JwtValidator;
import org.eclipse.edc.identitytrust.validation.VcValidationRule;
import org.eclipse.edc.identitytrust.verifier.PresentationVerifier;
import org.eclipse.edc.identitytrust.verification.JwtVerifier;
import org.eclipse.edc.identitytrust.verification.PresentationVerifier;
import org.eclipse.edc.spi.iam.ClaimToken;
import org.eclipse.edc.spi.iam.IdentityService;
import org.eclipse.edc.spi.iam.TokenParameters;
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.util.string.StringUtils;

Expand Down Expand Up @@ -58,21 +58,21 @@ public class IdentityAndTrustService implements IdentityService {
private final PresentationVerifier presentationVerifier;
private final CredentialServiceClient credentialServiceClient;
private final JwtValidator jwtValidator;
private final Monitor monitor;
private final JwtVerifier jwtVerifier;

/**
* Constructs a new instance of the {@link IdentityAndTrustService}.
*
* @param secureTokenService Instance of an STS, which can create SI tokens
* @param myOwnDid The DID which belongs to "this connector"
*/
public IdentityAndTrustService(SecureTokenService secureTokenService, String myOwnDid, PresentationVerifier presentationVerifier, CredentialServiceClient credentialServiceClient, JwtValidator jwtValidator, Monitor monitor) {
public IdentityAndTrustService(SecureTokenService secureTokenService, String myOwnDid, PresentationVerifier presentationVerifier, CredentialServiceClient credentialServiceClient, JwtValidator jwtValidator, JwtVerifier jwtVerifier) {
this.secureTokenService = secureTokenService;
this.myOwnDid = myOwnDid;
this.presentationVerifier = presentationVerifier;
this.credentialServiceClient = credentialServiceClient;
this.jwtValidator = jwtValidator;
this.monitor = monitor;
this.jwtVerifier = jwtVerifier;
}

@Override
Expand All @@ -93,7 +93,10 @@ public Result<TokenRepresentation> obtainClientCredentials(TokenParameters param

@Override
public Result<ClaimToken> verifyJwtToken(TokenRepresentation tokenRepresentation, String audience) {
var issuerResult = jwtValidator.validateToken(tokenRepresentation, audience)

// verify and validate incoming SI Token
var issuerResult = jwtVerifier.verify(tokenRepresentation, audience)
.compose(v -> jwtValidator.validateToken(tokenRepresentation, audience))
.compose(claimToken -> success(claimToken.getStringClaim("iss")));

if (issuerResult.failed()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* 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
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.iam.identitytrust.verification;

import com.nimbusds.jwt.SignedJWT;
import org.eclipse.edc.iam.did.crypto.JwtUtils;
import org.eclipse.edc.iam.did.crypto.key.KeyConverter;
import org.eclipse.edc.iam.did.spi.document.DidConstants;
import org.eclipse.edc.iam.did.spi.document.DidDocument;
import org.eclipse.edc.iam.did.spi.document.VerificationMethod;
import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry;
import org.eclipse.edc.identitytrust.verification.JwtVerifier;
import org.eclipse.edc.spi.iam.TokenRepresentation;
import org.eclipse.edc.spi.result.Result;
import org.jetbrains.annotations.NotNull;

import java.text.ParseException;
import java.util.Optional;

/**
* This implementation
*/
public class JwtVerifierImpl implements JwtVerifier {
private final DidResolverRegistry resolverRegistry;

public JwtVerifierImpl(DidResolverRegistry resolverRegistry) {
this.resolverRegistry = resolverRegistry;
}

@Override
public Result<Void> verify(TokenRepresentation tokenRepresentation, String audience) {

SignedJWT jwt;
try {
jwt = SignedJWT.parse(tokenRepresentation.getToken());
var didResult = resolverRegistry.resolve(jwt.getJWTClaimsSet().getIssuer());
if (didResult.failed()) {
return Result.failure("Unable to resolve DID: %s".formatted(didResult.getFailureDetail()));
}

// this will return the _first_ public key entry
var keyId = jwt.getHeader().getKeyID();

//either get the first verification method, or the one specified by the key id
var publicKey = Optional.ofNullable(keyId)
.map(kid -> getVerificationMethod(didResult.getContent(), kid))
.orElseGet(() -> firstVerificationMethod(didResult.getContent()));

if (publicKey.isEmpty()) {
return Result.failure("Public Key not found in DID Document.");
}

//convert the POJO into a usable PK-wrapper:
var publicKeyJwk = publicKey.get().getPublicKeyJwk();
var publicKeyWrapperResult = KeyConverter.toPublicKeyWrapper(publicKeyJwk, publicKey.get().getId());
if (publicKeyWrapperResult.failed()) {
return publicKeyWrapperResult.mapTo();
}

var verified = JwtUtils.verify(jwt, publicKeyWrapperResult.getContent(), audience);
if (verified.failed()) {
return Result.failure("Token could not be verified: %s".formatted(verified.getFailureDetail()));
}
return Result.success();
} catch (ParseException e) {
return Result.failure("Error parsing JWT");
}
}

private Optional<VerificationMethod> getVerificationMethod(DidDocument content, String kid) {
return content.getVerificationMethod().stream().filter(vm -> vm.getId().equals(kid))
.findFirst();
}

@NotNull
private Optional<VerificationMethod> firstVerificationMethod(DidDocument did) {
return did.getVerificationMethod().stream()
.filter(vm -> DidConstants.ALLOWED_VERIFICATION_TYPES.contains(vm.getType()))
.findFirst();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
package org.eclipse.edc.iam.identitytrust.verification;

import org.eclipse.edc.identitytrust.model.CredentialFormat;
import org.eclipse.edc.identitytrust.verifier.PresentationVerifier;
import org.eclipse.edc.identitytrust.verification.PresentationVerifier;
import org.eclipse.edc.spi.result.Result;

public class MultiFormatPresentationVerifier implements PresentationVerifier {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
import org.eclipse.edc.identitytrust.model.CredentialSubject;
import org.eclipse.edc.identitytrust.model.VerifiablePresentationContainer;
import org.eclipse.edc.identitytrust.validation.JwtValidator;
import org.eclipse.edc.identitytrust.verifier.PresentationVerifier;
import org.eclipse.edc.identitytrust.verification.JwtVerifier;
import org.eclipse.edc.identitytrust.verification.PresentationVerifier;
import org.eclipse.edc.spi.iam.ClaimToken;
import org.eclipse.edc.spi.iam.TokenParameters;
import org.eclipse.edc.spi.result.Result;
Expand Down Expand Up @@ -60,11 +61,13 @@ class IdentityAndTrustServiceTest {
private final PresentationVerifier mockedVerifier = mock();
private final CredentialServiceClient mockedClient = mock();
private final JwtValidator jwtValidatorMock = mock();
private final IdentityAndTrustService service = new IdentityAndTrustService(mockedSts, EXPECTED_OWN_DID, mockedVerifier, mockedClient, jwtValidatorMock, mock());
private final JwtVerifier jwtVerfierMock = mock();
private final IdentityAndTrustService service = new IdentityAndTrustService(mockedSts, EXPECTED_OWN_DID, mockedVerifier, mockedClient, jwtValidatorMock, jwtVerfierMock);

@BeforeEach
void setup() {
when(jwtValidatorMock.validateToken(any(), any())).thenReturn(success(ClaimToken.Builder.newInstance().claim("iss", CONSUMER_DID).build()));
when(jwtVerfierMock.verify(any(), any())).thenReturn(success());
}

@Nested
Expand Down Expand Up @@ -194,6 +197,16 @@ void jwtTokenNotValid() {
.messages().hasSize(1)
.containsExactly("test failure");
}

@Test
void jwtTokenNotVerified() {
when(jwtVerfierMock.verify(any(), any())).thenReturn(failure("test-failure"));
var token = createJwt();
assertThat(service.verifyJwtToken(token, "test-audience"))
.isFailed()
.messages().hasSize(1)
.containsExactly("test-failure");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
*
*/

package org.eclipse.edc.iam.identitytrust.service;
package org.eclipse.edc.iam.identitytrust.validation;

import com.nimbusds.jwt.JWTClaimsSet;
import org.eclipse.edc.iam.identitytrust.validation.JwtValidatorImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -64,8 +63,7 @@ void issAndSubNotEqual() {
.build();
assertThat(validator.validateToken(createJwt(claimsSet), "some-aud"))
.isFailed()
.messages().hasSize(1)
.containsExactly("The iss and aud claims must be identical.");
.detail().isEqualTo("The iss and aud claims must be identical.");
}

@Disabled("not testable")
Expand All @@ -87,8 +85,7 @@ void audNotEqualToOwnDid() {
var token = createJwt(claimsSet);
assertThat(validator.validateToken(token, EXPECTED_OWN_DID))
.isFailed()
.messages().hasSize(1)
.containsExactly("aud claim expected to be %s but was [%s]".formatted(EXPECTED_OWN_DID, "invalid-audience"));
.detail().isEqualTo("aud claim expected to be %s but was [%s]".formatted(EXPECTED_OWN_DID, "invalid-audience"));
}

@Test
Expand All @@ -104,8 +101,7 @@ void clientIdClaim_NotEqualToConsumerDid() {
var token = createJwt(claimsSet);
assertThat(validator.validateToken(token, EXPECTED_OWN_DID))
.isFailed()
.messages().hasSize(1)
.containsExactly("client_id must be equal to the issuer ID");
.detail().isEqualTo("client_id must be equal to the issuer ID");
}

@Test
Expand All @@ -122,8 +118,7 @@ void subJwkClaimPresent() {

var token = createJwt(claimsSet);
assertThat(validator.validateToken(token, EXPECTED_OWN_DID)).isFailed()
.messages().hasSize(1)
.containsExactly("The sub_jwk claim must not be present.");
.detail().isEqualTo("The sub_jwk claim must not be present.");
}


Expand All @@ -139,8 +134,7 @@ void jtiNotPresent() {

var token = createJwt(claimsSet);
assertThat(validator.validateToken(token, EXPECTED_OWN_DID)).isFailed()
.messages().hasSize(1)
.containsExactly("The jti claim is mandatory.");
.detail().isEqualTo("The jti claim is mandatory.");
}

@Test
Expand All @@ -155,8 +149,7 @@ void expNotPresent() {

var token = createJwt(claimsSet);
assertThat(validator.validateToken(token, EXPECTED_OWN_DID)).isFailed()
.messages().hasSize(1)
.containsExactly("The exp claim is mandatory.");
.detail().isEqualTo("The exp claim is mandatory.");
}

@Test
Expand All @@ -172,8 +165,7 @@ void tokenExpired() {

var token = createJwt(claimsSet);
assertThat(validator.validateToken(token, EXPECTED_OWN_DID)).isFailed()
.messages().hasSize(1)
.containsExactly("The token must not be expired.");
.detail().isEqualTo("The token must not be expired.");
}

}
Loading

0 comments on commit 98acd09

Please sign in to comment.