[java-idp-plugin-vci] branch dev/REFRESH updated: Update README for refresh tokens, audience changes and new attribute resolution at the token endpoint
Codeberg
noreply at shibboleth.net
Wed Nov 4 11:16:21 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch dev/REFRESH
in repository java-idp-plugin-vci.
View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-vci/commit/f3a80fcdba8d08b13ef4860228aecb5188a76938
The following commit(s) were added to refs/heads/dev/REFRESH by this push:
new f3a80fc Update README for refresh tokens, audience changes and new attribute resolution at the token endpoint
f3a80fc is described below
commit f3a80fcdba8d08b13ef4860228aecb5188a76938
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Wed Nov 4 13:15:54 2026 +0200
Update README for refresh tokens, audience changes and new attribute resolution at the token endpoint
---
README.md | 617 +++++++++++++++-----------
docs/example/step-1/metadata/oidc-client.json | 3 +-
docs/example/step-2/metadata/oidc-client.json | 3 +-
docs/example/step-3/metadata/oidc-client.json | 6 +-
docs/example/step-4/metadata/oidc-client.json | 6 +-
docs/example/step-5/conf/relying-party.xml | 1 +
docs/example/step-5/metadata/oidc-client.json | 4 +-
7 files changed, 375 insertions(+), 265 deletions(-)
diff --git a/README.md b/README.md
index a6ce905..c67296c 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,9 @@
- [Complex Credential Configurations](#complex-credential-configurations)
- [Resolving complex values](#resolving-complex-values)
- [Getting the types right](#getting-the-types-right)
+- [Refresh tokens, attribute resolution in token endpoint](#refresh-tokens-attribute-resolution-in-token-endpoint)
+ - [Refresh tokens](#refresh-tokens)
+ - [Attribute resolution in token endpoint](#attribute-resolution-in-token-endpoint)
- [Reference](#reference)
- [Profile configuration settings](#profile-configuration-settings)
- [Credential Configuration settings](#credential-configuration-settings)
@@ -50,7 +53,7 @@
## Overview
This plugin implements [OpenID for Verifiable Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html)
-for Shibboleth Identity Provider (IdP). It enables the IdP to act as a Credential Issuer. A wallet
+for Shibboleth Identity Provider (IdP). It allows the IdP to act as a Credential Issuer. A wallet
gets an access token from the IdP and exchanges it for a Verifiable Credential containing IdP attributes.
Our first target is the [Decentralized Identity Interop Profile 5](https://fidescommunity.github.io/DIIP/).
@@ -72,7 +75,7 @@ The plugin issues credentials in three formats: `dc+sd-jwt` (IETF SD-JWT VC), `v
(ISO mdoc). All three support selective disclosure. Revocation information is published through
the [IETF Token Status List](https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/).
-Things you need to know about this add-on:
+A few things to know about this add-on:
- It is not certified. Do not deploy it in production.
- Credentials are signed with EC keys only.
@@ -128,7 +131,7 @@ The following optional features are not currently supported.
### [OpenID4VC High Assurance Interoperability Profile 1.0](https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0-final.html)
-| Requirement | Status |
+| Feature | Status |
|---|---|
| Authorization Code Flow | Yes |
| Format IETF SD-JWT VC, `dc+sd-jwt` | Yes |
@@ -146,7 +149,7 @@ The following optional features are not currently supported.
| Key attestation in a `jwt` proof, `key_attestation` | Yes |
| Proof type `attestation` | Yes |
| Batch issuance advertised by `batch_credential_issuance` | Yes |
-| Refresh tokens | No |
+| Refresh tokens | Yes |
| SD-JWT VC with `exp` and `status` | Yes |
| Holder key in `cnf` `jwk` member | Yes |
| Issuer key as `x5c` chain in the SD-JWT VC | Yes |
@@ -160,9 +163,9 @@ The following optional features are not currently supported.
## Plugin Installation
> **Info**
-> This plugin is not released. Until it is released,
-> the distribution is in directory *download/* (only seldom updated) of this repository and you install it from
-> there by URL, see below.
+> This plugin has not been released yet. Until then,
+> the distribution is in this repository’s *download/* directory, which is updated infrequently. You can
+> install it from there using a URL, as described below.
| Plugin | Plugin ID | Module(s) | Depends on | Bug Reporting |
|---|---|---|---|---|
@@ -182,10 +185,10 @@ Requires IdP **5.2.3** or later.
> 4. OpenID Connect OP (`idp-plugin-oidc-op`)
> 5. OpenID Connect OP Federation Plugin (`idp-plugin-oidfed-op`)
>
-> Install them by
+> Install them by following
> [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.
+> on the Shibboleth wiki. It lists the snapshot locations and installation commands for all five. Follow
+> that page to install everything this plugin needs.
>
> The two OpenID Federation plugins of the list are needed only for
> [Step 4](#step-4-make-the-url-credential-an-openid-federation-credential), where
@@ -203,8 +206,8 @@ signature, and the signing key are in this repository's *download/* directory:
| *shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz.asc* | Signature of the distribution. `plugin.sh` fetches it from the same directory automatically. |
| *keys.txt* | The key that signed the distribution. It becomes the truststore of the plugin. |
-The archive is uploaded manually and is updated infrequently. It is not implemented for every commit,
-and there is no nightly build. Therefore, the archive may not contain the latest repository changes.
+The archive is uploaded manually and updated infrequently. It is not built for every commit, and there
+is no nightly build. Therefore, it may not contain the latest changes in the repository.
If you need a newer version, build the plugin yourself.
<details>
@@ -233,7 +236,7 @@ Installing the plugin activates module `OpenID-VCI` and creates six files:
|---|---|
| *conf/openid-vci.properties* | Every deployer-facing property.|
| *conf/openid-vci-credentials.xml* | Which key signs credentials, Status List Tokens and issuer metadata. Not imported by default. |
-| *conf/openid-vci-oidfed.xml* | States this deployment as a Credential Issuer in OpenID Federation. Not imported by default. |
+| *conf/openid-vci-oidfed.xml* | Declares this deployment as a Credential Issuer in OpenID Federation. Not imported by default. |
| *static/openid-credential-issuer.json* | Credential Issuer metadata, as a Velocity template. |
| *static/jwt-vc-issuer.json* | Keys that sign the credentials, for a verifier. As a Velocity template. |
| *static/oauth-authorization-server.json* | Authorization server metadata of the VCI endpoints, for a wallet. As a Velocity template. |
@@ -246,8 +249,8 @@ Credential Configurations, and the plugin does not provide a template. See
Steps 1–4 build a DIIP deployment. Step 5 describes a separate HAIP deployment. Each of Steps 2–4
adds to the setup from the previous step and explains only the changes. Step 5 starts again from
-Step 1; it does not depend on Steps 2–4. The examples come from a working test deployment, so they
-are consistent with each other, but they may need changes for your environment.
+Step 1; it does not depend on Steps 2–4. The examples are based on a working test deployment, so they
+are consistent with one another. You may still need to adapt them to your environment.
Before you start, make sure the OP has an EC signing key. See
[Key Generation](https://shibboleth.atlassian.net/wiki/spaces/IDPPLUGINS/pages/1376878976/OIDC+OP#Key-Generation)
@@ -256,16 +259,16 @@ in the OIDC OP wiki. A standard installation of the OP plugin includes
1. A did:jwk credential with both flows and the Status List. Signed with the key of the OP.
2. A second credential, did:web.
-3. A third credential, the issuer is the plain URL.
+3. A third credential, with a plain URL as the issuer.
4. The third credential is made an OpenID Federation credential.
[What we have now](#what-we-have-now) sums up the DIIP setup.
5. A HAIP setup on its own, two credentials as SD-JWT VC and as mdoc. The signing key has a
certificate chain.
-6. Rolling the signing key to the next one, in either setup.
+6. Rotating the signing key, in either setup.
-Steps 1 and 2 use the [Paradym](https://paradym.id) wallet. Paradym accepts only `did` and `x5c`
-issuers, so use the TOPPAN wallet for the URL-based credential in Steps 3 and 4. Use the Hopae EUDI
-Wallet for the HAIP credentials in Step 5. Those credentials have also been tested with the OpenID
+Steps 1 and 2 use the [Paradym](https://paradym.id) wallet. In our tests Paradym accepted only `did`
+and `x5c` issuers, so use the TOPPAN wallet for the URL-based credential in Steps 3 and 4. Use the Hopae EUDI
+Wallet for the HAIP credentials in Step 5. We have also tested those credentials with the OpenID
conformance suite.
`https://issuer.example.org` is your IdP's OIDC issuer everywhere below.
@@ -287,7 +290,7 @@ metadata and uses it to build credentials.
The specification defines this file's structure in
[Credential Issuer Metadata](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata),
-with details that depend on the credential format. The notes below explain only the members that need clarification.
+with details that depend on the credential format. The notes below clarify only the members that may need explanation.
<details>
<summary>metadata/verifiable-credentials.json</summary>
@@ -412,7 +415,7 @@ only `dc+sd-jwt` credentials.
##### The W3C credential
> **Note**
-> If you are reading this for the first time and your energy levels are not top notch, **just skip
+> If you are reading this for the first time and are not feeling particularly energetic, **just skip
> to [Settings of your own, per credential](#settings-of-your-own-per-credential)**. SD-JWT VC is
> much simpler to set up, and it is what all the following steps use.
@@ -446,12 +449,12 @@ Full file: [webapps/root/credentials/GeantIncubatorDiploma_W3C.json](docs/exampl
</details>
-You publish it at the url that is in `@context`, see [Metadata endpoints](#metadata-endpoints)
+Publish it at the URL specified in `@context`; see [Metadata endpoints](#metadata-endpoints)
below.
`@context` and [`type`](https://www.w3.org/TR/vc-data-model-2.0/#types) are required and
-published. The members below are optional and go to the credential only. Their content is yours,
-the plugin copies it to every credential as it is.
+published. The following members are optional and appear only in the credential. You define their
+contents, and the plugin copies them unchanged into every credential.
| Member | Here |
|---|---|
@@ -460,17 +463,17 @@ the plugin copies it to every credential as it is.
| [`credentialSchema`](https://www.w3.org/TR/vc-data-model-2.0/#data-schemas) | A [JSON Schema](https://www.w3.org/TR/vc-json-schema/#jsonschema) of the credential, a static file next to the context |
| [`termsOfUse`](https://www.w3.org/TR/vc-data-model-2.0/#terms-of-use) | A policy of your own, VCDM defines no types for it |
| [`relatedResource`](https://www.w3.org/TR/vc-data-model-2.0/#integrity-of-related-resources) | [Subresource Integrity](https://www.w3.org/TR/SRI/) digests of the context and the schema |
-| [`evidence`](https://www.w3.org/TR/vc-data-model-2.0/#evidence) | How the diploma was granted, the same for everyone |
+| [`evidence`](https://www.w3.org/TR/vc-data-model-2.0/#evidence) | How the diploma was granted; the same for everyone |
The plugin adds [`id`](https://www.w3.org/TR/vc-data-model-2.0/#identifiers), [`issuer`](https://www.w3.org/TR/vc-data-model-2.0/#issuer),
[`validFrom` and `validUntil`](https://www.w3.org/TR/vc-data-model-2.0/#validity-period) and
-[`credentialSubject`](https://www.w3.org/TR/vc-data-model-2.0/#credential-subject) itself. You cannot configure them. `termsOfUse` is
-the one member both of you can set. When a `vc+sd-jwt` credential is in a federation, see
+[`credentialSubject`](https://www.w3.org/TR/vc-data-model-2.0/#credential-subject) itself. You cannot configure these members. `termsOfUse` is
+the only member that both you and the plugin can set. When a `vc+sd-jwt` credential is in a federation, see
[Step 4](#step-4-make-the-url-credential-an-openid-federation-credential), the plugin adds a
federation entry and your entries go in the same array. See
[Credential Configuration settings](#credential-configuration-settings) for the full list.
-The schema of the example:
+The example schema:
<details>
<summary>webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json</summary>
@@ -540,7 +543,7 @@ It goes inside the credential's own configuration, next to `format` and `vct`:
| `claim_types` | Array | | Type of a claim value in an mdoc where JSON has none, a date for example, `{"path": [...], "type": ...}`. See [The mdoc credential](#the-mdoc-credential) and [Complex claims](#complex-claims). |
| `sd` | Array | | Arrays disclosed element by element as in [Disclosures for Array Elements](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/) of SD-JWT, `{"path": [..., null], "sd": "always"}`. See [Complex claims](#complex-claims). |
-All of them are optional and so is the member itself.
+All of these settings are optional, as is the `_issuance_configuration` member itself.
##### The validity window
@@ -553,7 +556,7 @@ Write `not_before` as one of these:
|---|---|---|
| `P30D`, `PT12H` | That much after the credential was issued | A window of the same length for everyone, whenever they ask |
| `2026-08-01T06:00Z` | At exactly that minute, `Z` being UTC | A moment you decide, the start of a school year |
-| `2026-08-01T00:00+03:00` | The same, in the offset you state | Well, if UTC is too hard for you. I may remove this. |
+| `2026-08-01T00:00+03:00` | The same, in the offset you specify | Use this if UTC is too difficult for you. I may remove this option. |
| `2026-08-01` | At midnight UTC of that day | The same, when the time of day does not matter |
What ends up in the credential:
@@ -561,8 +564,8 @@ What ends up in the credential:
| Format | Members carrying the window |
|---|---|
| `dc+sd-jwt` | `iat` is the instant of issuance, `exp` closes the window, and `nbf` opens it whenever that is not the instant of issuance |
-| `vc+sd-jwt` | The same three, and [`validFrom` and `validUntil`](https://www.w3.org/TR/vc-data-model-2.0/#validity-period) as the same two ends of the window |
-| `mso_mdoc` | `validityInfo` of the mobile security object, `signed` is the instant of issuance, `validFrom` and `validUntil` the two ends of the window |
+| `vc+sd-jwt` | The same three, plus [`validFrom` and `validUntil`](https://www.w3.org/TR/vc-data-model-2.0/#validity-period), which represent the same start and end of the window |
+| `mso_mdoc` | `validityInfo` of the mobile security object: `signed` is the instant of issuance, and `validFrom` and `validUntil` are the two ends of the window |
If the validity window starts at issuance, the credential has no `nbf` claim. By default, the
issuance timestamp is rounded down to the start of the hour. To use a different precision, set
@@ -572,7 +575,7 @@ issuance timestamp is rounded down to the start of the hour. To use a different
openidvci.credentialTimestampPrecision = PT15M
```
-Set this precision to a value shorter than the shortest `lifetime` you issue. Otherwise, the
+Use a precision shorter than the shortest `lifetime` you issue. Otherwise, the
validity window may end before the credential is issued, and issuance will fail.
##### Whether a credential goes on a Status List
@@ -580,7 +583,7 @@ validity window may end before the credential is issued, and issuance will fail.
`"status_list": false` keeps the credential off the list. It carries no `status` claim and it
cannot be revoked. Use it for short lived credentials.
-A credential that states nothing goes on the list. Set
+A credential with no explicit setting is added to the list. Set
`openidvci.status-list.includeByDefault` to `false` in *conf/openid-vci.properties*. Then only a
credential with `"status_list": true` goes on the list.
@@ -655,7 +658,7 @@ Full file: [conf/relying-party.xml](docs/example/step-1/conf/relying-party.xml)
</details>
-You do not need to enter your host name in the templates under *static/*. The variables `$issuer`,
+You do not need to enter your hostname in the templates under *static/*. The variables `$issuer`,
`$baseUrl`, and `$host` are populated from `idp.oidc.issuer` in *conf/oidc.properties*.
However, you must add the credential scopes to `scopes_supported` in
*static/oauth-authorization-server.json*. In the authorization code flow, the wallet requests a
@@ -806,7 +809,7 @@ is shown to the wallet, usually as a QR code. The credential's contents are spec
offer is created.
The component that creates the offer is a confidential client. The wallet that redeems the code
-uses a separate client IDentity. Register both clients:
+uses a separate client identity. Register both clients:
<details>
<summary>metadata/oidc-client.json</summary>
@@ -835,8 +838,8 @@ The `/wallet` suffix is fixed. A wallet redeeming a pre-authorized code does not
The plugin finds its client ID by appending `/wallet` to the client ID of the client that created
the offer.
-Then one relying party for the party that creates the content for credential and one for the
-wallet that redeems the code:
+Next, configure one relying party for the component that creates the credential content and another
+for the wallet that redeems the code:
<details>
<summary>conf/relying-party.xml</summary>
@@ -968,10 +971,10 @@ openid-credential-offer://?credential_offer=%7B%22credential_issuer%22:%22https:
</details>
This example requests both credentials in one offer. In practice, include only one credential
-per offer because many wallets cannot handle multiple credentials in a single offer.
+per offer. In our experience many wallets cannot handle several credentials in one offer.
-Note that the paths are flat in both, also in the `vc+sd-jwt` one. The `credentialSubject`
-nesting of that format is done when the credential is built.
+The paths are flat in both formats, including `vc+sd-jwt`. The plugin adds the `credentialSubject`
+nesting for that format when it builds the credential.
Every configuration ID in the request must exist in *metadata/verifiable-credentials.json*;
otherwise, the plugin rejects the offer. The plugin does not resolve attributes when the wallet
@@ -995,11 +998,11 @@ it once as a QR code:
openid-credential-offer://?credential_offer=%7B%22credential_issuer%22:%22https://issuer.example.org%22,%22credential_configuration_ids%22:%5B%22GeantIncubatorDiploma_SDJWT%22%5D,%22grants%22:%7B%22authorization_code%22:%7B%7D%7D%7D
```
-The `authorization_code` grant is what tells the wallet to run this flow. It is empty,
+The `authorization_code` grant is what tells the wallet to run this flow. The grant object is empty;
`issuer_state` is not supported. The `credential_issuer` value has to be the Credential Issuer
Identifier this deployment publishes, the same string as in *openid-credential-issuer.json*.
-The wallet is a normal OIDC client and uses a single client IDentity. Unlike the pre-authorized
+The wallet is a standard OIDC client and uses a single client identity. Unlike the pre-authorized
code flow, it does not need a second identity. Register it with the OP plugin's client metadata
resolver in the same *metadata/oidc-client.json* file as the clients above:
@@ -1025,8 +1028,7 @@ resolver in the same *metadata/oidc-client.json* file as the clients above:
+ "redirect_uris": ["https://paradym.id/invitation/redirect"],
+ "scope": "openid GeantIncubatorDiploma_SDJWT GeantIncubatorDiploma_W3C",
+ "audience": [
-+ "credentials",
-+ "urn:shibboleth.oidc.op.resource:https://issuer.example.org"
++ "https://issuer.example.org"
+ ],
+ "token_endpoint_auth_method": "none",
+ "response_types": ["code"],
@@ -1045,8 +1047,9 @@ Full file: [metadata/oidc-client.json](docs/example/step-1/metadata/oidc-client.
Check the `scope` value: it must include every configuration ID that wallets should be able to
request by scope. A wallet needs the pre-authorized code grant in `grant_types` only if it sends
its own `client_id` to the token endpoint during that flow. The example includes it to demonstrate
-this case. The `audience` must include `credentials` for wallets that omit `openid` from `scope`.
-Otherwise, the authorization endpoint rejects the request if the client has no audience configured.
+this case. The `audience` must include the Credential Issuer Identifier, the `credential_issuer` of
+the Credential Issuer metadata. Put it first in the list. Without it,
+the authorization endpoint rejects wallets that do not request `openid`.
The relying-party override needs four profile configurations: authorization, token, credential,
and pushed authorization request (PAR):
@@ -1151,7 +1154,7 @@ Full file: [conf/relying-party.xml](docs/example/step-1/conf/relying-party.xml)
</details>
-Wallets usually omit `openid` from `scope`, so they send a standard OAuth 2.0 authorization
+In our experience wallets omit `openid` from `scope`, so they send a standard OAuth 2.0 authorization
request. Setting `requireAuthenticationRequest="false"` on `OIDC.SSO` and `OAUTH2.PAR` allows
this request type. Without these settings, PAR fails with the log message
*The request did not match with the configured authorization request type requirements*.
@@ -1188,12 +1191,12 @@ This is the same structure used by the Credential Offer API. Define the attribut
</details>
-One attribute serves both credentials. Every `AttributeEncoder` of the definition becomes a
-claim of its own name, so the same claim list is found with either configuration ID and the
-script is written once. The encoder names are credential configuration IDs.
+One attribute serves both credentials. Each `AttributeEncoder` in the definition creates a claim named after the encoder. This means the
+wallet can retrieve the same claim list using either configuration ID, and you only need to write
+the script once. The encoder names are credential configuration IDs.
`idp.oidc.encodedAttributes` and the filter policy use the attribute id.
-Then you tell the OP plugin to embed the attribute into the access token in
+Then you tell the OP plugin to carry the attribute in the authorization code in
*conf/oidc.properties*:
```properties
@@ -1201,8 +1204,8 @@ Then you tell the OP plugin to embed the attribute into the access token in
idp.oidc.encodedAttributes = GeantIncubatorDiploma
```
-And you release it to the wallet in *conf/attribute-filter.xml*. This flow reads the
-**filtered** attribute set, so an attribute that is not released reaches nothing:
+Finally, release the attribute to the wallet in *conf/attribute-filter.xml*. This flow reads the
+**filtered** attribute set, so any attribute that is not released is omitted:
<details>
<summary>conf/attribute-filter.xml</summary>
@@ -1233,7 +1236,7 @@ The token endpoint of the code flow keeps only the credentials the wallet was au
configuration whose `scope` in the
[Credential Issuer metadata](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p) is in the granted scope, see
[Using scope Parameter](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-using-scope-parameter-to-re), or one the wallet asked with
-[`authorization_details`](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-using-authorization-details). Everything else released by the filter is dropped from
+[`authorization_details`](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-using-authorization-details). Any other attributes released by the filter are omitted from
the access token. The filter can be narrower than that, for example per scope or per client, but it
cannot widen it.
@@ -1263,19 +1266,19 @@ is told not to take one.
</details>
-Withdrawing that credential is setting bit 41337 of list `2027-03`. The list a credential lands
-in follows from the month the credential **expires**, not from the month it was issued. With the
-default `credentialLifetime` of `P180D` a credential that is issued in September 2026 belongs to
+To revoke this credential, set bit 41337 in the `2027-03` list. The list a credential belongs to
+is determined by the month it **expires**, not the month it was issued. With the
+default `credentialLifetime` of `P180D` a credential that is issued in mid-September 2026 belongs to
the list of March 2027.
-The part after `~` tells under which issuer ID the credential was issued. A Status List
-Token has to have the same issuer as the credential. For `did:jwk` the part is the thumbprint
-of the signing key, as the identifier is the key. For `did:web` it is `did-web` and for the plain
-url `url`. It is still one list per month, whatever the identifier.
+The part after `~` tells under which issuer ID the credential was issued. The plugin
+signs a Status List Token under the same issuer ID as the credentials on it. For `did:jwk` the part is the thumbprint
+of the signing key, because the identifier is the key. For `did:web`, it is `did-web`; for a plain
+URL, it is `url`. There is still one list per month, regardless of the identifier.
-The expiration is the one the credential actually gets, so a credential with a `lifetime` or a
-`not_before` of its own lands in the list its own window chooses. A `not_before` fixed to a date
-puts every holder of that credential in one list.
+The relevant expiration is the one actually assigned to the credential. A credential with its own
+`lifetime` or `not_before` is therefore added to the list selected by its own validity window. A
+`not_before` value fixed to a date puts every holder of that credential in the same list.
The Status List endpoints:
@@ -1305,10 +1308,10 @@ A Status List has bits and nothing else in it, so the mapping from a credential
in the assignment records. The credential endpoint writes one record per issuance that took
slots, and nothing in the IdP reads them back.
-Store the records in the system that knows who holds which credential, and call the revoke api
+Store the records in the system that tracks who holds each credential, and call the revoke API
from there.
-You collect the records to whatever system of yours knows who holds which credential:
+Collect the records in the system that tracks who holds each credential:
<details>
<summary>Example of collecting assignment records</summary>
@@ -1375,10 +1378,10 @@ Cache-Control: no-store
</details>
-The `id` is the month. You may give it also with the `~` part copied from the credential, a slot
-belongs to the month whatever the issuer ID of the credential.
+The `id` identifies the month. You can also include the `~` part copied from the credential. A slot
+belongs to the month regardless of the credential’s issuer ID.
-Revoking is idempotent and it goes one way only. Nothing in the plugin turns a slot back to
+Revocation is idempotent and irreversible. Nothing in the plugin turns a slot back to
valid. The revoke endpoint does **not** check that the caller issued the credential that holds
the slot. Any client that has **OAuth.StatusList.Revoke** enabled may revoke any slot of any
list, so enable it in the relying party that owns the issuance and nowhere else.
@@ -1414,18 +1417,18 @@ Its claims, `lst` being the compressed list itself:
</details>
-A list is published until its month is over. Asking for a month that has ended is
-`410 retired_status_list` and asking for something that is not a month at all is
+Each list remains available until the end of its month. A request for a month that has ended returns
+`410 retired_status_list`; a request for a value that is not a month returns
`404 unknown_status_list`.
### Step 2: Add a did:web credential
-Next we add a credential whose issuer is the `did:web` of this deployment,
+Next, add a credential whose issuer is the `did:web` identifier of this deployment,
`did:web:issuer.example.org`. The identifier is formed from the host of `idp.oidc.issuer`. Unlike
-did:jwk it stays the same when the signing key changes, which matters in
+`did:jwk`, it stays the same when the signing key changes. This matters in
[Step 6](#step-6-rolling-the-signing-key).
-What is new:
+What’s new:
- the credential states `"issuer": "did:web"` in its `_issuance_configuration`
- the did:web document is published at `/.well-known/did.json`
@@ -1433,7 +1436,7 @@ What is new:
- the Status List url of the credential ends with `~did-web`
- the plugin gets a signing key list of its own, with the EC key of the OP only
-Each file as it is after this step, the changes marked:
+The files below show their state after this step, with changes marked:
<details>
<summary>metadata/verifiable-credentials.json</summary>
@@ -1773,8 +1776,7 @@ Full file: [etc/jetty-rewrite-rules.xml](docs/example/step-2/etc/jetty-rewrite-r
- "scope": "openid GeantIncubatorDiploma_SDJWT GeantIncubatorDiploma_W3C",
+ "scope": "openid GeantIncubatorDiploma_SDJWT GeantIncubatorDiploma_W3C GeantIncubatorDiploma_DIDWEB",
"audience": [
- "credentials",
- "urn:shibboleth.oidc.op.resource:https://issuer.example.org"
+ "https://issuer.example.org"
],
"token_endpoint_auth_method": "none",
"response_types": ["code"],
@@ -1872,10 +1874,10 @@ Full file: [conf/openid-vci-credentials.xml](docs/example/step-2/conf/openid-vci
</details>
-A key needs a name to be listed, the `kid` of a JWK key. The EC key of the OP has one.
+A key needs a name to be listed: the `kid` of its JWK. The OP’s EC key already has one.
-Import the file into *conf/global.xml*. The Entity Configuration of
-[Step 4](#step-4-make-the-url-credential-an-openid-federation-credential) does not see beans of
+Import the file into *conf/global.xml*. The Entity Configuration in
+[Step 4](#step-4-make-the-url-credential-an-openid-federation-credential) cannot access beans defined in
*conf/credentials.xml*:
```xml
@@ -1917,7 +1919,7 @@ attribute definition of Step 1. The claims are the same, so the script does not
</details>
-And the offer of the pre-authorized code flow has the new id:
+Update the pre-authorized code offer to use the new ID:
```bash
curl -u 'offer-caller:CHANGE-ME' \
@@ -1939,25 +1941,24 @@ curl -u 'offer-caller:CHANGE-ME' \
### Step 3: Add a plain URL-based credential
-Third credential has the value of `idp.oidc.issuer` as its issuer, `https://issuer.example.org`. A
-verifier finds the key from JWT VC issuer metadata, `/.well-known/jwt-vc-issuer`, which points to
-the keyset of the OP.
+The third credential uses the value of `idp.oidc.issuer` as its issuer: `https://issuer.example.org`. A
+verifier obtains the key from the JWT VC issuer metadata at `/.well-known/jwt-vc-issuer`, which points to
+the OP’s keyset.
-What is new:
+What’s new:
- the credential states `"issuer": "url"`
- JWT VC issuer metadata is published at `/.well-known/jwt-vc-issuer`
- the credential header `kid` is the plain name of the key, as in the keyset of the OP
-- the Status List url of the credential ends with `~url`
+- the Status List URL of the credential ends with `~url`
- the TOPPAN wallet is a second client, with a relying party and an attribute release of its own
-Paradym does not take a credential whose issuer is a plain URL, it accepts did and x5c issuers
-only. The URL-based credential is fetched with TOPPAN instead. TOPPAN sends the client ID
-`wallet-dev` and the redirect uri `walletapp-goid-openid4vci-callback://authorization`, and
-these are what the client entry has. TOPPAN binds the PAR to its DPoP key already, so the code
-and the access token are bound to that key.
+In our tests Paradym did not accept credentials whose issuer is a plain URL, so the URL-based
+credential is fetched with TOPPAN instead. In our tests TOPPAN sent the client ID `wallet-dev` and the
+redirect URI `walletapp-goid-openid4vci-callback://authorization`, so the client entry has these
+values.
-Each file as it is after this step, the changes marked:
+The files below show their state after this step, with changes marked:
<details>
<summary>metadata/verifiable-credentials.json</summary>
@@ -2127,7 +2128,7 @@ Full file: [metadata/verifiable-credentials.json](docs/example/step-3/metadata/v
</details>
-`url` stands for the value of `idp.oidc.issuer`. You may also write the url itself.
+`url` stands for the value of `idp.oidc.issuer`. You can also specify the URL itself.
<details>
<summary>conf/relying-party.xml</summary>
@@ -2354,8 +2355,7 @@ Full file: [etc/jetty-rewrite-rules.xml](docs/example/step-3/etc/jetty-rewrite-r
- "scope": "openid GeantIncubatorDiploma_SDJWT GeantIncubatorDiploma_W3C GeantIncubatorDiploma_DIDWEB",
+ "scope": "openid GeantIncubatorDiploma_SDJWT GeantIncubatorDiploma_W3C GeantIncubatorDiploma_DIDWEB GeantIncubatorDiploma_URL",
"audience": [
- "credentials",
- "urn:shibboleth.oidc.op.resource:https://issuer.example.org"
+ "https://issuer.example.org"
],
"token_endpoint_auth_method": "none",
"response_types": ["code"],
@@ -2371,8 +2371,7 @@ Full file: [etc/jetty-rewrite-rules.xml](docs/example/step-3/etc/jetty-rewrite-r
+ "redirect_uris": ["walletapp-goid-openid4vci-callback://authorization"],
+ "scope": "openid GeantIncubatorDiploma_URL",
+ "audience": [
-+ "credentials",
-+ "urn:shibboleth.oidc.op.resource:https://issuer.example.org"
++ "https://issuer.example.org"
+ ],
+ "token_endpoint_auth_method": "none",
+ "response_types": ["code"],
@@ -2476,25 +2475,25 @@ Attribute encoder and offer as in Step 2, with `GeantIncubatorDiploma_URL`.
### Step 4: Make the URL-based credential an OpenID Federation credential
-No new credential in this step. The deployment is stated as a Credential Issuer in OpenID
-Federation and the URL-based credential of Step 3 carries its Entity Identifier. The federation itself, trust anchors,
-authority hints and so on, is configured in the plugins of [Dependencies](#dependencies), see
+No new credential is added in this step. The deployment is declared as a Credential Issuer in OpenID
+Federation, and the URL-based credential from Step 3 includes its Entity Identifier. The federation setup, including trust anchors and
+authority hints, is configured in the plugins listed under [Dependencies](#dependencies). See
[OIDFEDCommon](https://shibboleth.atlassian.net/wiki/spaces/IDPPLUGINS/pages/5601296385/OIDFEDCommon).
-What is new:
+What’s new:
- the Entity Configuration at `/.well-known/openid-federation` has `openid_credential_issuer`
metadata and the credential signing keys
-- the URL-based credential carries `"fed": "https://issuer.example.org"`, the other credentials not
+- the URL-based credential carries `"fed": "https://issuer.example.org"`; the other credentials do not
-The URL-based credential is still fetched with TOPPAN, the client of Step 3.
+The URL-based credential is still requested using TOPPAN, the wallet from Step 3.
The Entity Configuration publishes the keys of **openidvci.SigningCredentials** of Step 2. If
the federation file is imported and the list is not defined, the IdP does not start.
#### Federation
-The Entity Identifier and `credential_issuer` of the Credential Issuer metadata have to be the
+Set the Entity Identifier and the `credential_issuer` of the Credential Issuer metadata to the
same value. The template publishes `$baseUrl` as `credential_issuer`, so in
*conf/oidfed/oidfed.properties*:
@@ -2510,9 +2509,9 @@ Import the federation file into *conf/global.xml*, after the import of Step 2:
<import resource="openid-vci-oidfed.xml"/>
```
-Once the import is there every credential states the Entity Identifier, unless it says
-`"oidfed": false`. We want only the URL-based credential in the federation, so the three others get
-`"oidfed": false`:
+After you add the import, every `dc+sd-jwt` and `vc+sd-jwt` credential includes the Entity Identifier
+unless it has `"oidfed": false`. We want only the URL-based credential to be part of the federation, so set
+`"oidfed": false` for the other three credentials:
<details>
<summary>metadata/verifiable-credentials.json</summary>
@@ -2952,7 +2951,7 @@ metadata file of the federation module instead.
### What we have now
-Steps 1 to 4 make a DIIP setup:
+Steps 1 to 4 make the DIIP setup we tested:
| Credential | Issuer | Key found from | Federation | Wallet |
|---|---|---|---|---|
@@ -2970,30 +2969,29 @@ SD-JWT VC or as ISO mdoc, see
[HAIP 1.0](https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0-final.html#vc-profiles).
This step has one of each.
-It is not implemented on Steps 2 to 4. Start from Step 1 and keep its general parts: the
+This setup does not build on Steps 2–4. Start again from Step 1 and keep the general parts:
installation, [Metadata endpoints](#metadata-endpoints),
[Wallet authentication](#wallet-authentication) with the client `offer-caller`, the attribute
-definition and the [Status List](#status-list). Leave out its credentials, the Paradym client
-and the wallet client of the pre-authorized code flow, `offer-caller/wallet`. That client
-authenticates with `none`, and here the wallets authenticate with a Wallet Attestation. The
-diffs below are against the files of Step 1.
+definition and the [Status List](#status-list). Do not include the credential configurations, the Paradym client, or the pre-authorized code flow
+wallet client, `offer-caller/wallet`. That client authenticates with `none`, whereas the wallets in this
+setup authenticate with a Wallet Attestation. The diffs below show the changes to the Step 1 files.
-In theory one IdP can run the DIIP setup of Steps 1 to 4 and this one at the same time. In
-practice it gets complicated, because the metadata is the same for every client and every
-credential, and wallets act on what is published, not on what is registered for them:
+In theory, one IdP can run both the DIIP setup from Steps 1–4 and this HAIP setup at the same time.
+In practice, this gets complicated because the metadata is shared by all clients and credentials,
+and wallets act on what the issuer publishes, not on what is registered for them:
-- The authorization server metadata lists `attest_jwt_client_auth`. Some wallets switch to it as
- soon as it is listed, also for the DIIP credentials. Such a wallet works only if it is
+- The authorization server metadata lists `attest_jwt_client_auth`. In our tests some wallets switched to
+ it as soon as it appeared in the metadata, even when requesting DIIP credentials. Such a wallet works only if it is
registered with `attest_jwt_client_auth` and the CA of its wallet provider is trusted.
-- The signing key with a certificate goes first in the list. A URL-based credential is then signed with
- it and carries `x5c` instead of `kid`, and its verifiers have to move to the chain.
+- The signing key with a certificate must be first in the list. A URL-based credential is then signed with
+ that key and is verified only by verifiers that trust your CA.
So run one setup or the other.
-The wallet of this step is [HopaeEUDIWallet](https://apps.apple.com/app/id6759562486) on iPhone,
-Hopae's build of the [EUDI Reference Wallet](https://github.com/eu-digital-identity-wallet), source
-in [hopae-eudi-ref-wallet](https://github.com/hopae-official/hopae-eudi-ref-wallet). The wallet
-needs no configuration for your issuer, it takes the Credential Offer QR code as it is.
+The wallet used in this step is [HopaeEUDIWallet](https://apps.apple.com/app/id6759562486) for iPhone.
+It is Hopae’s build of the [EUDI Reference Wallet](https://github.com/eu-digital-identity-wallet); its source
+code is in [hopae-eudi-ref-wallet](https://github.com/hopae-official/hopae-eudi-ref-wallet). In our tests
+the wallet needed no issuer-specific configuration and used the Credential Offer QR code as provided.
What you configure:
@@ -3009,14 +3007,13 @@ What you configure:
You need an EC signing key and its certificate, issued by a CA the verifier trusts, see
[HAIP issuer key resolution](https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0-final.html#issuer-key-resolution).
-For the mdoc the certificates follow the profiles of Annex B of
-[ISO/IEC 18013-5](https://www.iso.org/standard/69084.html): the signing certificate is a Document
-Signer certificate issued directly by the IACA root, and Status List Tokens are signed with a
-certificate of their own.
+For the mdoc the signing certificate is a Document Signer certificate issued directly by the IACA
+root, as in Annex B of [ISO/IEC 18013-5](https://www.iso.org/standard/69084.html). In this setup Status
+List Tokens are signed with a certificate of their own.
-Below the Document Signer is *credentials/vci-signing.key* and *credentials/vci-signing.crt*, the
-Status List signer *credentials/vci-status-list.key* and *credentials/vci-status-list.crt*. The
-IACA root is not configured, the verifier has it as its trust anchor.
+The Document Signer uses *credentials/vci-signing.key* and *credentials/vci-signing.crt*. The
+Status List signer uses *credentials/vci-status-list.key* and *credentials/vci-status-list.crt*. The
+IACA root is not configured here; the verifier uses it as its trust anchor.
Step 1 signs with the key of the OP and has no *conf/openid-vci-credentials.xml*. Here the file
gives the keys: the signing key signs the credentials and the Credential Issuer metadata, the
@@ -3079,8 +3076,8 @@ Import the file into *conf/global.xml*, with the trust anchors of [Attestations]
<import resource="openid-vci-credentials.xml"/>
```
-The chain is emitted whenever the issuer of the credential, Status List Token or metadata is an
-https url, there is no switch for it.
+The certificate chain is included whenever the issuer of the credential, Status List Token, or metadata
+is an HTTPS URL. There is no setting to disable it.
#### The SD-JWT VC credential
@@ -3225,18 +3222,16 @@ Full file: [metadata/verifiable-credentials.json](docs/example/step-5/metadata/v
</details>
-`cryptographic_binding_methods_supported` is `jwk`, so the holder key is in the `jwk` member of
-`cnf`. A credential that also lists `did:jwk` keeps a did:jwk `kid` of the proof as it is.
+Set `cryptographic_binding_methods_supported` to `jwk` to bind credentials to a JWK. With
+`did:jwk` also listed, a wallet that sends its key as a `did:jwk` gets a credential bound to the `did:jwk`.
For the values of `key_attestations_required`, see
[attack potential resistance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-attack-potential-resistance).
A wallet sends the key attestation in one of two ways,
see [HAIP key attestation](https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0-final.html#key-attestation).
-With proof type `jwt` the attestation is in the `key_attestation` header of the proof. With proof
-type `attestation` the attestation is the proof, and each of its `attested_keys` gets a credential.
-Advertise both. The OpenID conformance suite uses `jwt`, the Hopae EUDI Wallet only
-`attestation`.
+Advertise both proof types, `jwt` and `attestation`. In our tests the OpenID conformance suite used
+`jwt` and the Hopae EUDI Wallet only `attestation`.
The attribute definition of Step 1 gives the content. Replace its encoders with
`<AttributeEncoder xsi:type="oidc:OIDCString" name="GeantIncubatorDiploma_HAIP" />`. The attribute
@@ -3246,8 +3241,8 @@ is released to the HAIP wallet in *conf/attribute-filter.xml*, see the mdoc cred
`GeantIncubatorDiploma_MDOC` is a diploma in
[ISO mdoc](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#mdocs) format.
-An mdoc carries the certificate chain of its signing key in the `x5chain` header, the Credential
-Issuer identifier is not used.
+An mdoc is always verified by its certificate chain; the Credential Issuer identifier does not
+change that.
<details>
<summary>metadata/verifiable-credentials.json</summary>
@@ -3353,12 +3348,12 @@ What is different from an SD-JWT VC configuration:
A shorter path is refused when the file is read.
- `credential_signing_alg_values_supported` has COSE algorithm identifiers, `-7` is ES256.
- `cryptographic_binding_methods_supported` is `cose_key`.
-- `issuer` decides only the url of the Status List. With `url` the Status List signer of
+- `issuer` determines only the URL of the Status List. With `url`, the Status List signer from
[Certificate chain](#certificate-chain) signs it.
- `lifetime` is shorter than what is left of the Document Signer certificate.
- `claim_types` gives a claim a type JSON does not have. It is not published.
-A claim value goes in as its JSON type, decimal numbers are refused. A date or a byte string
+A claim value is encoded using its JSON type; decimal numbers are not accepted. A date or a byte string
needs a `claim_types` entry. The definition of the document type tells which element is which,
for an mDL [ISO/IEC 18013-5](https://www.iso.org/standard/69084.html):
@@ -3370,7 +3365,7 @@ for an mDL [ISO/IEC 18013-5](https://www.iso.org/standard/69084.html):
| `int` | a number or a string of digits | integer |
| `bool` | `true` or `false`, also as a string | boolean |
-The paths differ from the SD-JWT VC credentials, so the content is an attribute of its own:
+The mdoc paths differ from those of the SD-JWT VC credentials, so this credential uses a separate attribute definition:
<details>
<summary>conf/attribute-resolver.xml</summary>
@@ -3431,20 +3426,20 @@ Full file: [conf/attribute-filter.xml](docs/example/step-5/conf/attribute-filter
</details>
-The Status List needs no further configuration for the mdoc, it is also served as CWT, see
+The Status List needs no further configuration for mdoc credentials. It is also served as a CWT; see
[OAuth.StatusList](#oauth-statuslist).
#### Attestations
-Key attestations and Wallet Attestations are signed by an attester, the wallet provider. You
-trust the certificate of the attester CA of the wallet provider. The Hopae EUDI Wallet gets
-its attestations from the EUDI wallet provider, https://wallet-provider.eudiw.dev. Its CA is
+Key Attestations and Wallet Attestations are signed by an attester (the wallet provider). You
+need to trust the certificate of the wallet provider’s attester CA. In our tests the Hopae EUDI
+Wallet got its attestations from the EUDI wallet provider, https://wallet-provider.eudiw.dev. Its CA is
*PID Issuer CA 02, EUDI Wallet Reference Implementation, EU*, a root, with SHA-256 fingerprint
`3B:0A:22:3E:87:48:F3:C4:22:17:29:8A:D9:05:EC:F1:A1:04:E7:3C:D2:C5:4F:F2:1D:A1:6F:DA:80:70:B2:97`.
Copy [credentials/eudiw-pid-issuer-ca02.crt](docs/example/step-5/credentials/eudiw-pid-issuer-ca02.crt)
-to *credentials/*. The same certificate is in the wallet sources as
+to *credentials/*. The same certificate is available in the wallet source code as
[Wallet/Certificate/pidissuerca02_eu.der](https://github.com/hopae-official/hopae-eudi-ref-wallet/tree/main/Wallet/Certificate)
-and at https://preprod.pki.eudiw.dev/aia/PIDIssuerCA02-EU.cacert.pem. Trust it in
+and at https://preprod.pki.eudiw.dev/aia/PIDIssuerCA02-EU.cacert.pem. Add it as a trust anchor in
*conf/global.xml*:
```xml
@@ -3463,15 +3458,15 @@ and at https://preprod.pki.eudiw.dev/aia/PIDIssuerCA02-EU.cacert.pem. Trust it i
Full file: [conf/global.xml](docs/example/step-5/conf/global.xml)
-Key attestation is checked for every credential that has `key_attestations_required`. Without
-anchors such a request is refused. For the conformance suite add the certificate of its attester CA
-to both lists the same way.
+The plugin checks key attestation for every credential that has `key_attestations_required`. Without
+trust anchors, such a request is rejected. For the conformance suite, add its attester CA certificate
+to both lists in the same way.
-**Optional:** revocation of the attester certificates. Give the CRLs of the CA, and a chain under
-it is checked against them. Each IdP node downloads the CRL itself and reads it again every
-`openidvci.attestation.crlRefreshInterval`, one hour by default. The backing file is used when the
-url cannot be read. The CA of the EUDI wallet provider publishes its CRL for two days at a time,
-in *conf/global.xml*:
+**Optional:** You can also check the revocation status of attester certificates by providing the CA’s
+CRLs. The plugin checks the certificate chain against them. Each IdP node downloads the CRL and
+refreshes it at the interval set by `openidvci.attestation.crlRefreshInterval` (one hour by default).
+The backing file is used if the URL cannot be read. In our tests the CA of the EUDI wallet provider published its CRL for two days at a time.
+In *conf/global.xml*:
```xml
<bean id="openidvci.CRLHttpClient" parent="shibboleth.HttpClientFactory" />
@@ -3491,13 +3486,13 @@ in *conf/global.xml*:
</util:list>
```
-A CRL only applies to the trust anchor that issued it, so the attester CA of the conformance suite
-needs none. If CRLs are configured and none can be read, attestations are refused.
+A CRL applies only to the trust anchor that issued it, so the conformance suite’s attester CA
+does not need one. If CRLs are configured but none can be read, the plugin rejects attestations.
Wallet Attestation is a client authentication method, so it is added to the login flow of
-clients in *conf/authn/oauth2client-authn-config.xml*. It has to be before the validator of
-public clients of [Step 1](#wallet-authentication), otherwise a public client match
-authenticates an attested request without checking the attestation:
+clients in *conf/authn/oauth2client-authn-config.xml*. It must come before the public-client validator from
+[Step 1](#wallet-authentication). Otherwise, an attested request matches the public-client validator
+and is authenticated without the attestation being checked:
```xml
<util:list id="shibboleth.authn.OAuth2Client.Validators">
@@ -3510,9 +3505,9 @@ authenticates an attested request without checking the attestation:
Full file: [conf/authn/oauth2client-authn-config.xml](docs/example/step-5/conf/authn/oauth2client-authn-config.xml)
-The VCI endpoints use login flow `OpenIDVCIClient` of this plugin, which knows the method. The
-PAR endpoint is the OP's, and it refuses a client registered with a method its own login flow
-does not know. Give the OAuth endpoints of the OP the same flow in *conf/oidc.properties*:
+The VCI endpoints use this plugin’s `OpenIDVCIClient` login flow, which supports this method. The
+PAR endpoint belongs to the OP and rejects clients that use an authentication method its login flow
+does not support. Configure the OP’s OAuth endpoints to use the same flow in *conf/oidc.properties*:
```properties
idp.oauth2.authn.flows = OpenIDVCIClient
@@ -3520,8 +3515,12 @@ idp.oauth2.authn.flows = OpenIDVCIClient
#### The HAIP wallet as a client
-The client ID is the `sub` of the Wallet Attestation of the wallet. The EUDI wallet provider gives
-the Hopae EUDI Wallet `eudiw-abca`. In *metadata/oidc-client.json*:
+> **Note**
+> `openidvci.PushedAuthorizationDPoPJktManipulationStrategy` is a workaround for the OP plugin. Once
+> the OP plugin handles pushed requests the same way, the strategy is no longer needed.
+
+The client ID is the `sub` claim in the wallet’s Wallet Attestation. In our tests the EUDI wallet
+provider gave the Hopae EUDI Wallet the client ID `eudiw-abca`. Configure it in *metadata/oidc-client.json*:
<details>
<summary>metadata/oidc-client.json</summary>
@@ -3545,22 +3544,21 @@ the Hopae EUDI Wallet `eudiw-abca`. In *metadata/oidc-client.json*:
- "redirect_uris": ["https://paradym.id/invitation/redirect"],
- "scope": "openid GeantIncubatorDiploma_SDJWT GeantIncubatorDiploma_W3C",
- "audience": [
-- "credentials",
-- "urn:shibboleth.oidc.op.resource:https://issuer.example.org"
+- "https://issuer.example.org"
- ],
- "token_endpoint_auth_method": "none",
+ "client_id": "eudiw-abca",
+ "client_name": "Hopae EUDI Wallet",
+ "redirect_uris": ["eu.europa.ec.euidi://authorization"],
+ "scope": "openid GeantIncubatorDiploma_HAIP GeantIncubatorDiploma_MDOC",
-+ "audience": "credentials",
++ "audience": "https://issuer.example.org",
+ "token_endpoint_auth_method": "attest_jwt_client_auth",
"response_types": ["code"],
- "grant_types": [
- "authorization_code",
- "urn:ietf:params:oauth:grant-type:pre-authorized_code"
- ]
-+ "grant_types": ["authorization_code"]
++ "grant_types": ["authorization_code", "refresh_token"]
}
]
```
@@ -3569,10 +3567,11 @@ Full file: [metadata/oidc-client.json](docs/example/step-5/metadata/oidc-client.
</details>
-In *conf/relying-party.xml* PKCE and PAR are required, `iss` is returned in the authorization
-response, the authorization code lives one minute, see
-[FAPI 2.0 5.3.2.1](https://openid.net/specs/fapi-security-profile-2_0-final.html#section-5.3.2.1),
-and both token and PAR endpoints take the Wallet Attestation:
+In *conf/relying-party.xml*, require PKCE and PAR, return `iss` in the authorization
+response, and set the authorization code lifetime to one minute. See
+[FAPI 2.0 5.3.2.1](https://openid.net/specs/fapi-security-profile-2_0-final.html#section-5.3.2.1) for the
+code lifetime and [FAPI 2.0 5.3.2.2](https://openid.net/specs/fapi-security-profile-2_0-final.html#section-5.3.2.2)
+for PKCE, PAR and `iss`. Both token and PAR endpoints take the Wallet Attestation:
<details>
<summary>conf/relying-party.xml</summary>
@@ -3661,6 +3660,7 @@ and both token and PAR endpoints take the Wallet Attestation:
- p:tokenEndpointAuthMethods="#{{'none'}}"
- p:accessTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy" />
+ p:accessTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
++ p:refreshTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
+ p:tokenEndpointAuthMethods="#{{'attest_jwt_client_auth'}}"
+ p:requireDpopProof="true" />
<bean parent="OpenID.VCI.Credentials" />
@@ -3684,8 +3684,13 @@ Full file: [conf/relying-party.xml](docs/example/step-5/conf/relying-party.xml)
`requireDpopProof` makes DPoP mandatory at the token endpoint, as HAIP requires sender-constrained
access tokens. The other DPoP settings are left to their defaults, so a DPoP nonce is required.
+`refreshTokenClaimsSetManipulationStrategy` and `refresh_token` in the `grant_types` of the client
+give the wallet refresh tokens, bound to the key of its Wallet Attestation, see
+[Refresh tokens](#refresh-tokens).
+
`openidvci.PushedAuthorizationDPoPJktManipulationStrategy` on **OAUTH2.PAR** is recommended with
-DPoP, see [RFC 9449 10.1](https://www.rfc-editor.org/rfc/rfc9449#section-10.1).
+DPoP, see [RFC 9449 10.1](https://www.rfc-editor.org/rfc/rfc9449#section-10.1). It is also needed
+when wallets send `resource` with a pushed request.
`token_endpoint_auth_methods_supported` of *static/oauth-authorization-server.json* lists the
methods your `tokenEndpointAuthMethods` allow on the token and PAR endpoints, here
@@ -3769,15 +3774,15 @@ of the credentials it signed.
| Identifier | What happens when the key changes |
|---|---|
-| `did:jwk` | The identifier is the key, so it changes. New credentials have a new issuer. Old credentials verify on their own, the key is inside, but a verifier that trusts the issuer by its identifier has to be told the new one. The Status List url of an old credential has the thumbprint of the old key, and that list is signed with the old key. If the old key is not in the list anymore, the Status List Token of the old credentials is not served and they cannot be checked. |
+| `did:jwk` | The identifier is the key, so it changes. New credentials have a new issuer. Old credentials can still be verified on their own because they contain the key. However, a verifier that trusts the issuer by its identifier must be told about the new identifier. The Status List URL of an old credential contains the old key’s thumbprint, and the list is signed with that key. If the old key is removed from the list, the Status List Token for the old credentials is no longer served [...]
| `did:web` | The identifier stays. The did:web document lists every key of the list, each by its name, so the new key needs a name of its own. Old credentials have the `kid` of the old key and it stays in the document as long as it is in the list. |
-| url, no chain | The identifier stays. The key is found by `kid` from the keyset of the OP, so the new key goes to **shibboleth.oidc.SigningCredentialsToPublish** before it signs, and the old one stays there until its credentials have expired. |
-| url, with chain | The identifier stays. The credential carries its chain, a verifier trusts the root. A new leaf under the same CA needs nothing published. The Status List of URL-based credentials is not tied to a key, it is signed with the first key of **openidvci.status-list.SigningCredentials**. |
+| URL, no chain | The identifier stays. The key is found by `kid` in the OP’s keyset. Add the new key to **shibboleth.oidc.SigningCredentialsToPublish** before using it for signing, and keep the old key there until its credentials have expired. |
+| URL, with chain | The identifier stays. The credential carries its certificate chain, and the verifier trusts the root. You do not need to publish a new leaf certificate issued by the same CA. The Status List of URL-based credentials is not tied to a key; it is signed with the first key in **openidvci.status-list.SigningCredentials**. |
| OpenID Federation | The Entity Configuration publishes the keys of the list. Same as did:web. |
-| mdoc | Same as url with chain. |
+| mdoc | Same as URL with a certificate chain. |
-So did:jwk is not good for a deployment that rotates keys and revokes credentials. Use it for
-testing and for credentials that live shorter than the key.
+Therefore, `did:jwk` is not a good choice for a deployment that rotates keys and revokes credentials.
+Use it for testing and for credentials that expire before the signing key is rotated.
Status List Tokens may be signed with a key of their own, **openidvci.status-list.SigningCredentials**,
as in [Step 5](#certificate-chain). While any credential is did:jwk, keep the keys that signed
@@ -3788,15 +3793,15 @@ them in that list too.
**File(s):** *metadata/verifiable-credentials.json*, *metadata/oidc-client.json*, *conf/relying-party.xml*
**Format:** JSON, Native Spring
-A wallet tells which credential it wants in the
+A wallet identifies the credential it wants in the
[Credential Request](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request) in one of two ways, see
[Section 3.3.4](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-identifying-credentials-bei) and the [Terminology](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#terminology):
- `credential_configuration_id`, a key of `credential_configurations_supported` in your
[Credential Issuer metadata](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p). It is the kind of
credential.
-- `credential_identifier`, one of the `credential_identifiers` this issuer returned in the
- [token response](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-successful-token-response). It points to one Credential Dataset of a
+- `credential_identifier`, one of the `credential_identifiers` returned by this issuer in the
+ [token response](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-successful-token-response). It identifies one Credential Dataset within a
configuration.
If the token response has `credential_identifiers`, the wallet uses `credential_identifier`.
@@ -3817,12 +3822,12 @@ What you control:
- The user has several credentials of one configuration, see
[Several credentials of one configuration](#several-credentials-of-one-configuration).
- In other cases the token response has no `credential_identifiers`. Most wallets use
- `credential_configuration_id` only, and they work without changes.
+ In other cases the token response has no `credential_identifiers`. In our experience most
+ wallets use `credential_configuration_id` only, and they work without changes.
- **Wallets that ignore the identifiers.** If the token response had identifiers, the credential
endpoint accepts only them. It refuses `credential_configuration_id`, as
[Section 8.2](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request)
- requires. Some wallets ask with `authorization_details` but still send
+ requires. In our tests some wallets asked with `authorization_details` but still sent
`credential_configuration_id`. To accept that, set
`allowCredentialConfigurationIdWithIdentifiers` on the credentials profile of those wallets.
This does not help when the user has several credentials of one configuration. Then the
@@ -4174,17 +4179,17 @@ Foundation conformance suite, the mdoc with the EUDI Reference Wallet and the co
</details>
-The rest of the settings are those of [The mdoc credential](#the-mdoc-credential) in Step 5. What
-is new here:
+The remaining settings are the same as those for [The mdoc credential](#the-mdoc-credential) in Step 5.
+The new points here are:
-- A `claim_types` path is a Claims Path Pointer as the paths of the metadata. It types the value
- it points to, and nothing else of the element. JSON has no date or byte string, a JSON string
- without an entry is a text string in CBOR:
+- A `claim_types` path is a Claims Path Pointer, just like the paths in the metadata. It specifies
+ the type of the value it points to, but does not affect the rest of the element. JSON has no native
+ date or byte-string type; without an entry, a JSON string becomes a text string in CBOR:
- `["org.iso.18013.5.1", "birth_date"]` types the whole element. A string of the form
- `YYYY-MM-DD` is treated as a `full-date`, CBOR tag 1004, the type of a date of birth in an mDL.
+ `YYYY-MM-DD` is treated as a `full-date` (CBOR tag 1004), the type used for a date of birth in an mDL.
- `["org.geant.incubator.complex.1", "medical_certificate", "issue_date"]` types one member of
the map in the element. A string of the form `YYYY-MM-DD` is treated as a `full-date`, so
- that the date is a date in our own name space too. `fit_to_drive`, `vision` and the rest of
+ that the value is treated as a date in our own namespace too. `fit_to_drive`, `vision`, and the rest of
`medical_certificate` keep their JSON types.
- `["org.iso.18013.5.1", "driving_privileges", null, "issue_date"]` types `issue_date` in every
object of the array, in the privileges of both `A` and `B`. A string of the form `YYYY-MM-DD`
@@ -4200,7 +4205,7 @@ is new here:
**Format:** Native Spring, Property files
The attributes are as in [Authorization code flow](#authorization-code-flow) of Step 1. The values
-here are static, the data of the specifications. Your own come from the attributes of the user.
+here are static examples taken from the specifications. Your own values should come from the user’s attributes.
#### SD-JWT VC values
@@ -4261,8 +4266,8 @@ The picture is shortened here.
The type of a value in the credential is the JSON type you give it here, `"70"` is a string and
`70` a number. The script builds the claims as objects and serializes them with `JSON.stringify`,
-instead of joining strings as in Step 1. That keeps `true`, `70` and `1.82` as a boolean and numbers. In a
-joined string you would have to leave the quotes out yourself.
+instead of joining strings as in Step 1. This preserves `true` as a Boolean and `70` and `1.82` as numbers. If you build the value by
+joining strings, you have to omit the quotation marks yourself.
The values of IdP attributes are strings. Convert them before they go to the list:
@@ -4272,10 +4277,9 @@ The values of IdP attributes are strings. Convert them before they go to the lis
{"path": ["verified_claims", "claims", "given_name"], "value": String(givenName.getValues().get(0))}
```
-`ageInYears` and `over18` stand for attributes of your own. `String()` makes a JavaScript string of
-the Java one, so that `JSON.stringify` serializes it as a string.
+`ageInYears` and `over18` stand for attributes of your own. `String()` converts the Java value to a JavaScript string, so `JSON.stringify` serializes it as a string.
-How deep a path goes decides what a wallet can show without the rest:
+The depth of a path determines what a wallet can disclose separately:
| Claim | Given as | A wallet discloses |
|---|---|---|
@@ -4284,14 +4288,14 @@ How deep a path goes decides what a wallet can show without the rest:
| `nationalities`, `evidence` | one path, an array as the value | all elements together, or each element on its own with `sd`, see below |
| `age_equal_or_over` | one path per age | each age on its own |
-[Disclosures for Object Properties](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/) of SD-JWT shows
-how the claims of an object are hidden behind digests in its `_sd`. The plugin does that for every
-object that holds values you give, for `address` in this example. The objects on the path to them,
-`verified_claims` and `claims`, are always visible, only their names show, not their contents.
+[Disclosures for Object Properties](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/) of SD-JWT applies
+to every object that contains values you provide, such as `address` in this example: each of its
+members is disclosed on its own. The objects leading to it, `verified_claims` and `claims`, are
+always disclosed.
[Disclosures for Array Elements](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/) of SD-JWT shows
-`nationalities` with each element disclosed on its own. To issue an array like that, name it in
-`sd` of `_issuance_configuration`:
+`nationalities` with each element disclosed on its own. To issue an array this way, add it to the
+`sd` member of `_issuance_configuration`:
```json
"_issuance_configuration": {
@@ -4441,6 +4445,105 @@ The places to configure, in order:
4. For a document type of a standard, the definitions of that standard. The types, the values and
for an mDL the country of the Document Signer certificate.
+## Refresh tokens, attribute resolution in token endpoint
+
+**File(s):** *conf/relying-party.xml*, *metadata/oidc-client.json*, *conf/oidc.properties*,
+*conf/attribute-filter.xml*
+
+The token endpoint is the token endpoint of the OP plugin with the pre-authorized code grant added.
+Two of its features matter for credentials: refresh tokens, and attribute resolution at the token
+endpoint. Of the steps above only [Step 5](#step-5-a-haip-setup) uses refresh tokens.
+
+### Refresh tokens
+
+A refresh token gives the wallet a new access token, and with it new credentials, without the user
+logging in again. [HAIP](https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0-final.html)
+recommends them.
+
+The token endpoint issues a refresh token with the access token of the authorization code grant. A
+request with `openid` in `scope` gets one only when the scope also has `offline_access`. The
+pre-authorized code grant gets none. `refreshTokensEnabled="false"` on **OpenID.VCI.Token** stops
+refresh tokens altogether.
+
+The wallet redeems a refresh token only if its client has `refresh_token` in `grant_types` in
+*metadata/oidc-client.json*:
+
+```json
+ "grant_types": ["authorization_code", "refresh_token"]
+```
+
+In *conf/relying-party.xml*, set **openidvci.TokenManipulationStrategy** as the
+`refreshTokenClaimsSetManipulationStrategy` of **OpenID.VCI.Token**, next to the access token one:
+
+```xml
+<bean parent="OpenID.VCI.Token"
+ p:accessTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
+ p:refreshTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
+ p:tokenEndpointAuthMethods="#{{'attest_jwt_client_auth'}}" />
+```
+
+The strategy carries the `authorization_details` of the grant into the refresh token, so a
+refreshed access token is limited to the same credentials as the first one. For a client that
+authenticates with a Wallet Attestation the refresh token works only for the same wallet instance, as
+[Attestation-Based Client Authentication 10.3](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-attestation-based-client-auth-12#section-10.3)
+requires. A refresh from another instance, or without an attestation, is refused with
+`invalid_grant`.
+
+The lifetimes and rotation are the OP settings of **OpenID.VCI.Token**:
+
+| Name | Default | Description |
+|---|---|---|
+| `refreshTokenTimeout` | `PT2H`, `idp.oidc.refreshToken.defaultTimeout` | Lifetime of one refresh token. |
+| `refreshTokenChainLifetime` | `PT2H`, `idp.oidc.refreshToken.defaultChainLifetime` | Lifetime of the chain of refresh tokens from one authorization code. |
+| `enforceRefreshTokenRotation` | `false`, `idp.oauth2.enforceRefreshTokenRotation` | Revokes a refresh token once it has been used. |
+
+### Attribute resolution in token endpoint
+
+For the authorization code and refresh token grants the token endpoint resolves the user's
+attributes again and filters them with the wallet as the requester, as the OP token endpoint does.
+A released attribute whose OIDC encoder is named by a credential configuration ID becomes a
+credential of the grant, next to the ones carried in the authorization code or refresh token. The
+scope and `authorization_details` of the grant then limit them as in
+[Authorization code flow](#authorization-code-flow).
+
+An attribute reaches a credential in one of two ways:
+
+| | In `idp.oidc.encodedAttributes` | Resolved at the token endpoint |
+|---|---|---|
+| Resolved | At the authorization endpoint | At both endpoints, used from the token endpoint, again on every refresh |
+| Carried in | The authorization code and the refresh token, encrypted | Neither |
+| Values | As at login | Current |
+| Attributes of the login | Available | Not available |
+
+Either way, the access token carries the credentials of the grant, limited to its scope and
+`authorization_details`. Use one way per attribute.
+
+#### Keeping the authorization code small
+
+The authorization code travels to the wallet in the redirect URL. Every attribute in
+`idp.oidc.encodedAttributes` is encrypted into it. The OP encodes every released attribute in the
+list, whatever scope the wallet asks for. Only the token endpoint limits them to the grant. So a
+credential with many claims, or one with a long list of values, can make the URL too long for
+wallets, browsers or proxies, even when the wallet asks for another credential. Resolving
+the attribute at the token endpoint keeps it out of the code.
+
+For the attribute of [Step 1](#authorization-code-flow), leave it out of *conf/oidc.properties*:
+
+```properties
+# idp.oidc.encodedAttributes = GeantIncubatorDiploma
+```
+
+The policy of *conf/attribute-filter.xml* stays as it is. It releases the attribute to the wallet
+at both endpoints.
+
+The token endpoint has no login. The resolver gets the user's principal name from the grant, but no
+result of an authentication, so an attribute that comes from the login itself, a claim of an
+upstream identity provider for example, cannot be resolved there. Keep such attributes in
+`idp.oidc.encodedAttributes`.
+
+`resolveAttributes="false"` on **OpenID.VCI.Token** turns resolution at the token endpoint off.
+Credentials then come only from `idp.oidc.encodedAttributes`.
+
## Reference
### Profile configuration settings
@@ -4501,10 +4604,16 @@ Response is the Credential Offer as an `openid-credential-offer://` deep link.
Endpoint `/idp/profile/openid/vci/token`, profile ID `http://geant.org/ns/profiles/openid/vci/token`.
+The endpoint is the token endpoint of the OP plugin with the pre-authorized code grant added, and
+has every setting of its **OAUTH2.Token**, refresh tokens among them. The rows below are the ones
+this plugin sets or relies on.
+
| Name | Type | Default | Description |
|---|---|---|---|
-| `grantTypes` | Set<String> | `authorization_code`, `urn:ietf:params:oauth:grant-type:pre-authorized_code` | Grants served. The wallet client has to have the grant in its `grant_types`. |
+| `grantTypes` | Set<String> | `authorization_code`, `refresh_token`, `urn:ietf:params:oauth:grant-type:pre-authorized_code` | Grants served. The wallet client has to have the grant in its `grant_types`. |
| `accessTokenClaimsSetManipulationStrategy` | Function | | Of the OP plugin. Must be **openidvci.TokenManipulationStrategy**. |
+| `refreshTokenClaimsSetManipulationStrategy` | Function | | Of the OP plugin. Must be **openidvci.TokenManipulationStrategy**. Without it a refresh token loses the `authorization_details` of the grant and is not bound to the client instance key of a client attestation. See [Refresh tokens](#refresh-tokens). |
+| `resolveAttributes` | Boolean | `true` | Of the OP plugin. Resolves and filters the user's attributes at the token endpoint and adds them to the credentials of the grant. See [Attribute resolution in token endpoint](#attribute-resolution-in-token-endpoint). |
| `tokenEndpointAuthMethods` | Collection<String> | | Of the OP plugin. `none` for public wallets, `attest_jwt_client_auth` for Wallet Attestation. |
| `dpopProofNonceGenerator` | Function | | Of the OP plugin. `#{null}` for no DPoP nonce. |
| `requireDpopProof` | Boolean | `false` | Of the OP plugin. Refuses a token request without a DPoP proof. |
@@ -4602,7 +4711,7 @@ Request, a `POST` of `application/json`, success is `204 No Content`:
**File(s):** *metadata/verifiable-credentials.json*
**Format:** JSON
-Member `_issuance_configuration` of a credential configuration. It is not published.
+The `_issuance_configuration` member of a credential configuration. It is not published.
| Name | Type | Default | Description |
|---|---|---|---|
@@ -4614,9 +4723,9 @@ Member `_issuance_configuration` of a credential configuration. It is not publis
| `claim_types` | Array | | `{"path": [...], "type": ...}` per claim of an mdoc. `full-date`, `tdate`, `bstr`, `int` or `bool`. The path is a Claims Path Pointer, `null` for every element of an array, see [Complex claims](#complex-claims). |
| `sd` | Array | | `{"path": [..., null], "sd": "always"}` per array of a `dc+sd-jwt` or `vc+sd-jwt` credential that is disclosed as in [Disclosures for Array Elements](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/) of SD-JWT. For `vc+sd-jwt` the path starts with `credentialSubject`. See [Complex claims](#complex-claims). |
-Members of a `vc+sd-jwt` credential, [W3C VCDM 2.0](https://www.w3.org/TR/vc-data-model-2.0/). The ones you set are members of
-`credential_definition`. Their content is purely yours, the plugin does not check it. Only
-`@context` and `type` are published, the rest goes to the credential only.
+The following members of a `vc+sd-jwt` credential are defined by [W3C VCDM 2.0](https://www.w3.org/TR/vc-data-model-2.0/). You set them in
+`credential_definition`. Their contents are entirely up to you; the plugin does not validate them. Only
+`@context` and `type` are published. The other members are included only in the credential.
| Member | Set by | In `credential_definition` | Description |
|---|---|---|---|
@@ -4627,7 +4736,7 @@ Members of a `vc+sd-jwt` credential, [W3C VCDM 2.0](https://www.w3.org/TR/vc-dat
| [`credentialSchema`](https://www.w3.org/TR/vc-data-model-2.0/#data-schemas) | You | Optional | Schema of the credential. |
| [`termsOfUse`](https://www.w3.org/TR/vc-data-model-2.0/#terms-of-use) | You and the plugin | Optional | Terms of use. The plugin adds an `OpenIDFederation` entry when the credential states the Entity Identifier. When both are there, the credential has one array, the plugin's entry first. |
| [`relatedResource`](https://www.w3.org/TR/vc-data-model-2.0/#integrity-of-related-resources) | You | Optional | Integrity information of resources the credential refers to. |
-| [`evidence`](https://www.w3.org/TR/vc-data-model-2.0/#evidence) | You | Optional | Evidence of the claims. The same for every credential of the configuration. |
+| [`evidence`](https://www.w3.org/TR/vc-data-model-2.0/#evidence) | You | Optional | Evidence supporting the claims. It is the same for every credential issued with this configuration. |
| [`id`](https://www.w3.org/TR/vc-data-model-2.0/#identifiers) | The plugin | Not read | `urn:uuid:` and a random UUID, per credential. |
| [`issuer`](https://www.w3.org/TR/vc-data-model-2.0/#issuer) | The plugin | Not read | The issuer ID, see [Credential Issuer identifiers](#credential-issuer-identifiers). |
| [`validFrom`, `validUntil`](https://www.w3.org/TR/vc-data-model-2.0/#validity-period) | The plugin | Not read | The validity window, see `lifetime` and `not_before` above. |
@@ -4635,13 +4744,13 @@ Members of a `vc+sd-jwt` credential, [W3C VCDM 2.0](https://www.w3.org/TR/vc-dat
### Credential Issuer identifiers
-| Value | `iss` of the credential | Key found from | Needs | Status List url ends with |
+| Value | `iss` of the credential | Key found from | Needs | Status List URL ends with |
|---|---|---|---|---|
| empty or `did:jwk`, the default | did:jwk of the signing key | the credential | nothing | `~did-jwk:<key thumbprint>` |
| `did:web` | did:web of this deployment, from the host of `idp.oidc.issuer` | `/.well-known/did.json`, header `kid` `did:web:…#<key name>` | **DID.Document** | `~did-web` |
| `url` or the url itself | `idp.oidc.issuer`, or the url | `x5c` when the key has certificates, otherwise `/.well-known/jwt-vc-issuer` | **OAuth.JWTVCIssuer** and the key in **shibboleth.oidc.SigningCredentialsToPublish**, when no `x5c` | `~url` |
-The Status List Token of a credential has the same issuer as the credential.
+A credential’s Status List Token has the same issuer as the credential.
### Signing keys
@@ -4657,19 +4766,20 @@ The plugin installs this file but does not import it automatically. Import it in
| **openidvci.status-list.SigningCredentials** | Status List Tokens | **openidvci.SigningCredentials**, then the ones of the OP |
| **openidvci.issuerMetadata.SigningCredentials** | Credential Issuer metadata | nothing, published unsigned |
-A list replaces the credentials of the OP, it does not add to them. The first key that fits the
-algorithm signs. The others are published in the did:web document and the Entity Configuration,
-and sign Status List Tokens of did:jwk credentials they signed. EC keys only. A key with
-certificates emits `x5c` when the issuer is an https url. See
+A list replaces the OP’s credentials; it does not add to them. The first key that supports the
+algorithm is used for signing. The other keys are published in the `did:web` document and the Entity Configuration.
+They also sign the Status List Tokens of the `did:jwk` credentials they signed. Only EC keys are supported. A key with
+certificates includes `x5c` when the issuer is an HTTPS URL. See
[Step 6](#step-6-rolling-the-signing-key).
### Beans
-Beans you may define or refer to. Define your own in *conf/global.xml* or in any file imported.
+Beans you can define or reference. Define your own beans in *conf/global.xml* or in any file imported from it.
| Bean ID | Type | Default | Function |
|---|---|---|---|
-| **openidvci.TokenManipulationStrategy** | Function | built in | Carries authorization details and claim values into the authorization code and the access token. Referenced from **OIDC.SSO** and **OpenID.VCI.Token**. |
+| **openidvci.TokenManipulationStrategy** | Function | built in | Carries authorization details and claim values into the authorization code, the access token and the refresh token. For a wallet with a Wallet Attestation it binds the tokens to the wallet instance; a refresh from another instance is refused. Referenced from **OIDC.SSO** and **OpenID.VCI.Token**. |
+| **openidvci.ClientInstanceKeyThumbprintLookup** | Function | built in | Identifies the wallet instance of the Wallet Attestation of the request. |
| **openidvci.CredentialConfigurationsResolver** | CredentialConfigurationsResolver | built in | Reads *metadata/verifiable-credentials.json*. |
| **openidvci.SigningCredentials** | List<Credential> | | Keys signing credentials. |
| **openidvci.status-list.SigningCredentials** | List<Credential> | | Keys signing Status List Tokens. |
@@ -4681,7 +4791,7 @@ Beans you may define or refer to. Define your own in *conf/global.xml* or in any
| **openidvci.DefaultProofBodyClaimsValidator** | ClaimsValidator | built in | Validation of key proof claims, `aud` against the published `credential_issuer`. |
| **openidvci.DefaultOpenIDVCINonceGenerator** | Function | built in | Nonce of key proofs. |
| **openidvci.DefaultAccessTokenClaimsValidator** | ClaimsValidator | built in | Validation of access token claims at the credential endpoint. Chains **openidvci.AccessTokenClaimsValidators**. |
-| **openidvci.AccessTokenClaimsValidators** | List<ClaimsValidator> | built in | `jti`, expiry, not before, `iss`, `client_id`, `aud` containing `credentials`, the claims only this plugin's token endpoint writes, and revocation. A list of your own needs to keep **openidvci.CredentialsAudienceClaimsValidator** and **openidvci.RequiredAccessTokenClaimsValidator**, or access tokens of the OP are accepted. |
+| **openidvci.AccessTokenClaimsValidators** | List<ClaimsValidator> | built in | `jti`, expiry, not before, `iss`, `client_id`, `aud` containing the Credential Issuer Identifier, the claims only this plugin's token endpoint writes, and revocation. A list of your own needs to keep **openidvci.CredentialsAudienceClaimsValidator** and **openidvci.RequiredAccessTokenClaimsValidator**, or access tokens of the OP are accepted. |
| **openidvci.KeyAttestationTrustAnchors** | List<Credential> | | Certificates key attestations are accepted under. Needed for `key_attestations_required`. |
| **openidvci.KeyAttestationCRLs** | List<Resource> | | CRLs of the trust anchors of key attestations, read again every `openidvci.attestation.crlRefreshInterval`. |
| **openidvci.KeyAttestationClaimsValidator** | ClaimsValidator | | Validation of key attestation claims, `nonce` among them. |
@@ -4718,6 +4828,7 @@ The file comes with the plugin and is loaded automatically.
| `openidvci.preAuthorizedCode.defaultLifetime` | Duration | `PT10M` | Lifetime of pre-authorized codes. |
| `openidvci.preAuthorizedCode.defaultLength` | Integer | `0` | `0` self-contained, `10` or more stored. |
| `openidvci.nonce.lifetime` | Duration | `PT5M` | Lifetime of the `c_nonce` of the Nonce endpoint. |
+| `openidvci.requestParser.TokenRequest` | Bean ID | | Custom parser of the Token endpoint request, a `CustomNimbusRequestParser` of the OP plugin returning an `OpenIDVCITokenRequest`. |
#### Credential Configurations
@@ -4813,30 +4924,34 @@ The file comes with the plugin and is loaded automatically.
| `/.well-known/did.json` | `/idp/profile/did/document` | none, formed from the signing keys |
| `/.well-known/openid-federation` | `/idp/profile/oidfed/entity-configuration` | of the federation plugin |
-The templates are Velocity, `$issuer`, `$baseUrl` and `$host` are filled in from
-`idp.oidc.issuer` of *conf/oidc.properties*.
-When `idp.oidc.issuer` has a path, the JWT VC issuer metadata is at `/.well-known/jwt-vc-issuer`
-followed by that path, `https://issuer.example.org/tenant` has it at
-`/.well-known/jwt-vc-issuer/tenant`, and the rewrite pattern is that.
-The authorization server metadata is likewise at `/.well-known/oauth-authorization-server`
-followed by that path, `/.well-known/oauth-authorization-server/tenant`.
-The JWT VC issuer metadata names one issuer. A credential with a url other than
-`idp.oidc.issuer` as its issuer is verified by `x5c`, or needs the `issuer` of the template
-changed to that url.
-`credential_configurations_supported` is added from *metadata/verifiable-credentials.json*. The
-Credential Issuer metadata endpoint answers `500 server_error` when `credential_endpoint` or
-`credential_configurations_supported` is missing. The authorization server metadata endpoint
-answers `500 server_error` when `issuer`, `token_endpoint` or `grant_types_supported` is
-missing, when `authorization_code` is listed without `authorization_endpoint`, when
-`response_types_supported` is missing and a grant type other than the pre-authorized code is
-listed, when `implicit` is listed, or when an endpoint is not an https url.
-`batch_credential_issuance.batch_size` is the most proofs a credential request may carry, it
-overrides `batchSize` of the profile.
+The templates use Velocity. The values of `$issuer`, `$baseUrl`, and `$host` come from
+`idp.oidc.issuer` in *conf/oidc.properties*.
+If `idp.oidc.issuer` contains a path, that path is added to the JWT VC issuer metadata URL. For
+example, if the issuer is `https://issuer.example.org/tenant`, the metadata is available at
+`/.well-known/jwt-vc-issuer/tenant`. The rewrite pattern must include the same path.
+The authorization server metadata URL includes the same path. In this example, it is
+`/.well-known/oauth-authorization-server/tenant`.
+The JWT VC issuer metadata names one issuer. A credential whose issuer is a URL other than `idp.oidc.issuer` must be verified using `x5c`.
+Otherwise, change the `issuer` value in the template to match that URL.
+The plugin adds `credential_configurations_supported` from *metadata/verifiable-credentials.json*.
+The Credential Issuer metadata endpoint returns `500 server_error` if either
+`credential_endpoint` or `credential_configurations_supported` is missing.
+
+The authorization server metadata endpoint returns `500 server_error` if any of the following
+conditions apply:
+
+- `issuer`, `token_endpoint`, or `grant_types_supported` is missing.
+- `authorization_code` is listed, but `authorization_endpoint` is missing.
+- `response_types_supported` is missing and a grant type other than the pre-authorized code is listed.
+- `implicit` is listed.
+- An endpoint is not an HTTPS URL.
+`batch_credential_issuance.batch_size` sets the maximum number of proofs a credential request may
+contain. It overrides the profile’s `batchSize` setting.
`token_endpoint` is the token endpoint of this plugin. The OP's
`/.well-known/openid-configuration` has the OP's own token endpoint.
-`dpop_signing_alg_values_supported` should be in step with `idp.oauth2.dpop.proofAlgorithms` of
-*conf/oidc.properties*. `require_pushed_authorization_requests` and
-`code_challenge_methods_supported` describe the OP, they are set on **OIDC.SSO**.
+`dpop_signing_alg_values_supported` should match `idp.oauth2.dpop.proofAlgorithms` in
+*conf/oidc.properties*. The OP controls `require_pushed_authorization_requests` and
+`code_challenge_methods_supported`; these are set on **OIDC.SSO**.
<details>
<summary>Contents of static/openid-credential-issuer.json, example</summary>
@@ -4934,7 +5049,7 @@ Add the credential configuration IDs to `scopes_supported`. For Wallet Attestati
</details>
-The whole *etc/jetty-rewrite-rules.xml* of the DIIP setup: [etc/jetty-rewrite-rules.xml](docs/example/step-4/etc/jetty-rewrite-rules.xml).
+The complete *etc/jetty-rewrite-rules.xml* file for the DIIP setup: [etc/jetty-rewrite-rules.xml](docs/example/step-4/etc/jetty-rewrite-rules.xml).
The HAIP setup of Step 5 has only the rules of Step 1: [etc/jetty-rewrite-rules.xml](docs/example/step-5/etc/jetty-rewrite-rules.xml).
### Relying party configuration
@@ -4942,6 +5057,6 @@ The HAIP setup of Step 5 has only the rules of Step 1: [etc/jetty-rewrite-rules.
**File(s):** *conf/relying-party.xml*
**Format:** Native Spring
-The file of the DIIP setup of [First installation, by example](#first-installation-by-example):
-[conf/relying-party.xml](docs/example/step-4/conf/relying-party.xml), and of the HAIP setup:
-[conf/relying-party.xml](docs/example/step-5/conf/relying-party.xml).
+The *conf/relying-party.xml* file for the DIIP setup described in
+[First installation, by example](#first-installation-by-example) is available [here](docs/example/step-4/conf/relying-party.xml).
+The file for the HAIP setup is available [here](docs/example/step-5/conf/relying-party.xml).
diff --git a/docs/example/step-1/metadata/oidc-client.json b/docs/example/step-1/metadata/oidc-client.json
index a95e4a5..2aba01c 100644
--- a/docs/example/step-1/metadata/oidc-client.json
+++ b/docs/example/step-1/metadata/oidc-client.json
@@ -16,8 +16,7 @@
"redirect_uris": ["https://paradym.id/invitation/redirect"],
"scope": "openid GeantIncubatorDiploma_SDJWT GeantIncubatorDiploma_W3C",
"audience": [
- "credentials",
- "urn:shibboleth.oidc.op.resource:https://issuer.example.org"
+ "https://issuer.example.org"
],
"token_endpoint_auth_method": "none",
"response_types": ["code"],
diff --git a/docs/example/step-2/metadata/oidc-client.json b/docs/example/step-2/metadata/oidc-client.json
index 0d835e3..8a573e2 100644
--- a/docs/example/step-2/metadata/oidc-client.json
+++ b/docs/example/step-2/metadata/oidc-client.json
@@ -16,8 +16,7 @@
"redirect_uris": ["https://paradym.id/invitation/redirect"],
"scope": "openid GeantIncubatorDiploma_SDJWT GeantIncubatorDiploma_W3C GeantIncubatorDiploma_DIDWEB",
"audience": [
- "credentials",
- "urn:shibboleth.oidc.op.resource:https://issuer.example.org"
+ "https://issuer.example.org"
],
"token_endpoint_auth_method": "none",
"response_types": ["code"],
diff --git a/docs/example/step-3/metadata/oidc-client.json b/docs/example/step-3/metadata/oidc-client.json
index bd89fa3..16d3b7a 100644
--- a/docs/example/step-3/metadata/oidc-client.json
+++ b/docs/example/step-3/metadata/oidc-client.json
@@ -16,8 +16,7 @@
"redirect_uris": ["https://paradym.id/invitation/redirect"],
"scope": "openid GeantIncubatorDiploma_SDJWT GeantIncubatorDiploma_W3C GeantIncubatorDiploma_DIDWEB GeantIncubatorDiploma_URL",
"audience": [
- "credentials",
- "urn:shibboleth.oidc.op.resource:https://issuer.example.org"
+ "https://issuer.example.org"
],
"token_endpoint_auth_method": "none",
"response_types": ["code"],
@@ -32,8 +31,7 @@
"redirect_uris": ["walletapp-goid-openid4vci-callback://authorization"],
"scope": "openid GeantIncubatorDiploma_URL",
"audience": [
- "credentials",
- "urn:shibboleth.oidc.op.resource:https://issuer.example.org"
+ "https://issuer.example.org"
],
"token_endpoint_auth_method": "none",
"response_types": ["code"],
diff --git a/docs/example/step-4/metadata/oidc-client.json b/docs/example/step-4/metadata/oidc-client.json
index bd89fa3..16d3b7a 100644
--- a/docs/example/step-4/metadata/oidc-client.json
+++ b/docs/example/step-4/metadata/oidc-client.json
@@ -16,8 +16,7 @@
"redirect_uris": ["https://paradym.id/invitation/redirect"],
"scope": "openid GeantIncubatorDiploma_SDJWT GeantIncubatorDiploma_W3C GeantIncubatorDiploma_DIDWEB GeantIncubatorDiploma_URL",
"audience": [
- "credentials",
- "urn:shibboleth.oidc.op.resource:https://issuer.example.org"
+ "https://issuer.example.org"
],
"token_endpoint_auth_method": "none",
"response_types": ["code"],
@@ -32,8 +31,7 @@
"redirect_uris": ["walletapp-goid-openid4vci-callback://authorization"],
"scope": "openid GeantIncubatorDiploma_URL",
"audience": [
- "credentials",
- "urn:shibboleth.oidc.op.resource:https://issuer.example.org"
+ "https://issuer.example.org"
],
"token_endpoint_auth_method": "none",
"response_types": ["code"],
diff --git a/docs/example/step-5/conf/relying-party.xml b/docs/example/step-5/conf/relying-party.xml
index a2af800..d53c55d 100644
--- a/docs/example/step-5/conf/relying-party.xml
+++ b/docs/example/step-5/conf/relying-party.xml
@@ -65,6 +65,7 @@
p:includeIssuerInResponse="true" p:authorizeCodeLifetime="PT1M" />
<bean parent="OpenID.VCI.Token"
p:accessTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
+ p:refreshTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
p:tokenEndpointAuthMethods="#{{'attest_jwt_client_auth'}}"
p:requireDpopProof="true" />
<bean parent="OpenID.VCI.Credentials" />
diff --git a/docs/example/step-5/metadata/oidc-client.json b/docs/example/step-5/metadata/oidc-client.json
index 878fc67..3ab3641 100644
--- a/docs/example/step-5/metadata/oidc-client.json
+++ b/docs/example/step-5/metadata/oidc-client.json
@@ -9,9 +9,9 @@
"client_name": "Hopae EUDI Wallet",
"redirect_uris": ["eu.europa.ec.euidi://authorization"],
"scope": "openid GeantIncubatorDiploma_HAIP GeantIncubatorDiploma_MDOC",
- "audience": "credentials",
+ "audience": "https://issuer.example.org",
"token_endpoint_auth_method": "attest_jwt_client_auth",
"response_types": ["code"],
- "grant_types": ["authorization_code"]
+ "grant_types": ["authorization_code", "refresh_token"]
}
]
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list