Skip to content

Commit 17c1c69

Browse files
feat: add Participant-aware STS (#614)
1 parent 45da32c commit 17c1c69

File tree

31 files changed

+887
-65
lines changed

31 files changed

+887
-65
lines changed

core/common-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public class DefaultServicesExtension implements ServiceExtension {
7575
public static final String JWS_2020_JSON = "jws2020.json";
7676
public static final String CREDENTIALS_V_1_JSON = "credentials.v1.json";
7777

78-
78+
7979
public static final String NAME = "IdentityHub Default Services Extension";
8080
public static final long DEFAULT_REVOCATION_CACHE_VALIDITY_MILLIS = 15 * 60 * 1000L;
8181
static final String ACCESSTOKEN_JTI_VALIDATION_ACTIVATE = "edc.iam.accesstoken.jti.validation";

core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import org.eclipse.edc.http.spi.EdcHttpClient;
1919
import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver;
2020
import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry;
21-
import org.eclipse.edc.iam.identitytrust.spi.SecureTokenService;
2221
import org.eclipse.edc.iam.identitytrust.spi.verification.SignatureSuiteRegistry;
2322
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat;
2423
import org.eclipse.edc.iam.verifiablecredentials.spi.model.RevocationServiceRegistry;
@@ -33,6 +32,7 @@
3332
import org.eclipse.edc.identityhub.core.services.verifiablepresentation.generators.LdpPresentationGenerator;
3433
import org.eclipse.edc.identityhub.core.services.verification.SelfIssuedTokenVerifierImpl;
3534
import org.eclipse.edc.identityhub.publickey.KeyPairResourcePublicKeyResolver;
35+
import org.eclipse.edc.identityhub.spi.authentication.ParticipantSecureTokenService;
3636
import org.eclipse.edc.identityhub.spi.credential.request.store.HolderCredentialRequestStore;
3737
import org.eclipse.edc.identityhub.spi.keypair.KeyPairService;
3838
import org.eclipse.edc.identityhub.spi.keypair.store.KeyPairResourceStore;
@@ -134,7 +134,7 @@ public class CoreServicesExtension implements ServiceExtension {
134134
@Inject
135135
private HolderCredentialRequestStore credentialRequestStore;
136136
@Inject
137-
private SecureTokenService secureTokenService;
137+
private ParticipantSecureTokenService secureTokenService;
138138
@Inject
139139
private EdcHttpClient httpClient;
140140

core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/services/verifiablecredential/CredentialRequestManagerImpl.java

+9-8
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
import okhttp3.Response;
2222
import org.eclipse.edc.http.spi.EdcHttpClient;
2323
import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry;
24-
import org.eclipse.edc.iam.identitytrust.spi.SecureTokenService;
2524
import org.eclipse.edc.identityhub.protocols.dcp.spi.model.CredentialRequest;
2625
import org.eclipse.edc.identityhub.protocols.dcp.spi.model.CredentialRequestMessage;
26+
import org.eclipse.edc.identityhub.spi.authentication.ParticipantSecureTokenService;
2727
import org.eclipse.edc.identityhub.spi.credential.request.model.HolderCredentialRequest;
2828
import org.eclipse.edc.identityhub.spi.credential.request.model.HolderRequestState;
2929
import org.eclipse.edc.identityhub.spi.credential.request.store.HolderCredentialRequestStore;
@@ -68,7 +68,7 @@ public class CredentialRequestManagerImpl extends AbstractStateEntityManager<Hol
6868
private DidResolverRegistry didResolverRegistry;
6969
private TypeTransformerRegistry dcpTypeTransformerRegistry;
7070
private EdcHttpClient httpClient;
71-
private SecureTokenService secureTokenService;
71+
private ParticipantSecureTokenService secureTokenService;
7272
private String ownDid;
7373
private TransactionContext transactionContext;
7474

@@ -114,7 +114,7 @@ private Result<String> sendCredentialRequest(HolderCredentialRequest request, St
114114

115115
return transactionContext.execute(() -> {
116116
transition(request.copy().toBuilder(), REQUESTING);
117-
return getAuthToken(issuerDid, ownDid)
117+
return getAuthToken(request.getParticipantContextId(), issuerDid, ownDid)
118118
.compose(token -> createCredentialsRequest(token, endpoint, holderPid, typesAndFormats))
119119
.compose(httpRequest -> httpClient.execute(httpRequest, this::mapResponseAsString));
120120
});
@@ -207,18 +207,19 @@ private Result<String> mapResponseAsString(Response response) {
207207
/**
208208
* Fetches the authentication token from the SecureTokenService.
209209
*
210-
* @param audience the String used as {@code aud} claim
211-
* @param myOwnDid the String used as {@code iss} and {@code sub} claims
210+
* @param participantContextId The ID of the participant context on behalf of which the token is generated
211+
* @param audience the String used as {@code aud} claim
212+
* @param myOwnDid the String used as {@code iss} and {@code sub} claims
212213
* @return a JWT token that can be used to send DCP messages to the issuer
213214
*/
214-
private Result<TokenRepresentation> getAuthToken(String audience, String myOwnDid) {
215+
private Result<TokenRepresentation> getAuthToken(String participantContextId, String audience, String myOwnDid) {
215216
var siTokenClaims = Map.of(
216217
ISSUED_AT, Instant.now().toString(),
217218
AUDIENCE, audience,
218219
ISSUER, myOwnDid,
219220
SUBJECT, myOwnDid,
220221
EXPIRATION_TIME, Instant.now().plus(5, ChronoUnit.MINUTES).toString());
221-
return secureTokenService.createToken(siTokenClaims, null);
222+
return secureTokenService.createToken(participantContextId, siTokenClaims, null);
222223
}
223224

224225
/**
@@ -262,7 +263,7 @@ public Builder httpClient(EdcHttpClient httpClient) {
262263
return this;
263264
}
264265

265-
public Builder secureTokenService(SecureTokenService secureTokenService) {
266+
public Builder secureTokenService(ParticipantSecureTokenService secureTokenService) {
266267
manager.secureTokenService = secureTokenService;
267268
return this;
268269
}

core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/services/verifiablecredential/CredentialRequestManagerImplTest.java

+8-7
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
import org.eclipse.edc.iam.did.spi.document.DidDocument;
2222
import org.eclipse.edc.iam.did.spi.document.Service;
2323
import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry;
24-
import org.eclipse.edc.iam.identitytrust.spi.SecureTokenService;
2524
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat;
2625
import org.eclipse.edc.identityhub.protocols.dcp.spi.model.CredentialRequestMessage;
26+
import org.eclipse.edc.identityhub.spi.authentication.ParticipantSecureTokenService;
2727
import org.eclipse.edc.identityhub.spi.credential.request.model.HolderCredentialRequest;
2828
import org.eclipse.edc.identityhub.spi.credential.request.model.HolderRequestState;
2929
import org.eclipse.edc.identityhub.spi.credential.request.store.HolderCredentialRequestStore;
@@ -59,6 +59,7 @@
5959
import static org.mockito.ArgumentMatchers.any;
6060
import static org.mockito.ArgumentMatchers.anyInt;
6161
import static org.mockito.ArgumentMatchers.anyMap;
62+
import static org.mockito.ArgumentMatchers.anyString;
6263
import static org.mockito.ArgumentMatchers.argThat;
6364
import static org.mockito.ArgumentMatchers.eq;
6465
import static org.mockito.Mockito.doThrow;
@@ -77,7 +78,7 @@ class CredentialRequestManagerImplTest {
7778
private final DidResolverRegistry resolver = mock();
7879
private final TypeTransformerRegistry transformerRegistry = mock();
7980
private final EdcHttpClient httpClient = mock();
80-
private final SecureTokenService sts = mock();
81+
private final ParticipantSecureTokenService sts = mock();
8182
private final CredentialRequestManagerImpl credentialRequestService = CredentialRequestManagerImpl.Builder.newInstance()
8283
.store(store)
8384
.didResolverRegistry(resolver)
@@ -94,7 +95,7 @@ class CredentialRequestManagerImplTest {
9495
void setUp() {
9596
when(transformerRegistry.transform(any(CredentialRequestMessage.class), eq(JsonObject.class)))
9697
.thenReturn(success(Json.createObjectBuilder().build()));
97-
when(sts.createToken(anyMap(), ArgumentMatchers.isNull())).thenReturn(success(TokenRepresentation.Builder.newInstance().build()));
98+
when(sts.createToken(anyString(), anyMap(), ArgumentMatchers.isNull())).thenReturn(success(TokenRepresentation.Builder.newInstance().build()));
9899
}
99100

100101
private DidDocument didDocument() {
@@ -156,7 +157,7 @@ void processInitial_shouldSendRequest(String stateString) {
156157
var inOrder = inOrder(resolver, store, httpClient, sts);
157158
inOrder.verify(resolver).resolve(eq(ISSUER_DID));
158159
inOrder.verify(store).save(argThat(r -> r.getState() == REQUESTING.code()));
159-
inOrder.verify(sts).createToken(anyMap(), ArgumentMatchers.isNull());
160+
inOrder.verify(sts).createToken(anyString(), anyMap(), ArgumentMatchers.isNull());
160161
inOrder.verify(httpClient).execute(any(), (Function<Response, Result<String>>) any());
161162
inOrder.verify(store).save(argThat(r -> r.getState() == REQUESTED.code() && r.getIssuerPid() != null));
162163
});
@@ -218,7 +219,7 @@ void processInitial_whenStsFails_shouldTransitionToError(String stateString) {
218219
var state = HolderRequestState.valueOf(stateString);
219220

220221
when(resolver.resolve(eq(ISSUER_DID))).thenReturn(success(didDocument()));
221-
when(sts.createToken(anyMap(), ArgumentMatchers.isNull())).thenReturn(Result.failure("sts-failure"));
222+
when(sts.createToken(anyString(), anyMap(), ArgumentMatchers.isNull())).thenReturn(Result.failure("sts-failure"));
222223

223224
var rq = createRequest()
224225
.state(state.code())
@@ -233,7 +234,7 @@ void processInitial_whenStsFails_shouldTransitionToError(String stateString) {
233234
var inOrder = inOrder(resolver, store, httpClient, sts);
234235
inOrder.verify(resolver).resolve(eq(ISSUER_DID));
235236
inOrder.verify(store).save(argThat(r -> r.getState() == REQUESTING.code()));
236-
inOrder.verify(sts).createToken(anyMap(), ArgumentMatchers.isNull());
237+
inOrder.verify(sts).createToken(anyString(), anyMap(), ArgumentMatchers.isNull());
237238
inOrder.verify(store).save(argThat(r -> r.getState() == ERROR.code() && r.getErrorDetail().equals("sts-failure")));
238239
});
239240
}
@@ -258,7 +259,7 @@ void processInitial_whenIssuerReturnsError_shouldTransitionToError(String stateS
258259
var inOrder = inOrder(resolver, store, httpClient, sts);
259260
inOrder.verify(resolver).resolve(eq(ISSUER_DID));
260261
inOrder.verify(store).save(argThat(r -> r.getState() == REQUESTING.code()));
261-
inOrder.verify(sts).createToken(anyMap(), ArgumentMatchers.isNull());
262+
inOrder.verify(sts).createToken(anyString(), anyMap(), ArgumentMatchers.isNull());
262263
inOrder.verify(httpClient).execute(any(), (Function<Response, Result<String>>) any());
263264
inOrder.verify(store).save(argThat(r -> r.getState() == ERROR.code() && r.getErrorDetail().equals("issuer failure bad request")));
264265
});

core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImpl.java

+1
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ private ParticipantContext convert(ParticipantManifest manifest) {
178178
.did(manifest.getDid())
179179
.apiTokenAlias("%s-%s".formatted(manifest.getParticipantId(), API_KEY_ALIAS_SUFFIX))
180180
.state(ParticipantContextState.CREATED)
181+
.properties(manifest.getAdditionalProperties())
181182
.build();
182183
}
183184
}

e2e-tests/identity-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndTestContext.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public String storeCredential(VerifiableCredential credential, String participan
116116
.participantContextId(participantContextId)
117117
.holderId("holderId")
118118
.issuerId("issuerId")
119-
.credential(new VerifiableCredentialContainer("rawVc", CredentialFormat.JWT, credential))
119+
.credential(new VerifiableCredentialContainer("rawVc", CredentialFormat.VC1_0_JWT, credential))
120120
.build();
121121
runtime.getService(CredentialStore.class).create(resource).orElseThrow(f -> new EdcException(f.getFailureDetail()));
122122
return resource.getId();

extensions/api/identity-api/api-configuration/src/main/resources/identity-api-version.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{
33
"version": "1.0.0-alpha",
44
"urlPath": "/v1alpha",
5-
"lastUpdated": "2025-02-21T12:00:00Z",
5+
"lastUpdated": "2025-02-21T14:00:00Z",
66
"maturity": null
77
}
88
]

extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/BaseSqlDialectStatements.java

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public String getInsertTemplate() {
3232
.column(getApiTokenAliasColumn())
3333
.column(getDidColumn())
3434
.jsonColumn(getRolesRolumn())
35+
.jsonColumn(getPropertiesColumn())
3536
.insertInto(getParticipantContextTable());
3637
}
3738

@@ -45,6 +46,7 @@ public String getUpdateTemplate() {
4546
.column(getApiTokenAliasColumn())
4647
.column(getDidColumn())
4748
.jsonColumn(getRolesRolumn())
49+
.jsonColumn(getPropertiesColumn())
4850
.update(getParticipantContextTable(), getIdColumn());
4951
}
5052

extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/ParticipantContextStoreStatements.java

+4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ default String getRolesRolumn() {
5555
return "roles";
5656
}
5757

58+
default String getPropertiesColumn() {
59+
return "properties";
60+
}
61+
5862
String getInsertTemplate();
5963

6064
String getUpdateTemplate();

extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/SqlParticipantContextStore.java

+7-5
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
package org.eclipse.edc.identityhub.store.sql.participantcontext;
1616

17-
import com.fasterxml.jackson.core.type.TypeReference;
1817
import com.fasterxml.jackson.databind.ObjectMapper;
1918
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext;
2019
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContextState;
@@ -32,6 +31,7 @@
3231
import java.sql.SQLException;
3332
import java.util.Collection;
3433
import java.util.List;
34+
import java.util.Map;
3535
import java.util.Objects;
3636

3737
import static org.eclipse.edc.spi.result.StoreResult.alreadyExists;
@@ -43,8 +43,6 @@
4343
*/
4444
public class SqlParticipantContextStore extends AbstractSqlStore implements ParticipantContextStore {
4545

46-
private static final TypeReference<List<String>> LIST_REF = new TypeReference<>() {
47-
};
4846
private final ParticipantContextStoreStatements statements;
4947

5048
public SqlParticipantContextStore(DataSourceRegistry dataSourceRegistry,
@@ -74,7 +72,8 @@ public StoreResult<Void> create(ParticipantContext participantContext) {
7472
participantContext.getState(),
7573
participantContext.getApiTokenAlias(),
7674
participantContext.getDid(),
77-
toJson(participantContext.getRoles())
75+
toJson(participantContext.getRoles()),
76+
toJson(participantContext.getProperties())
7877
);
7978
return success();
8079

@@ -114,6 +113,7 @@ public StoreResult<Void> update(ParticipantContext participantContext) {
114113
participantContext.getApiTokenAlias(),
115114
participantContext.getDid(),
116115
toJson(participantContext.getRoles()),
116+
toJson(participantContext.getProperties()),
117117
id);
118118
return StoreResult.success();
119119
}
@@ -156,7 +156,8 @@ private ParticipantContext mapResultSet(ResultSet resultSet) throws Exception {
156156
var state = resultSet.getInt(statements.getStateColumn());
157157
var tokenAliase = resultSet.getString(statements.getApiTokenAliasColumn());
158158
var did = resultSet.getString(statements.getDidColumn());
159-
var roles = fromJson(resultSet.getString(statements.getRolesRolumn()), LIST_REF);
159+
List<String> roles = fromJson(resultSet.getString(statements.getRolesRolumn()), getTypeRef());
160+
Map<String, Object> props = fromJson(resultSet.getString(statements.getPropertiesColumn()), getTypeRef());
160161

161162
return ParticipantContext.Builder.newInstance()
162163
.participantContextId(id)
@@ -166,6 +167,7 @@ private ParticipantContext mapResultSet(ResultSet resultSet) throws Exception {
166167
.apiTokenAlias(tokenAliase)
167168
.did(did)
168169
.roles(roles)
170+
.properties(props)
169171
.build();
170172
}
171173
}

extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/resources/participant-schema.sql

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ CREATE TABLE IF NOT EXISTS participant_context
2121
state INTEGER NOT NULL, -- 0 = CREATED, 1 = ACTIVE, 2 = DEACTIVATED
2222
api_token_alias VARCHAR NOT NULL, -- alias under which this PC's api token is stored in the vault
2323
did VARCHAR, -- the DID with which this participant is identified
24-
roles JSON -- JSON array containing all the roles a user has. may be empty
24+
roles JSON, -- JSON array containing all the roles a user has. may be empty
25+
properties JSON DEFAULT '{}' -- JSON object containing additional information, such as OAuth2 client secret aliases
2526
);
2627
CREATE UNIQUE INDEX IF NOT EXISTS participant_context_participant_context_id_uindex ON participant_context USING btree (participant_context_id);
2728

extensions/sts/sts-account-provisioner/src/main/java/org/eclipse/edc/identityhub/common/provisioner/StsAccountProvisionerImpl.java

+1-5
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@
3434

3535
import java.util.Objects;
3636

37-
import static java.util.Optional.ofNullable;
38-
3937
/**
4038
* AccountProvisioner, that synchronizes the {@link org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext} object
4139
* to {@link StsAccount} entries. That means, when a participant is created, this provisioner takes care of creating a corresponding
@@ -79,9 +77,7 @@ public <E extends Event> void on(EventEnvelope<E> event) {
7977
@Override
8078
public ServiceResult<AccountInfo> create(ParticipantManifest manifest) {
8179

82-
var secretAlias = ofNullable(manifest.getProperty(CLIENT_SECRET_PROPERTY))
83-
.map(Object::toString)
84-
.orElseGet(() -> manifest.getParticipantId() + "-sts-client-secret");
80+
var secretAlias = manifest.clientSecretAlias();
8581
var createResult = stsAccountService.createAccount(manifest, secretAlias)
8682
.map(v -> stsClientSecretGenerator.generateClientSecret(null))
8783
.map(secret -> new AccountInfo(manifest.getDid(), secret))

extensions/sts/sts-account-service-local/build.gradle.kts

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ plugins {
2020
dependencies {
2121

2222
api(project(":spi:participant-context-spi"))
23+
api(project(":spi:identity-hub-spi")) // participant STS
24+
api(project(":spi:keypair-spi")) // keypair resource store
2325
implementation(libs.edc.lib.token)
24-
implementation(libs.edc.sts)
2526
implementation(libs.edc.sts.spi)
2627
implementation(libs.edc.spi.core)
2728
implementation(libs.edc.spi.transaction)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2025 Cofinity-X
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Apache License, Version 2.0 which is available at
6+
* https://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* SPDX-License-Identifier: Apache-2.0
9+
*
10+
* Contributors:
11+
* Cofinity-X - initial API and implementation
12+
*
13+
*/
14+
15+
package org.eclipse.edc.identityhub.sts;
16+
17+
import org.eclipse.edc.spi.iam.TokenParameters;
18+
import org.eclipse.edc.token.spi.TokenDecorator;
19+
20+
import java.time.Instant;
21+
import java.util.Date;
22+
import java.util.Map;
23+
24+
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.EXPIRATION_TIME;
25+
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUED_AT;
26+
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.JWT_ID;
27+
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.NOT_BEFORE;
28+
29+
/**
30+
* Opinionated decorator that adds "jti", "iat", "nbf" and "exp" claims to an existing set of claims, overwriting if the aforementioned
31+
* are already contained in the map.
32+
*/
33+
class AccessTokenDecorator implements TokenDecorator {
34+
35+
private final String jti;
36+
private final Instant now;
37+
private final Instant expiration;
38+
private final Map<String, String> claims;
39+
40+
AccessTokenDecorator(String jti, Instant now, Instant expiration, Map<String, String> claims) {
41+
this.jti = jti;
42+
this.now = now;
43+
this.expiration = expiration;
44+
this.claims = claims;
45+
}
46+
47+
@Override
48+
public TokenParameters.Builder decorate(TokenParameters.Builder tokenParameters) {
49+
this.claims.forEach(tokenParameters::claims);
50+
return tokenParameters
51+
.claims(ISSUED_AT, Date.from(now))
52+
.claims(NOT_BEFORE, Date.from(now))
53+
.claims(EXPIRATION_TIME, Date.from(expiration))
54+
.claims(JWT_ID, jti);
55+
}
56+
}

0 commit comments

Comments
 (0)