[java-idp-plugin-vci] 03/03: Create common shell for all supported credential types.
Codeberg
noreply at shibboleth.net
Fri Jan 2 12:26:25 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch dev/W3CCred
in repository java-idp-plugin-vci.
View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-vci/commit/702ba8e79151fc4dd9d9502d488b51189a491cd1
commit 702ba8e79151fc4dd9d9502d488b51189a491cd1
Author: jlauros <janne.lauros at csc.fi>
AuthorDate: Fri Jan 2 14:26:11 2026 +0200
Create common shell for all supported credential types.
---
.../messaging/context/CredentialsContext.java | 25 ++-
.../openidvci/profile/impl/AddCredentialShell.java | 224 +++++++++++++++++++++
.../FormOutboundCredentialsResponseMessage.java | 60 +-----
.../openid/vci/credentials/credentials-beans.xml | 5 +-
.../openid/vci/credentials/credentials-flow.xml | 7 +
5 files changed, 270 insertions(+), 51 deletions(-)
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialsContext.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialsContext.java
index 13ca331..c2ab09a 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialsContext.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialsContext.java
@@ -25,6 +25,7 @@ import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferReque
import org.opensaml.messaging.context.BaseContext;
import com.nimbusds.jose.JWSObject;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
/**
* Subcontext for /credentials - end point. This context appears as a subcontext
@@ -42,10 +43,14 @@ public class CredentialsContext extends BaseContext {
@Nullable
private String credentialIdentifier;
- /** Validated proofs of wallet . */
+ /** Validated proofs of wallet. */
@Nullable
private List<JWSObject> proofs;
+ /** Credential claims. */
+ @Nullable
+ private List<ClaimsSet> credentialClaimsSet;
+
/**
* Get validated proofs of wallet.
*
@@ -122,4 +127,22 @@ public class CredentialsContext extends BaseContext {
credentialIdentifier = identifier;
}
+ /**
+ * Get credential claims.
+ *
+ * @return Credential claims
+ */
+ public List<ClaimsSet> getCredentialClaimsSet() {
+ return credentialClaimsSet;
+ }
+
+ /**
+ * Set credential claims.
+ *
+ * @param credentialClaimsSet Credential claims
+ */
+ public void setCredentialClaimsSet(List<ClaimsSet> credentialClaimsSet) {
+ this.credentialClaimsSet = credentialClaimsSet;
+ }
+
}
\ No newline at end of file
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddCredentialShell.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddCredentialShell.java
new file mode 100644
index 0000000..6443aab
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddCredentialShell.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.geant.shibboleth.plugin.openidvci.profile.impl;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.profile.config.ProfileConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+import org.geant.shibboleth.plugin.openidvci.config.OpenIDVCIConfiguration;
+import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.oauth2.sdk.id.Audience;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+/**
+ * Action that creates an Array of {@link ClaimsSet} shells for Credential(s),
+ * and sets it to work context {@link CredentialsContext} located under
+ * {@link ProfileRequestContext#getInboundMessageContext()}.
+ *
+ * Shell is initialized with claims common to all credentials: 'iss', 'aud',
+ * 'iat', 'exp', 'validFrom', 'validUntil' and 'vct'. Final set of claims yet to
+ * be defined.
+ *
+ * Each proof will have own shell. In such cases also 'jwk' claim is set.
+ */
+public class AddCredentialShell extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull
+ private Logger log = LoggerFactory.getLogger(AddCredentialShell.class);
+
+ /** Strategy used to obtain the response issuer value. */
+ @NonnullAfterInit
+ private Function<ProfileRequestContext, String> issuerLookupStrategy;
+
+ /** Issuer ID to populate into Issuer element. */
+ @Nullable
+ private String issuerId;
+
+ /**
+ * Strategy used to locate the {@link RelyingPartyContext} associated with a
+ * given {@link ProfileRequestContext}.
+ */
+ @Nonnull
+ private Function<ProfileRequestContext, RelyingPartyContext> relyingPartyContextLookupStrategy;
+
+ /** The RelyingPartyContext to operate on. */
+ @Nullable
+ private RelyingPartyContext rpCtx;
+
+ /** Lifetime of credential. */
+ private Duration expiration;
+
+ /**
+ * Credential offer context.
+ */
+ @NonnullAfterInit
+ private CredentialsContext ctx;
+
+ /** Constructor. */
+ public AddCredentialShell() {
+ relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+ issuerLookupStrategy = (Function<ProfileRequestContext, String>) new IssuerLookupFunction();
+ }
+
+ /**
+ * Set the strategy used to locate the {@link RelyingPartyContext} associated
+ * with a given {@link ProfileRequestContext}.
+ *
+ * @param strategy strategy used to locate the {@link RelyingPartyContext}
+ * associated with a given {@link ProfileRequestContext}
+ */
+ public void setRelyingPartyContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyContext> strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+
+ relyingPartyContextLookupStrategy = Constraint.isNotNull(strategy,
+ "RelyingPartyContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the issuer value to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+ issuerLookupStrategy = Constraint.isNotNull(strategy, "IssuerLookupStrategy lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (issuerLookupStrategy == null) {
+ throw new ComponentInitializationException("Issuer lookup strategy cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ rpCtx = relyingPartyContextLookupStrategy.apply(profileRequestContext);
+ if (rpCtx == null) {
+ log.debug("{} No relying party context associated with this profile request", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+ return false;
+ }
+ final ProfileConfiguration pc = rpCtx.getProfileConfig();
+ if (pc instanceof OpenIDVCIConfiguration) {
+ expiration = ((OpenIDVCIConfiguration) pc).getCredentialLifetime(profileRequestContext);
+ } else {
+ log.error("{} No OpenID VCI profile configuration associated with this profile request", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+ return false;
+ }
+
+ issuerId = issuerLookupStrategy.apply(profileRequestContext);
+ if (issuerId == null) {
+ log.error("{} Unable to determine issuer to check audience", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ }
+
+ ctx = profileRequestContext.getInboundMessageContext().getSubcontext(CredentialsContext.class);
+ if (ctx == null) {
+ log.debug("{} No credentials context associated with this profile request", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ List<ClaimsSet> shells = new ArrayList<>();
+ if (ctx.getProofs() != null && !ctx.getProofs().isEmpty()) {
+ ctx.getProofs().forEach(proof -> {
+ ClaimsSet shell = createShell();
+ Map<String, Object> cnfKid = new HashMap<>();
+ try {
+ cnfKid.put("jwk",
+ new ObjectMapper().readValue(proof.getHeader().getJWK().toJSONString(), Object.class));
+ shell.setClaim("cnf", cnfKid);
+ } catch (JsonProcessingException e1) {
+ log.error("{} Parsing failed", getLogPrefix(), e1);
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return;
+ }
+ shells.add(shell);
+
+ });
+ } else {
+ shells.add(createShell());
+ }
+ ctx.setCredentialClaimsSet(shells);
+ }
+
+ /**
+ * Creates shell without proof.
+ *
+ * @return Shell without proof
+ */
+ private ClaimsSet createShell() {
+ ClaimsSet shell = new ClaimsSet();
+ shell.setIssuer(new Issuer(issuerId));
+ shell.setAudience(new Audience(rpCtx.getRelyingPartyId()));
+ ZonedDateTime now = ZonedDateTime.now();
+ shell.setClaim("iat", now.toEpochSecond());
+ shell.setClaim("exp", now.toEpochSecond() + expiration.toSeconds());
+ shell.setClaim("validFrom", DateTimeFormatter.ISO_DATE_TIME.format(now));
+ shell.setClaim("validUntil", DateTimeFormatter.ISO_DATE_TIME.format(now.plusSeconds(expiration.getSeconds())));
+ shell.setClaim("vct", ctx.getCredentialIdentifier());
+ return shell;
+ }
+
+}
\ No newline at end of file
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundCredentialsResponseMessage.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundCredentialsResponseMessage.java
index 7642474..8279494 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundCredentialsResponseMessage.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundCredentialsResponseMessage.java
@@ -17,7 +17,6 @@
package org.geant.shibboleth.plugin.openidvci.profile.impl;
import java.util.ArrayList;
-import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -27,7 +26,6 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.security.interfaces.ECPrivateKey;
-import java.time.Instant;
import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedCredential;
@@ -50,7 +48,6 @@ import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.logic.FunctionSupport;
import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
@@ -60,6 +57,7 @@ import com.nimbusds.jose.util.Base64;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTClaimsSet.Builder;
import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.ParseException;
/**
* Action forming {@link CredentialSuccessResponse}
@@ -195,52 +193,16 @@ public class FormOutboundCredentialsResponseMessage extends AbstractOIDCResponse
credential.getRequestedCredential().forEach(claim -> claims.put(claim.getPath().get(0), claim.getValue()));
SelectiveDisclosureClaimSetUtil sdClaims = new SelectiveDisclosureClaimSetUtil(claims);
- // cnfKid should be picked from request proof key section.
List<String> credentials = new ArrayList<>();
- // Map<String, String> cnfKid = null;
- if (ctx.getProofs() != null && !ctx.getProofs().isEmpty()) {
- ctx.getProofs().forEach(proof -> {
- Map<String, Object> cnfKid = new HashMap<>();
- try {
- cnfKid.put("jwk",
- new ObjectMapper().readValue(proof.getHeader().getJWK().toJSONString(), Object.class));
- } catch (JsonProcessingException e1) {
- // TODO Auto-generated catch block
- e1.printStackTrace();
- }
- // TODO: vct is the credential configuration id in SD JWT. Here we use however
- // "instance" specific
- // identifier that happes to be in our case derived from it. We need to get the
- // real configuration id here.
- Builder build = new JWTClaimsSet.Builder().claim("vct", ctx.getCredentialIdentifier().split("_")[0])
- .claim("iss", issuerLookupStrategy.apply(profileRequestContext))
- // Just to pass happy path test.
- .claim("exp", Instant.now().getEpochSecond() + 3600).claim("_sd", sdClaims.get_sd())
- .claim("_sd_alg", sdClaims.get_alg()).claim("cnf", cnfKid).issueTime(new Date());
- JWTClaimsSet claimsSet = build.build();
- SignedJWT signedJWT = new SignedJWT(
- new JWSHeader.Builder(new JWSAlgorithm(signatureSigningParameters.getSignatureAlgorithm()))
- .x509CertChain(certificateChainLookupStrategy.apply(signatureSigningParameters))
- .type(new JOSEObjectType("dc+sd-jwt")).keyID("signing").build(),
- claimsSet);
- try {
- signedJWT.sign(signer);
- } catch (JOSEException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- String v15 = signedJWT.serialize();
- // Add the plain text claims
- v15 += "~" + sdClaims.getFormattedDisclosures() + "~";
- credentials.add(v15);
-
- });
- } else {
- Builder build = new JWTClaimsSet.Builder().claim("vct", ctx.getCredentialIdentifier())
- // Just to pass happy path test.
- .claim("exp", Instant.now().getEpochSecond() + 3600)
- .claim("iss", issuerLookupStrategy.apply(profileRequestContext)).claim("_sd", sdClaims.get_sd())
- .claim("_sd_alg", sdClaims.get_alg()).issueTime(new Date());
+ ctx.getCredentialClaimsSet().forEach(cred -> {
+ Builder build = null;
+ try {
+ build = new JWTClaimsSet.Builder(cred.toJWTClaimsSet()).claim("_sd", sdClaims.get_sd())
+ .claim("_sd_alg", sdClaims.get_alg());
+ } catch (ParseException e1) {
+ // TODO Auto-generated catch block
+ e1.printStackTrace();
+ }
JWTClaimsSet claimsSet = build.build();
SignedJWT signedJWT = new SignedJWT(
new JWSHeader.Builder(new JWSAlgorithm(signatureSigningParameters.getSignatureAlgorithm()))
@@ -257,7 +219,7 @@ public class FormOutboundCredentialsResponseMessage extends AbstractOIDCResponse
// Add the plain text claims
v15 += "~" + sdClaims.getFormattedDisclosures() + "~";
credentials.add(v15);
- }
+ });
CredentialSuccessResponse response = new CredentialSuccessResponse(credentials);
log.info("Setting response as {}", response.toOffer());
profileRequestContext.ensureOutboundMessageContext().setMessage(response);
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml
index f75e960..c01f354 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml
@@ -69,8 +69,11 @@
c:g-ref="shibboleth.oidc.ChildLookupOrCreate.JWTSecurityParameters"
c:f-ref="shibboleth.MessageContextLookup.Outbound" />
</property>
- </bean>
+ </bean>
+ <bean id="AddCredentialShell" class="org.geant.shibboleth.plugin.openidvci.profile.impl.AddCredentialShell"
+ scope="prototype" />
+
<bean id="FormOutboundMessage" class="org.geant.shibboleth.plugin.openidvci.profile.impl.FormOutboundCredentialsResponseMessage"
scope="prototype" p:issuerLookupStrategy-ref="shibboleth.ResponderIdLookup.Simple" >
<property name="securityParametersLookupStrategy">
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-flow.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-flow.xml
index 4e13778..3d3870d 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-flow.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-flow.xml
@@ -31,6 +31,13 @@
<action-state id="ResumeAfterDoDPoPProofValidation">
<evaluate expression="PopulateCredentialsSignatureSigningParameters" />
<evaluate expression="'proceed'"/>
+ <transition on="proceed" to="BuildResponse"/>
+ </action-state>
+
+
+ <action-state id="BuildResponse">
+ <evaluate expression="AddCredentialShell" />
+ <evaluate expression="'proceed'"/>
<transition on="proceed" to="BuildResponseMessage"/>
</action-state>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list