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 6e7a582 commit b3e225e
Show file tree
Hide file tree
Showing 18 changed files with 269 additions and 188 deletions.
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 @@ -27,7 +27,7 @@
* <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>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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,19 @@
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 {

public static final String ACCESS_TOKEN_CLAIM = "access_token";

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 @@ -46,7 +50,6 @@ public StsClientTokenGeneratorServiceImpl(StsTokenGenerationProvider tokenGenera
this.tokenGenerationProvider = tokenGenerationProvider;
this.clock = clock;
this.tokenExpiration = tokenExpiration;

}

@Override
Expand All @@ -59,9 +62,12 @@ public ServiceResult<TokenRepresentation> tokenFor(StsClient client, StsClientTo
AUDIENCE, additionalParams.getAudience(),
CLIENT_ID, client.getClientId());

var claims = Optional.ofNullable(additionalParams.getAccessToken())
.map(enrichClaims(initialClaims))
.orElse(initialClaims);
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 @@ -80,10 +86,10 @@ private TokenRepresentation enrichWithExpiration(TokenRepresentation tokenRepres
.build();
}

private Function<String, Map<String, String>> enrichClaims(Map<String, String> claims) {
return (token) -> {
private Function<String, Map<String, String>> enrichClaimsWith(Map<String, String> claims, String claim) {
return (claimValue) -> {
var newClaims = new HashMap<>(claims);
newClaims.put(ACCESS_TOKEN_CLAIM, token);
newClaims.put(claim, claimValue);
return Collections.unmodifiableMap(newClaims);
};
}
Expand Down
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 @@ -94,7 +96,7 @@ void authenticateAndGenerateToken() throws Exception {
.containsEntry(ISSUER, id)
.containsEntry(SUBJECT, id)
.containsEntry(AUDIENCE, List.of(audience))
.containsEntry("client_id", clientId)
.containsEntry(CLIENT_ID, clientId)
.containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT);

}
Expand Down Expand Up @@ -127,12 +129,11 @@ void authenticateAndGenerateToken_withBearerAccessScope() throws Exception {
.containsEntry(ISSUER, id)
.containsEntry(SUBJECT, id)
.containsEntry(AUDIENCE, List.of(audience))
.containsEntry("client_id", clientId)
.containsEntry(CLIENT_ID, clientId)
.containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT, "access_token");

}


@Test
void authenticateAndGenerateToken_withAccessToken() throws Exception {
var id = "id";
Expand Down Expand Up @@ -161,13 +162,12 @@ void authenticateAndGenerateToken_withAccessToken() throws Exception {
.containsEntry(ISSUER, id)
.containsEntry(SUBJECT, id)
.containsEntry(AUDIENCE, List.of(audience))
.containsEntry("client_id", clientId)
.containsEntry("access_token", accessToken)
.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
@@ -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 @@ -30,8 +30,11 @@

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.jwt.spi.JwtRegisteredClaimNames.AUDIENCE;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUER;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.SCOPE;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.SUBJECT;
import static org.eclipse.edc.spi.result.Result.failure;
import static org.eclipse.edc.spi.result.Result.success;
Expand All @@ -43,9 +46,6 @@
*/
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 All @@ -69,16 +69,16 @@ 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_CLAIM, consumer))
.onSuccess(withClaim(ACCESS_TOKEN, consumer))
.mapTo();
}

