Skip to content

Commit

Permalink
feat: relying-party creates SI token for VP query (#3659)
Browse files Browse the repository at this point in the history
* feat: relying-party creates SI token for VP query

* add audience resolver - workaround

* fix compile error
  • Loading branch information
paullatzelsperger authored Nov 26, 2023
1 parent 1cf48c2 commit a3ef0f1
Show file tree
Hide file tree
Showing 16 changed files with 109 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import com.apicatalog.ld.signature.VerificationError;
import com.apicatalog.ld.signature.VerificationError.Code;
import com.apicatalog.ld.signature.key.VerificationKey;
import com.apicatalog.ld.signature.method.DidUrlMethodResolver;
import com.apicatalog.ld.signature.method.HttpMethodResolver;
import com.apicatalog.ld.signature.method.MethodResolver;
import com.apicatalog.ld.signature.method.VerificationMethod;
Expand Down Expand Up @@ -67,7 +66,7 @@ public class LdpVerifier implements CredentialVerifier {
private ObjectMapper jsonLdMapper;
private SignatureSuiteProvider suiteProvider;
private Map<String, Object> params;
private Collection<MethodResolver> methodResolvers = new ArrayList<>(List.of(new DidUrlMethodResolver(), new HttpMethodResolver()));
private Collection<MethodResolver> methodResolvers = new ArrayList<>(List.of(new HttpMethodResolver()));
private DocumentLoader loader;
private URI base;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.eclipse.edc.iam.identitytrust.core.defaults.DefaultCredentialServiceClient;
import org.eclipse.edc.iam.identitytrust.validation.SelfIssuedIdTokenValidator;
import org.eclipse.edc.iam.identitytrust.verification.MultiFormatPresentationVerifier;
import org.eclipse.edc.identitytrust.AudienceResolver;
import org.eclipse.edc.identitytrust.CredentialServiceClient;
import org.eclipse.edc.identitytrust.SecureTokenService;
import org.eclipse.edc.identitytrust.TrustedIssuerRegistry;
Expand Down Expand Up @@ -60,10 +61,6 @@ public class IdentityAndTrustExtension implements ServiceExtension {
@Inject
private SecureTokenService secureTokenService;


@Inject
private CredentialServiceClient credentialServiceClient;

@Inject
private TrustedIssuerRegistry registry;

Expand Down Expand Up @@ -94,12 +91,15 @@ public class IdentityAndTrustExtension implements ServiceExtension {
private JwtValidator jwtValidator;
private JwtVerifier jwtVerifier;
private PresentationVerifier presentationVerifier;
private CredentialServiceClient credentialServiceClient;
@Inject
private AudienceResolver audienceResolver;

@Provider
public IdentityService createIdentityService(ServiceExtensionContext context) {
var credentialServiceUrlResolver = new DidCredentialServiceUrlResolver(didResolverRegistry);
return new IdentityAndTrustService(secureTokenService, getOwnDid(context), context.getParticipantId(), getPresentationVerifier(context),
getCredentialServiceClient(context), getJwtValidator(), getJwtVerifier(), registry, clock, credentialServiceUrlResolver);
getCredentialServiceClient(context), getJwtValidator(), getJwtVerifier(), registry, clock, credentialServiceUrlResolver, audienceResolver);
}

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

package org.eclipse.edc.iam.identitytrust;

import com.nimbusds.jwt.SignedJWT;
import org.eclipse.edc.iam.identitytrust.validation.rules.HasValidIssuer;
import org.eclipse.edc.iam.identitytrust.validation.rules.HasValidSubjectIds;
import org.eclipse.edc.iam.identitytrust.validation.rules.IsNotExpired;
import org.eclipse.edc.iam.identitytrust.validation.rules.IsRevoked;
import org.eclipse.edc.identitytrust.AudienceResolver;
import org.eclipse.edc.identitytrust.CredentialServiceClient;
import org.eclipse.edc.identitytrust.CredentialServiceUrlResolver;
import org.eclipse.edc.identitytrust.SecureTokenService;
Expand All @@ -35,22 +37,30 @@
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.util.string.StringUtils;

import java.text.ParseException;
import java.time.Clock;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.nimbusds.jwt.JWTClaimNames.AUDIENCE;
import static com.nimbusds.jwt.JWTClaimNames.EXPIRATION_TIME;
import static com.nimbusds.jwt.JWTClaimNames.ISSUED_AT;
import static com.nimbusds.jwt.JWTClaimNames.ISSUER;
import static com.nimbusds.jwt.JWTClaimNames.SUBJECT;
import static org.eclipse.edc.identitytrust.SelfIssuedTokenConstants.PRESENTATION_ACCESS_TOKEN_CLAIM;
import static org.eclipse.edc.spi.result.Result.failure;
import static org.eclipse.edc.spi.result.Result.success;

/**
* Implements an {@link IdentityService}, that:
* <ul>
* <li>Obtains an SI token from a SecureTokenService</li>
* <li>Establishes proof-of-original possession, by extracting the access_token, and re-packaging it into a new SI token</li>
* <li>Establishes proof-of-original possession, by extracting the PRESENTATION_ACCESS_TOKEN_CLAIM, and re-packaging it into a new SI token</li>
* <li>Performs a presentation request against a CredentialService</li>
* <li>Validates and verifies the VerifiablePresentation</li>
* </ul>
Expand All @@ -59,7 +69,6 @@
*/
public class IdentityAndTrustService implements IdentityService {
private static final String SCOPE_STRING_REGEX = "(.+):(.+):(read|write|\\*)";

private final SecureTokenService secureTokenService;
private final String myOwnDid;
private final String participantId;
Expand All @@ -70,6 +79,7 @@ public class IdentityAndTrustService implements IdentityService {
private final TrustedIssuerRegistry trustedIssuerRegistry;
private final Clock clock;
private final CredentialServiceUrlResolver credentialServiceUrlResolver;
private final AudienceResolver audienceMapper;

/**
* Constructs a new instance of the {@link IdentityAndTrustService}.
Expand All @@ -79,7 +89,7 @@ public class IdentityAndTrustService implements IdentityService {
*/
public IdentityAndTrustService(SecureTokenService secureTokenService, String myOwnDid, String participantId,
PresentationVerifier presentationVerifier, CredentialServiceClient credentialServiceClient,
JwtValidator jwtValidator, JwtVerifier jwtVerifier, TrustedIssuerRegistry trustedIssuerRegistry, Clock clock, CredentialServiceUrlResolver csUrlResolver) {
JwtValidator jwtValidator, JwtVerifier jwtVerifier, TrustedIssuerRegistry trustedIssuerRegistry, Clock clock, CredentialServiceUrlResolver csUrlResolver, AudienceResolver audienceMapper) {
this.secureTokenService = secureTokenService;
this.myOwnDid = myOwnDid;
this.participantId = participantId;
Expand All @@ -90,10 +100,18 @@ public IdentityAndTrustService(SecureTokenService secureTokenService, String myO
this.trustedIssuerRegistry = trustedIssuerRegistry;
this.clock = clock;
this.credentialServiceUrlResolver = csUrlResolver;
this.audienceMapper = audienceMapper;
}

@Override
public Result<TokenRepresentation> obtainClientCredentials(TokenParameters parameters) {
var newAud = audienceMapper.resolve(parameters.getAudience());
parameters = TokenParameters.Builder.newInstance()
.audience(newAud)
.scope(parameters.getScope())
.additional(parameters.getAdditional())
.build();

var scope = parameters.getScope();
var scopeValidationResult = validateScope(scope);

Expand All @@ -118,19 +136,45 @@ public Result<TokenRepresentation> obtainClientCredentials(TokenParameters param
public Result<ClaimToken> verifyJwtToken(TokenRepresentation tokenRepresentation, String audience) {

// verify and validate incoming SI Token
var issuerResult = jwtVerifier.verify(tokenRepresentation.getToken(), audience)
.compose(v -> jwtValidator.validateToken(tokenRepresentation, audience))
.compose(claimToken -> success(claimToken.getStringClaim(ISSUER)));
var claimTokenResult = jwtVerifier.verify(tokenRepresentation.getToken(), participantId)
.compose(v -> jwtValidator.validateToken(tokenRepresentation, participantId)) // audience must be set to my own participant ID
.compose(Result::success);

if (issuerResult.failed()) {
return issuerResult.mapTo();
if (claimTokenResult.failed()) {
return claimTokenResult.mapTo();
}

// create our own SI token, to request the VPs
var claimToken = claimTokenResult.getContent();
var accessToken = claimToken.getStringClaim(PRESENTATION_ACCESS_TOKEN_CLAIM);
var issuer = claimToken.getStringClaim(ISSUER);
var intendedAudience = claimToken.getStringClaim("client_id");

/* TODO: DEMO the scopes should be extracted elsewhere. replace this section!!############################*/
var scopes = new ArrayList<String>();
try {
var scope = SignedJWT.parse(accessToken).getJWTClaimsSet().getStringClaim("scope");
scopes.add(scope);
} catch (ParseException e) {
throw new RuntimeException(e);
}
/* TODO: END DEMO ########################################################################################*/

var siTokenClaims = Map.of(PRESENTATION_ACCESS_TOKEN_CLAIM, accessToken,
ISSUED_AT, Instant.now().toString(),
AUDIENCE, intendedAudience,
ISSUER, myOwnDid,
SUBJECT, myOwnDid,
EXPIRATION_TIME, Instant.now().plus(5, ChronoUnit.MINUTES).toString());
var siToken = secureTokenService.createToken(siTokenClaims, null);
if (siToken.failed()) {
return siToken.mapTo();
}
var siTokenString = siToken.getContent().getToken();

// get CS Url, execute VP request
var issuer = issuerResult.getContent(); // the issuer is a DID
var vpResponse = credentialServiceUrlResolver.resolve(issuer)
.compose(url -> credentialServiceClient.requestPresentation(url, null, null));
.compose(url -> credentialServiceClient.requestPresentation(url, siTokenString, scopes));

if (vpResponse.failed()) {
return vpResponse.mapTo();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.Objects;

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

Expand Down Expand Up @@ -78,6 +79,9 @@ public Result<ClaimToken> validateToken(TokenRepresentation tokenRepresentation,
if (exp.toInstant().plusSeconds(EPSILON).isBefore(now())) {
return failure("The token must not be expired.");
}
if (claims.getClaim(PRESENTATION_ACCESS_TOKEN_CLAIM) == null) {
return failure("The 'access_token' claim is mandatory.");
}
var bldr = ClaimToken.Builder.newInstance();
jwt.getJWTClaimsSet().getClaims().forEach(bldr::claim);
return success(bldr.build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package org.eclipse.edc.iam.identitytrust.service;


import com.nimbusds.jwt.JWTClaimsSet;
import org.eclipse.edc.iam.identitytrust.IdentityAndTrustService;
import org.eclipse.edc.identitytrust.CredentialServiceClient;
import org.eclipse.edc.identitytrust.CredentialServiceUrlResolver;
Expand All @@ -29,6 +30,7 @@
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.iam.TokenRepresentation;
import org.eclipse.edc.spi.result.Result;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
Expand All @@ -45,6 +47,7 @@
import java.util.List;
import java.util.Map;

import static org.eclipse.edc.identitytrust.SelfIssuedTokenConstants.PRESENTATION_ACCESS_TOKEN_CLAIM;
import static org.eclipse.edc.identitytrust.TestFunctions.createCredentialBuilder;
import static org.eclipse.edc.identitytrust.TestFunctions.createJwt;
import static org.eclipse.edc.identitytrust.TestFunctions.createPresentationBuilder;
Expand Down Expand Up @@ -72,15 +75,20 @@ class IdentityAndTrustServiceTest {
private final JwtValidator jwtValidatorMock = mock();
private final JwtVerifier jwtVerfierMock = mock();
private final TrustedIssuerRegistry trustedIssuerRegistryMock = mock();
private final CredentialServiceUrlResolver resolverMock = mock();
private final CredentialServiceUrlResolver credentialServiceUrlResolverMock = mock();
private final IdentityAndTrustService service = new IdentityAndTrustService(mockedSts, EXPECTED_OWN_DID, EXPECTED_PARTICIPANT_ID, mockedVerifier, mockedClient,
jwtValidatorMock, jwtVerfierMock, trustedIssuerRegistryMock, Clock.systemUTC(), resolverMock);
jwtValidatorMock, jwtVerfierMock, trustedIssuerRegistryMock, Clock.systemUTC(), credentialServiceUrlResolverMock, i -> i);

@BeforeEach
void setup() {
when(resolverMock.resolve(any())).thenReturn(success("foobar"));
when(jwtValidatorMock.validateToken(any(), any())).thenReturn(success(ClaimToken.Builder.newInstance().claim("iss", CONSUMER_DID).build()));
when(credentialServiceUrlResolverMock.resolve(any())).thenReturn(success("foobar"));
var jwt = createJwt(new JWTClaimsSet.Builder().claim("scope", "foo-scope").build());
when(jwtValidatorMock.validateToken(any(), any())).thenReturn(success(ClaimToken.Builder.newInstance()
.claim("iss", CONSUMER_DID)
.claim("client_id", "sender-id")
.claim(PRESENTATION_ACCESS_TOKEN_CLAIM, jwt.getToken()).build()));
when(jwtVerfierMock.verify(any(), any())).thenReturn(success());
when(mockedSts.createToken(any(), any())).thenReturn(success(TokenRepresentation.Builder.newInstance().build()));
}

@Nested
Expand Down Expand Up @@ -242,7 +250,7 @@ void jwtTokenNotVerified() {

@Test
void cannotResolveCredentialServiceUrl() {
when(resolverMock.resolve(any())).thenReturn(Result.failure("test-failure"));
when(credentialServiceUrlResolverMock.resolve(any())).thenReturn(Result.failure("test-failure"));
assertThat(service.verifyJwtToken(createJwt(), "test-audience"))
.isFailed()
.detail()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.Date;
import java.util.UUID;

import static org.eclipse.edc.identitytrust.SelfIssuedTokenConstants.PRESENTATION_ACCESS_TOKEN_CLAIM;
import static org.eclipse.edc.identitytrust.TestFunctions.createJwt;
import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat;

Expand All @@ -43,6 +44,7 @@ void success() {
.issuer(CONSUMER_DID)
.audience(EXPECTED_OWN_DID)
.claim("jti", UUID.randomUUID().toString())
.claim(PRESENTATION_ACCESS_TOKEN_CLAIM, "foobar")
.claim("client_id", CONSUMER_DID)
.expirationTime(new Date(new Date().getTime() + 60 * 1000))
.build();
Expand Down Expand Up @@ -87,7 +89,7 @@ void audNotEqualToOwnDid() {
.isFailed()
.detail().isEqualTo("The aud claim expected to be %s but was [%s]".formatted(EXPECTED_OWN_DID, "invalid-audience"));
}

@Test
void subJwkClaimPresent() {
var claimsSet = new JWTClaimsSet.Builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
import java.util.Optional;
import java.util.function.Function;

import static org.eclipse.edc.identitytrust.SelfIssuedTokenConstants.ACCESS_TOKEN;
import static org.eclipse.edc.identitytrust.SelfIssuedTokenConstants.BEARER_ACCESS_ALIAS;
import static org.eclipse.edc.identitytrust.SelfIssuedTokenConstants.PRESENTATION_ACCESS_TOKEN_CLAIM;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.AUDIENCE;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.CLIENT_ID;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUER;
Expand All @@ -39,7 +39,7 @@
public class StsClientTokenGeneratorServiceImpl implements StsClientTokenGeneratorService {

private static final Map<String, Function<StsClientTokenAdditionalParams, String>> CLAIM_MAPPERS = Map.of(
ACCESS_TOKEN, StsClientTokenAdditionalParams::getAccessToken,
PRESENTATION_ACCESS_TOKEN_CLAIM, StsClientTokenAdditionalParams::getAccessToken,
BEARER_ACCESS_ALIAS, StsClientTokenAdditionalParams::getBearerAccessAlias);

private final long tokenExpiration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.edc.iam.identitytrust.sts.store.fixtures.TestFunctions.createClientBuilder;
import static org.eclipse.edc.identitytrust.SelfIssuedTokenConstants.ACCESS_TOKEN;
import static org.eclipse.edc.identitytrust.SelfIssuedTokenConstants.PRESENTATION_ACCESS_TOKEN_CLAIM;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.AUDIENCE;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.CLIENT_ID;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.EXPIRATION_TIME;
Expand Down Expand Up @@ -163,7 +163,7 @@ void authenticateAndGenerateToken_withAccessToken() throws Exception {
.containsEntry(SUBJECT, id)
.containsEntry(AUDIENCE, List.of(audience))
.containsEntry(CLIENT_ID, clientId)
.containsEntry(ACCESS_TOKEN, accessToken)
.containsEntry(PRESENTATION_ACCESS_TOKEN_CLAIM, accessToken)
.containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT);

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@

import static java.lang.String.format;
import static java.util.Optional.ofNullable;
import static org.eclipse.edc.identitytrust.SelfIssuedTokenConstants.ACCESS_TOKEN;
import static org.eclipse.edc.identitytrust.SelfIssuedTokenConstants.BEARER_ACCESS_ALIAS;
import static org.eclipse.edc.identitytrust.SelfIssuedTokenConstants.PRESENTATION_ACCESS_TOKEN_CLAIM;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.AUDIENCE;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUER;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.SCOPE;
Expand Down Expand Up @@ -69,7 +69,7 @@ public Result<TokenRepresentation> createToken(Map<String, String> claims, @Null
private Result<Void> createAndAcceptAccessToken(Map<String, String> claims, String scope, BiConsumer<String, String> consumer) {
return createAccessToken(claims, scope)
.compose(tokenRepresentation -> success(tokenRepresentation.getToken()))
.onSuccess(withClaim(ACCESS_TOKEN, consumer))
.onSuccess(withClaim(PRESENTATION_ACCESS_TOKEN_CLAIM, consumer))
.mapTo();
}

Expand Down
Loading

0 comments on commit a3ef0f1

Please sign in to comment.