[java-idp-plugin-vci] 01/01: Credentials carry now 'fed' and 'termsOfUse' claims if Federation is on and not prevented per credential
Codeberg
noreply at shibboleth.net
Wed Sep 9 11:34:16 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch dev/fedClaims
in repository java-idp-plugin-vci.
View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-vci/commit/0887791b601b0593b7a3138e54ae90c124abe67f
commit 0887791b601b0593b7a3138e54ae90c124abe67f
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Wed Sep 9 14:33:52 2026 +0300
Credentials carry now 'fed' and 'termsOfUse' claims if Federation is on and not prevented per credential
---
README.md | 203 ++++++++++++++++++++-
.../CredentialIssuanceConfiguration.java | 21 ++-
.../CredentialIssuanceConfigurationTest.java | 11 ++
...IssuerEntityConfigurationMetadataDecorator.java | 49 +++++
.../openidvci/profile/impl/AddCredentialShell.java | 124 +++++++++++++
.../openid/vci/credentials/credentials-beans.xml | 4 +-
.../plugin/openidvci/conf/openid-vci-oidfed.xml | 3 +-
7 files changed, 405 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
index 2ca5797..7f2dd05 100644
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@ Requires IdP **5.2.3** or later.
> 5. OpenID Connect OP Federation Plugin (`idp-plugin-oidfed-op`)
>
> Install them by
-> [OPFederationWIP](https://shibboleth.atlassian.net/wiki/spaces/IDPPLUGINS/pages/4500914216/OPFederationWIP)
+> [OIDFEDCommon](https://shibboleth.atlassian.net/wiki/spaces/IDPPLUGINS/pages/5601296385/OIDFEDCommon)
> of the Shibboleth wiki. It has the snapshot locations and the commands for all five. Follow
> that page and you have everything this plugin needs.
>
@@ -309,6 +309,7 @@ It goes inside the credential's own configuration, next to `format` and `vct`:
| `lifetime` | Duration | `credentialLifetime` of the profile configuration, `P180D` | Length of the validity window. |
| `not_before` | Duration, moment or date | The instant of issuance | Where the validity window opens. |
| `status_list` | Boolean | `openidvci.status-list.includeByDefault`, `true` | Whether the credential takes a Status List slot. |
+| `oidfed` | Boolean | `true` | Whether the credential states the Entity Identifier of this deployment in OpenID Federation. |
All three are optional and so is the member itself. A credential that states nothing behaves
exactly as it did before you knew this member existed.
@@ -351,6 +352,42 @@ What ends up in the credential:
A credential whose window opens at issuance carries no `nbf`.
+##### Whether a credential states this deployment in OpenID Federation
+
+Importing *conf/openid-vci-oidfed.xml* into *conf/global.xml* is what turns this on. That one
+import states this deployment as a Credential Issuer in its Entity Configuration and makes its
+credentials name the same identifier. Without it a credential says nothing about a federation,
+which is what a deployment outside one wants.
+
+`"oidfed": false` in a Credential Configuration takes one credential back out. That is the only
+way to prevent it.
+
+The value is not yours to write. It is the `credential_issuer` of your Credential Issuer
+metadata and it is read from there, because
+[OpenID Federation Digital Credentials Profile](https://fidescommunity.github.io/DIIP/) requires
+the Entity Identifier and `credential_issuer` to be the same string. A `dc+sd-jwt` credential
+carries it in `fed`, a `vc+sd-jwt` one in `termsOfUse`:
+
+```json
+{
+ "fed": "https://issuer.example.org"
+}
+```
+
+```json
+{
+ "termsOfUse": {
+ "type": "OpenIDFederation",
+ "policyId": "https://issuer.example.org"
+ }
+}
+```
+
+The federation module decides the `iss` and `sub` of your Entity Configuration, from
+`idp.oidfed.entityID`, and nothing joins that up with `credential_issuer`. When the two differ
+the log says so as the Entity Configuration is built, because a credential points at
+`credential_issuer` and a verifier resolving it would reach a statement about another entity.
+
##### Whether a credential goes on a Status List
`"status_list": false` keeps the credential off the list. It carries no `status` claim, it takes
@@ -398,13 +435,20 @@ the OP has an EC key, `credentials/idp-signing-es.jwk` of a normal installation.
this plugin does ES256, ES384 and ES512 only, so the RSA key of the OP is of no use here and
without the EC one there is nothing to sign a credential with.
-If you do take a key of your own into use you need to know what changes. Defining
-**openidvci.SigningCredentials** *replaces* the OP's credentials instead of adding to them. The
-key is not published in the OP's `jwks_uri` unless you add it also to
+If you do take a key of your own into use, defining **openidvci.SigningCredentials**
+*replaces* the OP's credentials instead of adding to them. Nothing else changes while the
+Credential Issuer identifier is `did:jwk`, which is the default. The key travels inside the
+credential itself and there is nothing to publish.
+
+Publishing starts to matter when you set `openidvci.issuer` to the plain issuer url instead.
+The key is then named rather than carried, so add it to
**shibboleth.oidc.SigningCredentialsToPublish** in *conf/oidc-credentials.xml*. Nobody is able
-to verify a credential that is signed with an unpublished key. The exception is Credential
-Issuer identifier `did:jwk`, which is the default, as then the key travels inside the credential
-itself.
+to verify a credential that is signed with a key they cannot find.
+
+Stating this deployment in OpenID Federation turns the list from optional into required. The
+Entity Configuration publishes those keys as the `jwks` of `vc_issuer`, and the bean that puts
+them there refuses to initialize without them. An IdP that imports *conf/openid-vci-oidfed.xml*
+while **openidvci.SigningCredentials** is undefined does not start.
You need to import the file where its beans are needed:
@@ -1015,6 +1059,151 @@ The rest of the profiles go to a relying party of their own. Every profile of th
an example of its own below, and [Relying party configuration](#relying-party-configuration)
has them all as one file.
+### Switches
+
+Everything this module lets you decide, grouped by what you are deciding rather than by the
+file it lives in. Names are here for orientation, their types and defaults are in
+[Properties](#properties), [Profile configuration settings](#profile-configuration-settings)
+and [Beans](#beans).
+
+#### Which credentials this deployment issues
+
+*metadata/verifiable-credentials.json* is the whole answer. A credential exists because it has
+a configuration in that file, and its id is the string a wallet asks for, the one `scope`
+matches and the one the access token carries. See
+[Credential Configurations](#credential-configurations).
+
+| Switch | Why you would touch it |
+|---|---|
+| `openidvci.credentialConfigurations.resource` | The file is somewhere else than *metadata/*. |
+| `openidvci.credentialConfigurations.minRefreshDelay`, `…maxRefreshDelay` | How soon an edit of that file takes effect. It is re-read while the IdP runs, so adding a credential needs no restart. |
+
+#### How long an issued credential is valid
+
+The window is `not_before` until `not_before` plus `lifetime`. A credential states its own in
+[Settings of your own, per credential](#settings-of-your-own-per-credential), and what it does
+not state comes from the profile configuration.
+
+| Switch | Why you would touch it |
+|---|---|
+| `lifetime` of a credential | One credential outlives another. A `credentialLifetime` for everything is the alternative. |
+| `not_before` of a credential | The window opens on a date you decide instead of at issuance. A cohort of one school year is the case for it. |
+| `credentialLifetime` | The validity of everything this deployment issues, and of anything that states no `lifetime` of its own. |
+
+#### Whether an issued credential can be withdrawn
+
+A credential is withdrawable when it took a Status List slot, and it took one unless something
+said otherwise. See [Status List](#status-list).
+
+| Switch | Why you would touch it |
+|---|---|
+| `status_list` of a credential | This credential is too short lived to be worth revoking. |
+| `openidvci.status-list.includeByDefault` | Turn the Status List into something each credential opts into instead. |
+| `openidvci.status-list.index.permutationKey` | **You must.** It decides the order slots are handed out in, and with the shipped value an observer is able to tell when a credential was issued. |
+| `openidvci.status-list.token.defaultLifetime` | How long a verifier is allowed to cache a list it read. |
+| `openidvci.status-list.assignment.retention` | How long you have to collect the record of a slot before it is dropped. Your own systems read those records, nothing in the IdP does. |
+
+#### Whether a credential names this deployment in a federation
+
+Off by default, because a credential has no business naming a federation this deployment does
+not publish itself in. The value it names is not yours to write, it is the `credential_issuer`
+of your Credential Issuer metadata. See
+[Whether a credential states this deployment in OpenID Federation](#whether-a-credential-states-this-deployment-in-openid-federation).
+
+| Switch | Why you would touch it |
+|---|---|
+| The import of *conf/openid-vci-oidfed.xml* | This is the switch. It states this deployment as a Credential Issuer in its Entity Configuration and makes credentials name the same identifier. |
+| `oidfed` of a credential | One credential stays out of it while the rest do not. |
+| `openidvci.oidfed.decorateFederationEntity` | State the federation entity in the federation module's own file instead of filling it from Credential Issuer metadata. |
+
+#### Which key signs what
+
+Nothing here is active to begin with. Credentials, Status List Tokens and Credential Issuer
+metadata are all signed with the OP's own signing credentials until you say otherwise, and that
+works for as long as the OP has an EC key. Signing does ES256, ES384 and ES512 only. See
+[Signing keys](#signing-keys).
+
+| Switch | Why you would touch it |
+|---|---|
+| **openidvci.SigningCredentials** | Credentials are signed with a key of their own. Defining this list *replaces* the OP's credentials, it does not add to them. |
+| **openidvci.status-list.SigningCredentials** | Status List Tokens are signed with a key of their own. |
+| **openidvci.issuerMetadata.SigningCredentials** | Publish the Credential Issuer metadata signed. Undefined means unsigned. |
+| `openidvci.signing.es.key`, `openidvci.signing.status-list.es.key`, `openidvci.signing.issuerMetadata.es.key` | Those keys are somewhere else than *credentials/*. |
+| `credentialSignatureSigningConfiguration`, `statusListSignatureSigningConfiguration`, `issuerMetadataSignatureSigningConfiguration` | Narrow the algorithms a deployment allows at all. The algorithm is narrowed again by the `credential_signing_alg_values_supported` of the credential in issue. |
+
+#### What a credential says issued it
+
+| Switch | Why you would touch it |
+|---|---|
+| `openidvci.issuer` | The `iss` of a credential is the plain issuer url instead of the `did:jwk` of the signing key. Empty is the default and gives `did:jwk`, which carries the key inside the credential and needs nothing published. |
+
+#### What the Credential Issuer metadata says
+
+The document is a Velocity template of yours and the `credential_configurations_supported`
+member is filled in from the file of credentials. See
+[Discovery documents](#discovery-documents).
+
+| Switch | Why you would touch it |
+|---|---|
+| `openidvci.issuerMetadata.template` | The document is somewhere else than *static/*. |
+| `openidvci.issuerMetadata.templated` | Your document contains a literal `$` or `#` and must not be read as Velocity. |
+| **openidvci.issuerMetadata.TemplateContext** | Add variables of your own, or replace `$issuer`, `$baseUrl` and `$host`. |
+| `openidvci.issuerMetadata.resolver`, `…resolver.values` | Resolve the document, or single members of it, some other way. |
+| **openidvci.CredentialIssuerMetadata** | Replace the document wholesale with a resource of your own. |
+| `openidvci.issuerMetadata.signedLifetime` | Signed metadata carries an `exp`. Unset publishes it without one. |
+| `openidvci.issuerMetadata.minRefreshDelay`, `…maxRefreshDelay` | How soon an edit of the document takes effect. |
+
+#### What a pre-authorized code looks like
+
+The length decides which of two quite different codes you get, and it is the one setting here
+to understand before you pick a value. See
+[Pre-authorized code flow](#pre-authorized-code-flow).
+
+| Switch | Why you would touch it |
+|---|---|
+| `openidvci.preAuthorizedCode.defaultLength` | `0` seals the whole offer inside the code, which needs no storage and makes a dense QR code. `10` or more is a short code and a stored offer. |
+| `openidvci.preAuthorizedCode.defaultLifetime` | How long the holder of an offer has to redeem it. |
+| `preAuthorizedCodeLength`, `preAuthorizedCodeLifetime` | The same two per relying party rather than per deployment. |
+
+#### Who is allowed to call an endpoint
+
+Nothing is enabled by default. An endpoint answers because its profile configuration bean is in
+a relying party, and the bean says how the caller authenticates. See
+[Relying party configuration](#relying-party-configuration).
+
+| Switch | Why you would touch it |
+|---|---|
+| The **OpenID.VCI.\*** and **OAuth.StatusList\*** beans | Enable an endpoint, for everyone in **shibboleth.UnverifiedRelyingParty** or for one client by name. |
+| `tokenEndpointAuthMethods` | An unregistered wallet needs `none`, your own caller of the offer and revoke endpoints needs a real method. |
+| `openidvci.authn.flows` | The login flows a VCI endpoint accepts. |
+| **openidvci.TokenManipulationStrategy** | Carry the authorization details and the claim values into the code and the access token. **OIDC.SSO** and **OpenID.VCI.Token** both need it. |
+
+#### What a wallet has to prove
+
+| Switch | Why you would touch it |
+|---|---|
+| `proofSignatureValidationConfiguration` | Which algorithms a key proof may be signed with. |
+| `proofClaimsValidator` | What the claims of a key proof have to say. |
+| `proofNonceGenerator` | The nonce a key proof carries. Set the same generator in **OpenID.VCI.Nonce**, which is what hands the nonce out, and `#{null}` in both to ask for no nonce at all. |
+
+#### Where server side state is kept
+
+Storage is only needed for what cannot be sealed into a token. All of these want real server
+side storage rather than the memory backed default of a cluster.
+
+| Switch | Why you would touch it |
+|---|---|
+| `openidvci.StorageService` | Credential Offers, when the code is a stored one. |
+| `openidvci.status-list.StorageService` | The lists themselves. |
+| `openidvci.status-list.index.StorageService` | The counter of slots handed out. It has the churn of the three, so it may want storage of its own. |
+| `openidvci.status-list.assignment.StorageService` | The records of which credential took which slot, kept for collection. |
+
+#### What the log calls each flow
+
+| Switch | Why you would touch it |
+|---|---|
+| `openidvci.logging.credentials`, `…credential-offer`, `…token`, `…nonce`, `…issuer-metadata`, `…status-list.list`, `…status-list.revoke`, `…status-list.assignments` | The logging id of a flow, which is what you filter your logs by. |
+
### Discovery documents
A wallet finds the issuer at `/.well-known/openid-credential-issuer`. The document comes from
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialIssuanceConfiguration.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialIssuanceConfiguration.java
index 03bd2d2..cf13e63 100644
--- a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialIssuanceConfiguration.java
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialIssuanceConfiguration.java
@@ -52,6 +52,10 @@ public final class CredentialIssuanceConfiguration {
@Nullable
private final Boolean statusList;
+ /** Whether credentials of this configuration are stated in OpenID Federation. */
+ @Nullable
+ private final Boolean oidFederation;
+
/**
* Constructor.
*
@@ -61,14 +65,18 @@ public final class CredentialIssuanceConfiguration {
* or an instant
* @param inStatusList whether credentials of this configuration take a
* Status List slot
+ * @param inOIDFederation whether credentials of this configuration are
+ * stated in OpenID Federation
*/
@JsonCreator
private CredentialIssuanceConfiguration(@JsonProperty("lifetime") @Nullable final String credentialLifetime,
@JsonProperty("not_before") @Nullable final String validityStart,
- @JsonProperty("status_list") @Nullable final Boolean inStatusList) {
+ @JsonProperty("status_list") @Nullable final Boolean inStatusList,
+ @JsonProperty("oidfed") @Nullable final Boolean inOIDFederation) {
lifetime = parseLifetime(credentialLifetime);
notBefore = validityStart == null || validityStart.trim().isEmpty() ? null : ValidityStart.parse(validityStart);
statusList = inStatusList;
+ oidFederation = inOIDFederation;
}
/**
@@ -101,6 +109,17 @@ public final class CredentialIssuanceConfiguration {
return statusList;
}
+ /**
+ * Get whether credentials of this configuration are stated in OpenID
+ * Federation, carrying the Entity Identifier of this deployment.
+ *
+ * @return the flag, or null when the configuration states none
+ */
+ @Nullable
+ public Boolean getOIDFederation() {
+ return oidFederation;
+ }
+
/**
* Resolve the instant the validity window of a credential opens.
*
diff --git a/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialIssuanceConfigurationTest.java b/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialIssuanceConfigurationTest.java
index 00d0f42..d4882ac 100644
--- a/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialIssuanceConfigurationTest.java
+++ b/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialIssuanceConfigurationTest.java
@@ -117,6 +117,17 @@ public class CredentialIssuanceConfigurationTest {
.getIssuanceConfiguration().getStatusList(), Boolean.FALSE);
}
+ @Test
+ public void openIDFederation() throws JsonProcessingException {
+
+ Assert.assertNull(CredentialConfiguration.parse(withSettings("{\"lifetime\":\"P7D\"}"))
+ .getIssuanceConfiguration().getOIDFederation());
+ Assert.assertEquals(CredentialConfiguration.parse(withSettings("{\"oidfed\":true}"))
+ .getIssuanceConfiguration().getOIDFederation(), Boolean.TRUE);
+ Assert.assertEquals(CredentialConfiguration.parse(withSettings("{\"oidfed\":false}"))
+ .getIssuanceConfiguration().getOIDFederation(), Boolean.FALSE);
+ }
+
@Test
public void emptyValuesAreAbsentValues() throws JsonProcessingException {
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/oidfed/impl/CredentialIssuerEntityConfigurationMetadataDecorator.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/oidfed/impl/CredentialIssuerEntityConfigurationMetadataDecorator.java
index b4fb3ff..9350a4d 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/oidfed/impl/CredentialIssuerEntityConfigurationMetadataDecorator.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/oidfed/impl/CredentialIssuerEntityConfigurationMetadataDecorator.java
@@ -117,6 +117,13 @@ public class CredentialIssuerEntityConfigurationMetadataDecorator extends Abstra
*/
private boolean decorateFederationEntity = true;
+ /**
+ * Entity Identifier this deployment publishes as 'iss' and 'sub' of its Entity
+ * Configuration, for comparing with the Credential Issuer.
+ */
+ @Nullable
+ private String entityIdentifier;
+
/**
* Set the resolver of the Credential Issuer metadata to state.
*
@@ -170,6 +177,21 @@ public class CredentialIssuerEntityConfigurationMetadataDecorator extends Abstra
}
}
+ /**
+ * Set the Entity Identifier this deployment publishes in its Entity
+ * Configuration.
+ *
+ * The federation module decides that value, this decorator only reads it to
+ * say so when it differs from the Credential Issuer. Nothing here is able to
+ * change it.
+ *
+ * @param entityId the Entity Identifier, or null to compare nothing
+ */
+ public void setEntityIdentifier(@Nullable final String entityId) {
+ checkSetterPreconditions();
+ entityIdentifier = entityId == null || entityId.isEmpty() ? null : entityId;
+ }
+
/** {@inheritDoc} */
@Override
public void accept(@Nullable final Map<String, Map<String, Object>> metadata,
@@ -193,6 +215,8 @@ public class CredentialIssuerEntityConfigurationMetadataDecorator extends Abstra
return;
}
+ warnOfIdentifierMismatch(credentialIssuer);
+
decorate(metadata, OPENID_CREDENTIAL_ISSUER, credentialIssuer.getMembers());
final List<Object> keys = credentialSigningKeys();
@@ -277,6 +301,31 @@ public class CredentialIssuerEntityConfigurationMetadataDecorator extends Abstra
* @param entityType entity type to write
* @param members members to write
*/
+ /**
+ * Say so when the Entity Identifier of this deployment is not the
+ * 'credential_issuer' of its Credential Issuer metadata.
+ *
+ * OpenID Federation Digital Credentials Profile requires the two to be the
+ * same. They come from two modules and nothing joins them up, so a deployment
+ * is able to publish an Entity Configuration that a credential of its own
+ * points away from. Then a trust chain dies at its first hop.
+ *
+ * @param credentialIssuer the Credential Issuer metadata
+ */
+ private void warnOfIdentifierMismatch(@Nonnull final CredentialIssuerMetadata credentialIssuer) {
+
+ if (entityIdentifier == null || credentialIssuer.getCredentialIssuer() == null) {
+ return;
+ }
+
+ final String issuer = credentialIssuer.getCredentialIssuer().getValue();
+ if (!entityIdentifier.equals(issuer)) {
+ log.error("Entity Identifier {} of this deployment is not the '{}' {} of its Credential Issuer "
+ + "metadata. A credential states the latter, so a verifier resolving it reaches the wrong "
+ + "entity.", entityIdentifier, CredentialIssuerMetadata.CREDENTIAL_ISSUER, issuer);
+ }
+ }
+
private void decorate(@Nonnull final Map<String, Map<String, Object>> metadata, @Nonnull final String entityType,
@Nonnull final Map<String, Object> members) {
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
index acb9c93..08d269d 100644
--- 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
@@ -31,10 +31,15 @@ import net.shibboleth.idp.profile.IdPEventIds;
import net.shibboleth.profile.config.ProfileConfiguration;
import net.shibboleth.profile.context.RelyingPartyContext;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.ResolverException;
+import org.geant.shibboleth.plugin.openidvci.credential.CredentialIssuanceConfiguration;
+import org.geant.shibboleth.plugin.openidvci.metadata.CredentialIssuerMetadata;
+import org.geant.shibboleth.plugin.openidvci.metadata.resolver.CredentialIssuerMetadataResolver;
import org.geant.shibboleth.plugin.openidvci.profile.logic.CredentialIssuerLookupFunction;
import org.geant.shibboleth.plugin.openidvci.statuslist.context.StatusListContext;
import org.geant.shibboleth.plugin.openidvci.profile.config.OpenIDVCIConfiguration;
@@ -62,6 +67,11 @@ import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
*/
public class AddCredentialShell extends AbstractProfileAction {
+ /** Value of the 'type' member of 'termsOfUse'. */
+ @Nonnull
+ @NotEmpty
+ public static final String OPENID_FEDERATION = "OpenIDFederation";
+
/** Class logger. */
@Nonnull
private Logger log = LoggerFactory.getLogger(AddCredentialShell.class);
@@ -85,6 +95,22 @@ public class AddCredentialShell extends AbstractProfileAction {
@Nullable
private RelyingPartyContext rpCtx;
+ /**
+ * Whether a credential states the Entity Identifier of this deployment in
+ * OpenID Federation.
+ */
+ private boolean stateEntityIdentifier;
+
+ /** Resolver of the Credential Issuer metadata the Entity Identifier is read from. */
+ @Nullable
+ private CredentialIssuerMetadataResolver metadataResolver;
+
+ /**
+ * Entity Identifier of this deployment, null when a credential states none.
+ */
+ @Nullable
+ private String entityIdentifier;
+
/** Issuance instant shared by all credentials of this request. */
@Nullable
private Instant issuedAt;
@@ -141,6 +167,37 @@ public class AddCredentialShell extends AbstractProfileAction {
issuerLookupStrategy = Constraint.isNotNull(strategy, "IssuerLookupStrategy lookup strategy cannot be null");
}
+ /**
+ * Set whether a credential states the Entity Identifier of this deployment in
+ * OpenID Federation.
+ *
+ * A credential states it so that a verifier is able to start a trust chain
+ * from the credential itself. It follows from this deployment publishing an
+ * Entity Configuration of its own, a credential of a deployment outside a
+ * federation has no business claiming one.
+ *
+ * @param flag whether a credential states the Entity Identifier
+ */
+ public void setStateEntityIdentifier(final boolean flag) {
+ ifInitializedThrowUnmodifiabledComponentException();
+ stateEntityIdentifier = flag;
+ }
+
+ /**
+ * Set the resolver of the Credential Issuer metadata.
+ *
+ * The Entity Identifier is the 'credential_issuer' member of that metadata,
+ * as OpenID Federation Digital Credentials Profile requires the two to be the
+ * same. It is read from there rather than configured, so that the credential
+ * and the metadata are unable to disagree.
+ *
+ * @param resolver resolver of the Credential Issuer metadata
+ */
+ public void setMetadataResolver(@Nullable final CredentialIssuerMetadataResolver resolver) {
+ ifInitializedThrowUnmodifiabledComponentException();
+ metadataResolver = resolver;
+ }
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -190,6 +247,15 @@ public class AddCredentialShell extends AbstractProfileAction {
return false;
}
+ if (stateEntityIdentifier) {
+ entityIdentifier = resolveEntityIdentifier(profileRequestContext);
+ if (entityIdentifier == null) {
+ log.error("{} Unable to resolve the Entity Identifier of this deployment", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ }
+ }
+
issuerId = issuerLookupStrategy.apply(profileRequestContext);
if (issuerId == null) {
log.error("{} Unable to determine issuer to check audience", getLogPrefix());
@@ -253,6 +319,58 @@ public class AddCredentialShell extends AbstractProfileAction {
return null;
}
+ /**
+ * Read the Entity Identifier of this deployment from the Credential Issuer
+ * metadata.
+ *
+ * @param profileRequestContext current profile request context
+ * @return the Entity Identifier, or null if it cannot be resolved
+ */
+ @Nullable
+ private String resolveEntityIdentifier(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (metadataResolver == null) {
+ log.error("{} No Credential Issuer metadata resolver", getLogPrefix());
+ return null;
+ }
+
+ final CredentialIssuerMetadata metadata;
+ try {
+ metadata = metadataResolver.resolveSingle(profileRequestContext);
+ } catch (final ResolverException e) {
+ log.error("{} Could not resolve Credential Issuer metadata", getLogPrefix(), e);
+ return null;
+ }
+ if (metadata == null || metadata.getCredentialIssuer() == null) {
+ log.error("{} Credential Issuer metadata states no '{}'", getLogPrefix(),
+ CredentialIssuerMetadata.CREDENTIAL_ISSUER);
+ return null;
+ }
+
+ return metadata.getCredentialIssuer().getValue();
+ }
+
+ /**
+ * Whether the credential in issue states the Entity Identifier of this
+ * deployment.
+ *
+ * It does when the deployment has one and the credential configuration does
+ * not opt out of it.
+ *
+ * @return true if the credential is stated in OpenID Federation
+ */
+ private boolean inOIDFederation() {
+
+ if (entityIdentifier == null) {
+ return false;
+ }
+ final CredentialIssuanceConfiguration settings =
+ ctx.getCredentialConfiguration().getIssuanceConfiguration();
+ final Boolean flag = settings == null ? null : settings.getOIDFederation();
+
+ return flag == null || flag.booleanValue();
+ }
+
/**
* Create the shell for credential.
*
@@ -271,10 +389,16 @@ public class AddCredentialShell extends AbstractProfileAction {
shell.setClaim("exp", expiresAt.getEpochSecond());
if ("dc+sd-jwt".equals(ctx.getCredentialConfiguration().getFormat())) {
shell.setClaim("vct", ctx.getCredentialConfiguration().getVct());
+ if (inOIDFederation()) {
+ shell.setClaim("fed", entityIdentifier);
+ }
}
if ("vc+sd-jwt".equals(ctx.getCredentialConfiguration().getFormat())) {
shell.setClaim("validFrom", DateTimeFormatter.ISO_INSTANT.format(notBefore));
shell.setClaim("validUntil", DateTimeFormatter.ISO_INSTANT.format(expiresAt));
+ if (inOIDFederation()) {
+ shell.setClaim("termsOfUse", Map.of("type", OPENID_FEDERATION, "policyId", entityIdentifier));
+ }
}
if (statusListContext != null && statusListContext.getStatusListUri() != null) {
final Long index = statusListContext.getIndex(ordinal);
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 c2b8651..ed1d67f 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
@@ -143,7 +143,9 @@
p:issuer="%{openidvci.issuer:}" />
<bean id="AddCredentialShell" class="org.geant.shibboleth.plugin.openidvci.profile.impl.AddCredentialShell"
- scope="prototype" p:issuerLookupStrategy-ref="openidvci.IssuerLookupStrategy" />
+ scope="prototype" p:issuerLookupStrategy-ref="openidvci.IssuerLookupStrategy"
+ p:stateEntityIdentifier="#{getObject('openidvci.oidfed.CredentialIssuerEntityConfigurationMetadataDecorator') != null}"
+ p:metadataResolver-ref="#{'%{openidvci.issuerMetadata.resolver:openidvci.DefaultCredentialIssuerMetadataResolver}'.trim()}" />
<bean id="OptionallyFormSelectiveDisclosureJWTCredential" class="org.geant.shibboleth.plugin.openidvci.profile.impl.FormSelectiveDisclosureJWTCredential"
scope="prototype" />
diff --git a/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci-oidfed.xml b/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci-oidfed.xml
index f9a92dc..b200f3a 100644
--- a/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci-oidfed.xml
+++ b/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci-oidfed.xml
@@ -14,6 +14,7 @@
p:id="openidvci.oidfed.CredentialIssuerEntityConfigurationMetadataDecorator"
p:metadataResolver-ref="#{'%{openidvci.issuerMetadata.resolver:openidvci.DefaultCredentialIssuerMetadataResolver}'.trim()}"
p:credentialSigningCredentials="#{getObject('openidvci.SigningCredentials')}"
- p:decorateFederationEntity="%{openidvci.oidfed.decorateFederationEntity:true}" />
+ p:decorateFederationEntity="%{openidvci.oidfed.decorateFederationEntity:true}"
+ p:entityIdentifier="%{idp.oidfed.entityID:%{idp.entityID}}" />
</beans>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list