private Result<TokenRepresentation> createAccessToken(Map<String, String> claims, String bearerAccessScope) {
var accessTokenClaims = new HashMap<>(accessTokenInheritedClaims(claims));
accessTokenClaims.put(SCOPE_CLAIM, bearerAccessScope);
accessTokenClaims.put(SCOPE, 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 -> addOptionalClaim(claims, BEARER_ACCESS_ALIAS, withClaim(SUBJECT, accessTokenClaims::put)))
.compose(v -> tokenGenerationService.generate(new SelfIssuedTokenDecorator(accessTokenClaims, clock, validity)));

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@
import static org.assertj.core.api.Assertions.as;
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.identitytrust.SelfIssuedTokenConstants.ACCESS_TOKEN;
import static org.eclipse.edc.identitytrust.SelfIssuedTokenConstants.BEARER_ACCESS_ALIAS;
import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.AUDIENCE;
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;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.JWT_ID;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.SCOPE;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.SUBJECT;


Expand Down Expand Up @@ -83,7 +83,7 @@ void createToken_withoutBearerAccessScope() {
assertThat(jwt.getJWTClaimsSet().getClaims())
.containsEntry(ISSUER, issuer)
.containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT)
.doesNotContainKey(ACCESS_TOKEN_CLAIM);
.doesNotContainKey(ACCESS_TOKEN);
});

}
Expand All @@ -104,15 +104,15 @@ void createToken_withBearerAccessScope() {
assertThat(jwt.getJWTClaimsSet().getClaims())
.containsEntry(ISSUER, issuer)
.containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT)
.extractingByKey(ACCESS_TOKEN_CLAIM, as(STRING))
.extractingByKey(ACCESS_TOKEN, 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, audience)
.containsEntry(AUDIENCE, List.of(issuer))
.containsEntry(SCOPE_CLAIM, scopes)
.containsEntry(SCOPE, scopes)
.containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT);
});
});
Expand All @@ -124,7 +124,7 @@ void createToken_withBearerAccessAlias() {
var issuer = "testIssuer";
var audience = "audience";
var bearerAccessAlias = "alias";
var claims = Map.of(ISSUER, issuer, AUDIENCE, audience, BEARER_ACCESS_ALIAS_CLAIM, bearerAccessAlias);
var claims = Map.of(ISSUER, issuer, AUDIENCE, audience, BEARER_ACCESS_ALIAS, bearerAccessAlias);
var tokenResult = secureTokenService.createToken(claims, scopes);

assertThat(tokenResult).isSucceeded()
Expand All @@ -135,21 +135,20 @@ void createToken_withBearerAccessAlias() {
assertThat(jwt.getJWTClaimsSet().getClaims())
.containsEntry(ISSUER, issuer)
.containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT)
.extractingByKey(ACCESS_TOKEN_CLAIM, as(STRING))
.extractingByKey(ACCESS_TOKEN, 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)
.containsEntry(SCOPE, scopes)
.containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT);
});
});
}



@ParameterizedTest
@ArgumentsSource(ClaimsArguments.class)
void createToken_shouldFail_withMissingClaims(Map<String, String> claims) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ public class StsRemoteClientConfigurationExtension implements ServiceExtension {
public static final String CLIENT_SECRET_ALIAS = "edc.iam.sts.oauth.client.secret.alias";

protected static final String NAME = "Sts remote client configuration extension";



@Inject
private Vault vault;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,9 @@
*/
@Extension(StsRemoteClientExtension.NAME)
public class StsRemoteClientExtension implements ServiceExtension {



protected static final String NAME = "Sts remote client configuration extension";


@Inject
private StsRemoteClientConfiguration clientConfiguration;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,21 @@
import java.util.Map;
import java.util.stream.Collectors;

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.BEARER_ACCESS_SCOPE;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.AUDIENCE;

public class RemoteSecureTokenService implements SecureTokenService {

public static final String GRANT_TYPE = "client_credentials";
public static final String ACCESS_TOKEN_PARAM = "access_token";
public static final String AUDIENCE_PARAM = "audience";
public static final String BEARER_ACCESS_ALIAS_PARAM = "bearer_access_alias";
public static final String BEARER_ACCESS_SCOPE_PARAM = "bearer_access_scope";

private static final Map<String, String> CLAIM_MAPPING = Map.of(
AUDIENCE, AUDIENCE_PARAM,
BEARER_ACCESS_ALIAS_PARAM, BEARER_ACCESS_ALIAS_PARAM,
ACCESS_TOKEN_PARAM, ACCESS_TOKEN_PARAM);
BEARER_ACCESS_ALIAS, BEARER_ACCESS_ALIAS,
ACCESS_TOKEN, ACCESS_TOKEN);

private final Oauth2Client oauth2Client;
private final StsRemoteClientConfiguration configuration;

Expand All @@ -59,18 +61,14 @@ private Oauth2CredentialsRequest createRequest(Map<String, String> claims, @Null
.clientId(configuration.getClientId())
.clientSecret(configuration.getClientSecret())
.grantType(GRANT_TYPE);

if (configuration.getScope() != null) {
builder.scope(configuration.getScope());
}


var additionalParams = claims.entrySet().stream()
.filter(entry -> CLAIM_MAPPING.containsKey(entry.getKey()))
.map(entry -> Map.entry(CLAIM_MAPPING.get(entry.getKey()), entry.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

if (bearerAccessScope != null) {
additionalParams.put(BEARER_ACCESS_SCOPE_PARAM, bearerAccessScope);
additionalParams.put(BEARER_ACCESS_SCOPE, bearerAccessScope);
}

builder.params(additionalParams);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,10 @@
* Configuration of the OAuth2 client
*/
public class StsRemoteClientConfiguration {

private String tokenUrl;
private String clientId;

private String clientSecret;
private String scope;

public String getScope() {
return scope;
}

public String getClientId() {
return clientId;
Expand Down Expand Up @@ -62,11 +57,6 @@ public Builder clientId(String clientId) {
return this;
}

public Builder scope(String scope) {
configuration.scope = scope;
return this;
}

public Builder clientSecret(String clientSecret) {
configuration.clientSecret = clientSecret;
return this;
Expand Down

0 comments on commit b3e225e

Please sign in to comment.