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

feat: Remote Secure Token Service #3569

Merged
merged 4 commits into from
Oct 30, 2023
Merged
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 @@ -13,7 +13,7 @@ dependencies {
implementation(project(":extensions:common:crypto:ldp-verifiable-credentials"))
implementation(project(":extensions:common:crypto:crypto-core"))
implementation(project(":extensions:common:iam:identity-trust:identity-trust-service"))
implementation(project(":extensions:common:iam:identity-trust:identity-trust-sts-embedded"))
implementation(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-embedded"))
implementation(libs.nimbus.jwt)

testImplementation(testFixtures(project(":spi:common:identity-trust-spi")))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ public Result<ClaimToken> validateToken(TokenRepresentation tokenRepresentation,
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");
Expand All @@ -70,9 +69,6 @@ public Result<ClaimToken> validateToken(TokenRepresentation tokenRepresentation,
if (!aud.contains(audience)) {
return failure("The aud claim expected to be %s but was %s".formatted(audience, aud));
}
if (!Objects.equals(clientId, iss)) {
return failure("The client_id must be equal to the issuer ID");
}
if (jti == null) {
return failure("The jti claim is mandatory.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,23 +87,7 @@ void audNotEqualToOwnDid() {
.isFailed()
.detail().isEqualTo("The aud claim expected to be %s but was [%s]".formatted(EXPECTED_OWN_DID, "invalid-audience"));
}

@Test
void clientIdClaim_NotEqualToConsumerDid() {
var claimsSet = new JWTClaimsSet.Builder()
.subject(CONSUMER_DID)
.issuer(CONSUMER_DID)
.audience(EXPECTED_OWN_DID)
.claim("jti", UUID.randomUUID().toString())
.claim("client_id", "invalid_client_id")
.expirationTime(new Date(new Date().getTime() + 60 * 1000))
.build();
var token = createJwt(claimsSet);
assertThat(validator.validateToken(token, EXPECTED_OWN_DID))
.isFailed()
.detail().isEqualTo("The client_id must be equal to the issuer ID");
}


@Test
void subJwkClaimPresent() {
var claimsSet = new JWTClaimsSet.Builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ plugins {
}

dependencies {
api(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-embedded"))
api(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-core"))
api(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-remote"))
api(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-remote-core"))
api(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-api"))
api(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-client-configuration"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ private StsClientTokenAdditionalParams additionalParams(StsTokenRequest request)
.audience(request.getAudience())
.accessToken(request.getAccessToken())
.bearerAccessScope(request.getBearerAccessScope())
.bearerAccessAlias(request.getBearerAccessAlias())
.build();
}

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

package org.eclipse.edc.connector.api.sts.model;

import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterStyle;
import jakarta.ws.rs.FormParam;

/**
Expand All @@ -25,23 +27,41 @@
* <li>clientSecret: Authorization secret for the client/</li>
* <li>audience: Audience according to the <a href="https://datatracker.ietf.org/doc/html/draft-tschofenig-oauth-audience-00#section-3">spec</a>.</li>
* <li>bearerAccessScope: Space-delimited scopes to be included in the access_token claim.</li>
* <li>bearerAccessAlias: Alias to be used in the sub of the VP access token (default is audience).</li>
* <li>accessToken: VP/VC Access Token to be included as access_token claim.</li>
* <li>grantType: Type of grant. Must be client_credentials.</li>
* </ul>
*/

public final class StsTokenRequest {


@Parameter(name = "grant_type", description = "Type of grant: must be set to client_credentials", required = true, style = ParameterStyle.FORM)
@FormParam("grant_type")
private String grantType;


@Parameter(name = "client_id", description = "Id of the client requesting an SI token", required = true, style = ParameterStyle.FORM)
@FormParam("client_id")
private String clientId;

@Parameter(name = "audience", description = "Audience for the SI token", required = true, style = ParameterStyle.FORM)
@FormParam("audience")
private String audience;

@Parameter(name = "bearer_access_scope", description = "Scope to be added in the VP access token", style = ParameterStyle.FORM)
@FormParam("bearer_access_scope")
private String bearerAccessScope;


@Parameter(name = "bearer_access_alias", description = "Alias to be use in the sub of the VP access token (default is audience)", style = ParameterStyle.FORM)
@FormParam("bearer_access_alias")
private String bearerAccessAlias;

@Parameter(name = "access_token", description = "VP access token to be added as a claim in the SI token", style = ParameterStyle.FORM)
@FormParam("access_token")
private String accessToken;

@Parameter(name = "client_secret", description = "Secret of the client requesting an SI token", required = true, style = ParameterStyle.FORM)
@FormParam("client_secret")
private String clientSecret;

Expand Down Expand Up @@ -69,6 +89,10 @@ public String getBearerAccessScope() {
return bearerAccessScope;
}

public String getBearerAccessAlias() {
return bearerAccessAlias;
}

public String getAccessToken() {
return accessToken;
}
Expand Down Expand Up @@ -105,6 +129,11 @@ public Builder bearerAccessScope(String bearerAccessScope) {
return this;
}

public Builder bearerAccessAlias(String bearerAccessAlias) {
this.request.bearerAccessAlias = bearerAccessAlias;
return this;
}

public Builder accessToken(String accessToken) {
this.request.accessToken = accessToken;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ dependencies {
api(project(":spi:common:transaction-spi"))
api(project(":spi:common:identity-trust-spi"))
api(project(":spi:common:identity-trust-sts-spi"))
implementation(project(":extensions:common:iam:identity-trust:identity-trust-sts-embedded"))
implementation(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-embedded"))
implementation(project(":core:common:jwt-core"))

testImplementation(testFixtures(project(":spi:common:identity-trust-sts-spi")))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,25 @@
import org.eclipse.edc.spi.result.ServiceResult;

import java.time.Clock;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
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.jwt.spi.JwtRegisteredClaimNames.AUDIENCE;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.CLIENT_ID;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUER;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.SUBJECT;

public class StsClientTokenGeneratorServiceImpl implements StsClientTokenGeneratorService {

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

private final long tokenExpiration;
private final StsTokenGenerationProvider tokenGenerationProvider;
private final Clock clock;
Expand All @@ -40,19 +50,25 @@ public StsClientTokenGeneratorServiceImpl(StsTokenGenerationProvider tokenGenera
this.tokenGenerationProvider = tokenGenerationProvider;
this.clock = clock;
this.tokenExpiration = tokenExpiration;

}

@Override
public ServiceResult<TokenRepresentation> tokenFor(StsClient client, StsClientTokenAdditionalParams additionalParams) {
var embeddedTokenGenerator = new EmbeddedSecureTokenService(tokenGenerationProvider.tokenGeneratorFor(client), clock, tokenExpiration);

var claims = Map.of(
var initialClaims = Map.of(
ISSUER, client.getId(),
SUBJECT, client.getId(),
AUDIENCE, additionalParams.getAudience(),
CLIENT_ID, client.getClientId());

var claims = CLAIM_MAPPERS.entrySet().stream()
.filter(entry -> entry.getValue().apply(additionalParams) != null)
.reduce(initialClaims, (accumulator, entity) ->
Optional.ofNullable(entity.getValue().apply(additionalParams))
.map(enrichClaimsWith(accumulator, entity.getKey()))
.orElse(accumulator), (a, b) -> b);

var tokenResult = embeddedTokenGenerator.createToken(claims, additionalParams.getBearerAccessScope())
.map(this::enrichWithExpiration);

Expand All @@ -70,4 +86,12 @@ private TokenRepresentation enrichWithExpiration(TokenRepresentation tokenRepres
.build();
}

private Function<String, Map<String, String>> enrichClaimsWith(Map<String, String> claims, String claim) {
return (claimValue) -> {
var newClaims = new HashMap<>(claims);
newClaims.put(claim, claimValue);
return Collections.unmodifiableMap(newClaims);
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@

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.jwt.spi.JwtRegisteredClaimNames.AUDIENCE;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.CLIENT_ID;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.EXPIRATION_TIME;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUED_AT;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUER;
Expand Down Expand Up @@ -84,6 +86,71 @@ void authenticateAndGenerateToken() throws Exception {

vault.storeSecret(privateKeyAlis, loadResourceFile("ec-privatekey.pem"));

var createResult = clientService.create(client);
assertThat(createResult.succeeded()).isTrue();

var tokenResult = tokenGeneratorService.tokenFor(client, additional);
var jwt = SignedJWT.parse(tokenResult.getContent().getToken());

assertThat(jwt.getJWTClaimsSet().getClaims())
.containsEntry(ISSUER, id)
.containsEntry(SUBJECT, id)
.containsEntry(AUDIENCE, List.of(audience))
.containsEntry(CLIENT_ID, clientId)
.containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT);

}

@Test
void authenticateAndGenerateToken_withBearerAccessScope() throws Exception {
var id = "id";
var clientId = "client_id";
var secretAlias = "client_id";
var privateKeyAlis = "client_id";
var audience = "aud";
var scope = "scope:test";
var client = createClientBuilder(id)
.clientId(clientId)
.privateKeyAlias(privateKeyAlis)
.secretAlias(secretAlias)
.build();

var additional = StsClientTokenAdditionalParams.Builder.newInstance().audience(audience).bearerAccessScope(scope).build();

vault.storeSecret(privateKeyAlis, loadResourceFile("ec-privatekey.pem"));

var createResult = clientService.create(client);
assertThat(createResult.succeeded()).isTrue();

var tokenResult = tokenGeneratorService.tokenFor(client, additional);
var jwt = SignedJWT.parse(tokenResult.getContent().getToken());

assertThat(jwt.getJWTClaimsSet().getClaims())
.containsEntry(ISSUER, id)
.containsEntry(SUBJECT, id)
.containsEntry(AUDIENCE, List.of(audience))
.containsEntry(CLIENT_ID, clientId)
.containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT, "access_token");

}

@Test
void authenticateAndGenerateToken_withAccessToken() throws Exception {
var id = "id";
var clientId = "client_id";
var secretAlias = "client_id";
var privateKeyAlis = "client_id";
var audience = "aud";
var accessToken = "tokenTest";
var client = createClientBuilder(id)
.clientId(clientId)
.privateKeyAlias(privateKeyAlis)
.secretAlias(secretAlias)
.build();

var additional = StsClientTokenAdditionalParams.Builder.newInstance().audience(audience).accessToken(accessToken).build();

vault.storeSecret(privateKeyAlis, loadResourceFile("ec-privatekey.pem"));

var createResult = clientService.create(client);
assertThat(createResult.succeeded()).isTrue();
Expand All @@ -95,7 +162,8 @@ void authenticateAndGenerateToken() throws Exception {
.containsEntry(ISSUER, id)
.containsEntry(SUBJECT, id)
.containsEntry(AUDIENCE, List.of(audience))
.containsEntry("client_id", clientId)
.containsEntry(CLIENT_ID, clientId)
.containsEntry(ACCESS_TOKEN, accessToken)
.containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT);

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ public class StsClientTokenGeneratorServiceImplTest {
private final StsTokenGenerationProvider tokenGenerationProvider = mock();
private final TokenGenerationService tokenGenerator = mock();
private StsClientTokenGeneratorServiceImpl clientTokenService;



@BeforeEach
void setup() {
clientTokenService = new StsClientTokenGeneratorServiceImpl(tokenGenerationProvider, Clock.systemUTC(), TOKEN_EXPIRATION);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Embedded Secure Token Service (STS) Extension

## Overview

This module implements the `SecureTokenService` spi, which will be used for generating the Self-Issued ID Token
in the `IATP` protocol flow. This is an embeddable implementation, which can be used in the same process of
the EDC control-plane runtime.

## Self-Issued ID Token Contents

As outlined in the [IATP](https://github.com/eclipse-tractusx/identity-trust/blob/main/specifications/M1/identity.protocol.base.md#41-self-issued-id-token-contents) spec
the token includes the following claims:

- The `iss` and `sub` claims MUST be equal and set to the bearer's (participant's) DID.
- The `sub_jwk` claim is not used
- The `aud` set to the `participant_id` of the relying party (RP)
- The `client_id` set to the `participant_id` of the consumer
- The `jti` claim that is used to mitigate against replay attacks
- The `exp` expiration time of the token
- The `access_token` VP Access Token (Optional)

Additionally, when generating the Self-Issued ID Token the `bearerAccessScope` parameter is passed the additional claim
`access_token` claim is added.

## VP Access Token format

The `IATP` protocol does not specify the format of the VP Access Token, which it's up to the specific STS implementation.
In this implementation the VP access token is still a JWT token with the following claims:

- The `iss` is the same of the SI token (participant's DID)
- The `sub` set to the `participant_id`/`alias (DID)` of the relying party (RP)
- The `aud` set to the `participant_id` of the participant
- The `jti` claim that is used to mitigate against replay attacks
- The `exp` expiration time of the token

`CredentialService` implementors, should verify that the `sub` of the Self-Issued ID token and the `sub` of
the VP access token matches.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ dependencies {
api(project(":spi:common:jwt-spi"))

implementation(project(":core:common:util"))
implementation(project(":extensions:common:iam:identity-trust:identity-trust-service"))
testImplementation(testFixtures(project(":spi:common:identity-trust-spi")))
testImplementation(project(":core:common:junit"))
testImplementation(project(":core:common:jwt-core"))
Expand Down
Loading
Loading