Skip to content

Commit

Permalink
feat: Secure Token Service remote implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
wolf4ood committed Oct 27, 2023
1 parent 6bee095 commit 6e7a582
Show file tree
Hide file tree
Showing 24 changed files with 1,115 additions and 55 deletions.
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 @@ -20,6 +20,8 @@ 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 @@ -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 use 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 @@ -23,7 +23,11 @@
import org.eclipse.edc.spi.iam.TokenRepresentation;

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.jwt.spi.JwtRegisteredClaimNames.AUDIENCE;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.CLIENT_ID;
Expand All @@ -32,6 +36,8 @@

public class StsClientTokenGeneratorServiceImpl implements StsClientTokenGeneratorService {

public static final String ACCESS_TOKEN_CLAIM = "access_token";

private final long tokenExpiration;
private final StsTokenGenerationProvider tokenGenerationProvider;
private final Clock clock;
Expand All @@ -47,12 +53,16 @@ public StsClientTokenGeneratorServiceImpl(StsTokenGenerationProvider tokenGenera
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 = Optional.ofNullable(additionalParams.getAccessToken())
.map(enrichClaims(initialClaims))
.orElse(initialClaims);

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

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

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,72 @@ 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 @@ -96,10 +162,12 @@ void authenticateAndGenerateToken() throws Exception {
.containsEntry(SUBJECT, id)
.containsEntry(AUDIENCE, List.of(audience))
.containsEntry("client_id", clientId)
.containsEntry("access_token", accessToken)
.containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT);

}


/**
* Load content from a resource file.
*/
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class EmbeddedSecureTokenService implements SecureTokenService {

public static final String SCOPE_CLAIM = "scope";
public static final String ACCESS_TOKEN_CLAIM = "access_token";
public static final String BEARER_ACCESS_ALIAS_CLAIM = "bearer_access_alias";
private static final List<String> ACCESS_TOKEN_INHERITED_CLAIMS = List.of(ISSUER);
private final TokenGenerationService tokenGenerationService;
private final Clock clock;
Expand Down Expand Up @@ -77,6 +78,7 @@ private Result<TokenRepresentation> createAccessToken(Map<String, String> claims
accessTokenClaims.put(SCOPE_CLAIM, bearerAccessScope);
return addClaim(claims, ISSUER, withClaim(AUDIENCE, accessTokenClaims::put))
.compose(v -> addClaim(claims, AUDIENCE, withClaim(SUBJECT, accessTokenClaims::put)))
.compose(v -> addOptionalClaim(claims, BEARER_ACCESS_ALIAS_CLAIM, withClaim(SUBJECT, accessTokenClaims::put)))
.compose(v -> tokenGenerationService.generate(new SelfIssuedTokenDecorator(accessTokenClaims, clock, validity)));

}
Expand All @@ -91,6 +93,11 @@ private Result<Void> addClaim(Map<String, String> claims, String claim, Consumer
}
}

private Result<Void> addOptionalClaim(Map<String, String> claims, String claim, Consumer<String> consumer) {
addClaim(claims, claim, consumer);
return Result.success();
}

private Consumer<String> withClaim(String key, BiConsumer<String, String> consumer) {
return (value) -> consumer.accept(key, value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.STRING;
import static org.eclipse.edc.iam.identitytrust.sts.embedded.EmbeddedSecureTokenService.ACCESS_TOKEN_CLAIM;
import static org.eclipse.edc.iam.identitytrust.sts.embedded.EmbeddedSecureTokenService.BEARER_ACCESS_ALIAS_CLAIM;
import static org.eclipse.edc.iam.identitytrust.sts.embedded.EmbeddedSecureTokenService.SCOPE_CLAIM;
import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.AUDIENCE;
Expand Down Expand Up @@ -117,6 +118,37 @@ void createToken_withBearerAccessScope() {
});
}

@Test
void createToken_withBearerAccessAlias() {
var scopes = "email:read";
var issuer = "testIssuer";
var audience = "audience";
var bearerAccessAlias = "alias";
var claims = Map.of(ISSUER, issuer, AUDIENCE, audience, BEARER_ACCESS_ALIAS_CLAIM, bearerAccessAlias);
var tokenResult = secureTokenService.createToken(claims, scopes);

assertThat(tokenResult).isSucceeded()
.satisfies(tokenRepresentation -> {
var jwt = SignedJWT.parse(tokenRepresentation.getToken());
assertThat(jwt.verify(createVerifier(jwt.getHeader(), keyPair.getPublic()))).isTrue();

assertThat(jwt.getJWTClaimsSet().getClaims())
.containsEntry(ISSUER, issuer)
.containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT)
.extractingByKey(ACCESS_TOKEN_CLAIM, as(STRING))
.satisfies(accessToken -> {
var accessTokenJwt = SignedJWT.parse(accessToken);
assertThat(accessTokenJwt.verify(createVerifier(accessTokenJwt.getHeader(), keyPair.getPublic()))).isTrue();
assertThat(accessTokenJwt.getJWTClaimsSet().getClaims())
.containsEntry(ISSUER, issuer)
.containsEntry(SUBJECT, bearerAccessAlias)
.containsEntry(AUDIENCE, List.of(issuer))
.containsEntry(SCOPE_CLAIM, scopes)
.containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT);
});
});
}


@ParameterizedTest
@ArgumentsSource(ClaimsArguments.class)
Expand All @@ -133,7 +165,7 @@ private JWSVerifier createVerifier(JWSHeader header, Key publicKey) throws JOSEE
private static class ClaimsArguments implements ArgumentsProvider {

@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception {
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
return Stream.of(Map.of(ISSUER, "iss"), Map.of(AUDIENCE, "aud")).map(Arguments::of);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
plugins {
`java-library`
`maven-publish`
}

dependencies {
api(project(":spi:common:identity-trust-spi"))
api(project(":spi:common:oauth2-spi"))
api(project(":spi:common:jwt-spi"))
implementation(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-remote"))

testImplementation(project(":core:common:junit"))
}

Loading

0 comments on commit 6e7a582

Please sign in to comment.