Skip to content

Commit

Permalink
added validation for incoming SI tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Oct 10, 2023
1 parent 64c501b commit 45fe711
Show file tree
Hide file tree
Showing 13 changed files with 354 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies {
implementation(project(":core:common:util"))
implementation(project(":extensions:common:crypto:jws2020"))
implementation(project(":extensions:common:iam:identity-trust:identity-trust-service"))
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 @@ -14,10 +14,12 @@

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

import org.eclipse.edc.iam.identitytrust.core.service.MultiFormatPresentationVerifier;
import org.eclipse.edc.iam.identitytrust.service.IdentityAndTrustService;
import org.eclipse.edc.iam.identitytrust.IdentityAndTrustService;
import org.eclipse.edc.iam.identitytrust.validation.JwtValidatorImpl;
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.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
Expand All @@ -42,9 +44,20 @@ public class IdentityAndTrustExtension implements ServiceExtension {
@Inject
private CredentialServiceClient credentialServiceClient;

private JwtValidator jwtValidator;

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

@Provider
public JwtValidator getJwtValidator() {
if (jwtValidator == null) {
jwtValidator = new JwtValidatorImpl();
}
return jwtValidator;
}

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

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

import org.eclipse.edc.iam.identitytrust.service.IdentityAndTrustService;
import org.eclipse.edc.iam.identitytrust.IdentityAndTrustService;
import org.eclipse.edc.identitytrust.SecureTokenService;
import org.eclipse.edc.junit.extensions.DependencyInjectionExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
*
*/

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

import com.nimbusds.jwt.SignedJWT;
import org.eclipse.edc.iam.identitytrust.validation.HasValidIssuer;
import org.eclipse.edc.iam.identitytrust.validation.HasValidSubjectIds;
import org.eclipse.edc.iam.identitytrust.validation.IsRevoked;
import org.eclipse.edc.identitytrust.CredentialServiceClient;
import org.eclipse.edc.identitytrust.SecureTokenService;
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.spi.iam.ClaimToken;
Expand All @@ -31,7 +31,6 @@
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.util.string.StringUtils;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
Expand All @@ -58,6 +57,7 @@ public class IdentityAndTrustService implements IdentityService {
private final String myOwnDid;
private final PresentationVerifier presentationVerifier;
private final CredentialServiceClient credentialServiceClient;
private final JwtValidator jwtValidator;
private final Monitor monitor;

/**
Expand All @@ -66,11 +66,12 @@ public class IdentityAndTrustService implements IdentityService {
* @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, Monitor monitor) {
public IdentityAndTrustService(SecureTokenService secureTokenService, String myOwnDid, PresentationVerifier presentationVerifier, CredentialServiceClient credentialServiceClient, JwtValidator jwtValidator, Monitor monitor) {
this.secureTokenService = secureTokenService;
this.myOwnDid = myOwnDid;
this.presentationVerifier = presentationVerifier;
this.credentialServiceClient = credentialServiceClient;
this.jwtValidator = jwtValidator;
this.monitor = monitor;
}

Expand All @@ -92,15 +93,9 @@ public Result<TokenRepresentation> obtainClientCredentials(TokenParameters param

@Override
public Result<ClaimToken> verifyJwtToken(TokenRepresentation tokenRepresentation, String audience) {
SignedJWT jwt;
try {
// todo: implement validation of consumer's SI token
jwt = SignedJWT.parse(tokenRepresentation.getToken());
} catch (ParseException e) {
monitor.severe("Error parsing JWT:", e);
return Result.failure("Error parsing JWT");
}
var issuerResult = getIssuerDid(jwt);
var issuerResult = jwtValidator.validateToken(tokenRepresentation, audience)
.compose(claimToken -> success(claimToken.getStringClaim("iss")));

if (issuerResult.failed()) {
return issuerResult.mapTo();
}
Expand Down Expand Up @@ -133,16 +128,6 @@ public Result<ClaimToken> verifyJwtToken(TokenRepresentation tokenRepresentation
return result.map(u -> extractClaimToken(credentials));
}

private Result<String> getIssuerDid(SignedJWT tokenRepresentation) {

try {
return success(tokenRepresentation.getJWTClaimsSet().getIssuer());
} catch (ParseException e) {
monitor.severe("Error extracting issuer claim");
return Result.failure("Failed to extract issuer claim");
}
}

private ClaimToken extractClaimToken(List<VerifiableCredential> credentials) {

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'credentials' is never used.
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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.validation;

import com.nimbusds.jwt.SignedJWT;
import org.eclipse.edc.identitytrust.validation.JwtValidator;
import org.eclipse.edc.spi.iam.ClaimToken;
import org.eclipse.edc.spi.iam.TokenRepresentation;
import org.eclipse.edc.spi.result.Result;

import java.text.ParseException;
import java.util.Objects;

import static java.time.Instant.now;
import static org.eclipse.edc.spi.result.Result.failure;
import static org.eclipse.edc.spi.result.Result.success;

/**
* Default implementation for JWT validation in the context of IATP.
*/
public class JwtValidatorImpl implements JwtValidator {

private static final long EPSILON = 60;

@SuppressWarnings("checkstyle:WhitespaceAfter")
@Override
public Result<ClaimToken> validateToken(TokenRepresentation tokenRepresentation, String audience) {
SignedJWT jwt;
try {
jwt = SignedJWT.parse(tokenRepresentation.getToken());

var claims = jwt.getJWTClaimsSet();
var iss = claims.getIssuer();
var aud = claims.getAudience();
var jti = claims.getClaim("jti");
var clientId = claims.getClaim("client_id");
var sub = claims.getSubject();
var exp = claims.getExpirationTime();
var subJwk = claims.getClaim("sub_jwk");

if (!Objects.equals(iss, sub)) {
return failure("The iss and aud claims must be identical.");
}
if (subJwk != null) {
return failure("The sub_jwk claim must not be present.");
}
if (!aud.contains(audience)) {
return failure("aud claim expected to be %s but was %s".formatted(audience, aud));
}
if (!Objects.equals(clientId, iss)) {
return failure("client_id must be equal to the issuer ID");
}
if (jti == null) {
return failure("The jti claim is mandatory.");
}
if (exp == null) {
return failure("The exp claim is mandatory.");
}
if (exp.toInstant().plusSeconds(EPSILON).isBefore(now())) {
return failure("The token must not be expired.");
}
var bldr = ClaimToken.Builder.newInstance();
jwt.getJWTClaimsSet().getClaims().forEach(bldr::claim);
return success(bldr.build());
} catch (ParseException e) {
return failure("Error parsing JWT");
}


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

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

import org.eclipse.edc.spi.result.Result;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*
*/

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

import org.eclipse.edc.spi.result.Result;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*
*/

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

import org.eclipse.edc.identitytrust.model.CredentialFormat;
import org.eclipse.edc.identitytrust.verifier.PresentationVerifier;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@
package org.eclipse.edc.iam.identitytrust.service;


import org.eclipse.edc.iam.identitytrust.IdentityAndTrustService;
import org.eclipse.edc.identitytrust.CredentialServiceClient;
import org.eclipse.edc.identitytrust.SecureTokenService;
import org.eclipse.edc.identitytrust.model.CredentialFormat;
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.spi.iam.ClaimToken;
import org.eclipse.edc.spi.iam.TokenParameters;
import org.eclipse.edc.spi.result.Result;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -51,14 +55,20 @@

class IdentityAndTrustServiceTest {
public static final String EXPECTED_OWN_DID = "did:web:test";
public static final String CONSUMER_DID = "did:web:consumer";
private final SecureTokenService mockedSts = mock();
private final PresentationVerifier mockedVerifier = mock();
private final CredentialServiceClient mockedClient = mock();
private final IdentityAndTrustService service = new IdentityAndTrustService(mockedSts, EXPECTED_OWN_DID, mockedVerifier, mockedClient, mock());
private final JwtValidator jwtValidatorMock = mock();
private final IdentityAndTrustService service = new IdentityAndTrustService(mockedSts, EXPECTED_OWN_DID, mockedVerifier, mockedClient, jwtValidatorMock, mock());

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

@Nested
class VerifyObtainToken {
class ObtainClientCredentials {
@ParameterizedTest(name = "{0}")
@ValueSource(strings = { "org.eclipse.edc:TestCredential:modify", "org.eclipse.edc:TestCredential:", "org.eclipse.edc:TestCredential: ", "org.eclipse.edc:TestCredential:write*", ":TestCredential:read",
"org.eclipse.edc:fooCredential:+" })
Expand Down Expand Up @@ -105,8 +115,9 @@ void obtainClientCredentials_stsFails() {
}
}


@Nested
class PresentationValidation {
class VerifyJwtToken {

@Test
void presentationRequestFails() {
Expand Down Expand Up @@ -142,12 +153,11 @@ void oneInvalidSubjectId() {
var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.JSON_LD, presentation);
when(mockedVerifier.verifyPresentation(anyString(), any(CredentialFormat.class))).thenReturn(success());
when(mockedClient.requestPresentation(any(), any(), any())).thenReturn(success(vpContainer));
var consumerDid = "did:web:test-consumer";
var token = createJwt(consumerDid, EXPECTED_OWN_DID);
var token = createJwt(CONSUMER_DID, EXPECTED_OWN_DID);
var result = service.verifyJwtToken(token, "test-audience");
assertThat(result).isFailed().messages()
.hasSizeGreaterThanOrEqualTo(1)
.contains("Not all subject IDs match the expected subject ID %s".formatted(consumerDid));
.contains("Not all subject IDs match the expected subject ID %s".formatted(CONSUMER_DID));
}

@Disabled("Not yet implemented")
Expand Down Expand Up @@ -175,6 +185,15 @@ void credentialHasInvalidIssuer_issuerIsUrl() {
.contains("Issuer 'invalid-issuer' is not in the list of allowed issuers");
}

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

}
Loading

0 comments on commit 45fe711

Please sign in to comment.