:heavy_exclamation_mark: Important! Before you proceed, please read the EUDI Wallet Reference Implementation project description
This is a Kotlin library, targeting JVM, that supports the OpenId4VCI (draft 14) protocol.
In particular, the library focuses on the wallet's role in and provides the following features:
Feature | Coverage |
---|---|
Wallet-initiated issuance | ✅ |
Resolve a credential offer | ✅ Unsigned metadata ❌ accept-language ❌ signed metadata |
Authorization code flow | ✅ |
Pre-authorized code flow | ✅ |
mso_mdoc format | ✅ |
SD-JWT-VC format | ✅ |
W3C VC DM | VC Signed as a JWT, Not Using JSON-LD |
Place credential request | ✅ Including automatic handling of invalid_proof & multiple proofs |
Query for deferred credentials | ✅ Including automatic refresh of access_token |
Query for deferred credentials at a later time | ✅ Including automatic refresh of access_token |
Notify credential issuer | ✅ |
Proof | ✅ JWT |
Credential response encryption | ✅ |
Pushed authorization requests | ✅ Used by default, if supported by issuer |
Demonstrating Proof of Possession (DPoP) | ✅ |
PKCE | ✅ |
Wallet authentication | ✅ public client, ✅ Attestation-Based Client Authentication |
The released software is an initial development release version:
As a wallet/caller having an out-of-band knowledge of a credential issuer, use the library to initiate issuance:
This is equivalent to resolving a credential offer, having authorization code grant, without issuer state.
Same as outcome, as if a equivalent offer was resolved.
Issuer
will be returned to the caller.import eu.europa.ec.eudi.openid4vci.*
val openId4VCIConfig = ...
val credentialIssuerId: CredentialIssuerId = //known
val credentialConfigurationIds: List<CredentialConfigurationIdentifier> = // known
val issuer =
Issuer.makeWalletInitiated(
config,
credentialIssuerId,
credentialConfigurationIds
).getOrThrow()
As a wallet/caller use the library to process a URI that represents a credential offer
This resolution includes the following
An instance of the Issuer interface (the main entry point to the library) will have been initiated. This instance includes the resolved CredentialOffer and the necessary methods to proceed. The resolved offer contains the mandatory elements required for issuance.
These elements can be used to populate a wallet view that asks user's consensus to proceed with the issuance. This includes the authorization flow to be used (either authorization code flow, or pre-authorized code)
This concern, though, is out of the scope of the library
To resolve a credential offer, wallet/caller must provide configuration options
import eu.europa.ec.eudi.openid4vci.*
val openId4VCIConfig = ...
val credentialOfferUri: String = "..."
val issuer = Issuer.make(openId4VCIConfig, credentialOfferUri).getOrThrow()
As a wallet/caller use the library to obtain an access_token
, to be able to access
the credential issuer's protected endpoints
Issuer
interface has been instantiatedThere are three distinct cases, depending on the content of the credential offer
At the end of the use case, wallet will have an AuthorizedRequest
instance.
Depending on the capabilities of the token endpoint, this AuthorizedRequest
will be either
ProofRequired
: That's the case where Token Endpoint provided a c_nonce
attributeNoProofRequired
: Otherwise.AuthorizedRequest
will contain the access_token
(bearer or DPoP), and, if provided,
the refresh_token
and c_nonce
---
title: Authorization Code Flow state transision diagram
---
stateDiagram-v2
state c_nonce_returned <<choice>>
[*] --> AuthorizationRequestPrepared: prepareAuthorizationRequest
AuthorizationRequestPrepared --> c_nonce_exists: authorizeWithAuthorizationCode
c_nonce_exists --> c_nonce_returned
c_nonce_returned --> AuthorizedRequest.ProofRequired : yes
c_nonce_returned --> AuthorizedRequest.NoProofRequired : no
In addition to the common authorization preconditions
Issuer
instance to prepare a URL where the mobile device browser needs to be pointed to.
Library prepares this URL as follows
redirect_uri
, providing the code
and a state
parametersIssuer
instance exchange the authorization code
for an access_token
.In the scope of the library are steps 1 and 5.
import eu.europa.ec.eudi.openid4vci.*
// Step 1
val preparedAuthorizationRequest =
with(issuer) {
prepareAuthorizationRequest().getOrThrow()
}
// Step 2
// Wallet opens mobile's browser and points it to
// Step 4
// Wallet has extracted from authorization redirect_uri
// the code and state parameters
val (authorizationCode, state) = ... // using url preparedAuthorizationRequest.authorizationCodeURL authenticate via front-channel on authorization server and retrieve authorization code
// Step 5
val authorizedRequest =
with(issuer) {
with(preparedAuthorizationRequest) {
authorizeWithAuthorizationCode(AuthorizationCode(authorizationCode),state).getOrThrow()
}
}
[!TIP] If credential issuer supports
authorization_details
, caller can reduce the scope of theaccess_token
by passingauthorization_details
also to the token endpoint. FunctionauthorizeWithAuthorizationCode
supports this via parameterauthDetailsOption: AccessTokenOption
---
title: PreAuthorization Code Flow state transision diagram
---
stateDiagram-v2
state c_nonce_returned <<choice>>
[*] --> c_nonce_exists: authorizeWithPreAuthorizationCode
c_nonce_exists --> c_nonce_returned
c_nonce_returned --> AuthorizedRequest.ProofRequired : yes
c_nonce_returned --> AuthorizedRequest.NoProofRequired : no
In addition to the common authorization preconditions
tx_code
tx_code
value, if neededSteps:
Issuer
instance exchange the pre-authorized code & optionally the tx_code
with an access_token
AuthorizedRequest
import eu.europa.ec.eudi.openid4vci.*
val txCode : Sting? = ... // Pin retrieved from another channel, if needed
val authorizedRequest =
with(issuer) {
authorizeWithPreAuthorizationCode(txCode).getOrThrow()
}
[!TIP] If credential issuer supports
authorization_details
, caller can reduce the scope of theaccess_token
by passingauthorization_details
to the token endpoint. FunctionauthorizeWithPreAuthorizationCode
supports this via parameterauthDetailsOption: AccessTokenOption
Wallet/caller wants to place a request against the credential issuer, for one of
the credential configurations that were present in the offer, or alternatively for
a specific credential identifier in case token endpoint provided an authorization_details
.
Issuer
interface has been instantiatedAuthorizedRequest
is availablecredential_configuration_id
- found in the offer - the request will be placed forcredential_identifier
- optional attribute found in the AuthorizedRequest
- the request will be placed forcredential_configuration_id
and optionally a credential_identifier
Issuer
and AuthorizedRequest
place the request SubmissionOutcome
SubmissionOutcome
for further processingAuthorizedRequest
carrying possibly a fresh c_nonce
The result of placing a request is represented by a SubmissionOutcome
as follows:
SubmissionOutcome.Sucess
This represents one or more issued credentials, orSubmissionOutcome.Deferred
This indicates a deferred issuance and contains a transaction_id
SubmissionOutcome.Failed
indication that credential issuer rejected the request, including the invalid_proof
case.In case of an unexpected error, a runtime exception will be raised.
import eu.europa.ec.eudi.openid4vci.*
val popSigner: PopSigner? = // optional JWT or CWT signer. Required only if proof are required by issuer
val claimSetToRequest : ClaimSet? = null // null indicates that all claims will be requested
// Step 1
// Assemble the request
val request =
IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, claimSetToRequest)
// Place the request
val (updatedAuthorizedRequest, outcome) =
with(issuer) {
with(authorizedRequest) {
request(request, listOf(popSigner))
}
}
[!TIP] If more than one
popSigner
are passed to functionrequest
multiple instances of the credential will be issued, provided that credential issuer supports batch issuance.[!NOTE]
The ability of the token endpoint to provide a
c_nonce
is an optional feature specified in the OpenId4VCI specification.According to the specification, the wallet must be able to receive a
c_nonce
primarily via the credential issuance response, which is represented bySubmissionOutcome
in the library.For this reason, it is not uncommon that the first request to the credential issuance endpoint will have as an outcome
Failed
with an errorInvalidProof
. That's typical if credential issuer's token endpoint doesn't provide ac_nonce
and proof is required for the requested credential.The library will automatically try to handle the invalid proof response and place a second request which includes proofs. This can be done only if caller has provided a
popSigner
while invokingrequest()
. In case, that this second request fails withinvalid_proof
library will report asIrrecoverableInvalidProof
.
Wallet/caller wants to query credential issuer for credentials, while still holding
an AuthorizedRequest
and an Issuer
instance.
transaction_id
AuthorizedRequest
Issuer
instance places the query providing AuthorizedRequest
and transaction_id
access_token
in AuthorizedRequest
is expiredaccess_token
is expired it will automatically be refreshed, if credential issuer has given a refresh_token
DeferredCredentialQueryOutcome
AuthorizedRequest
and DeferredCredentialQueryOutcome
The outcome of placing this query is a pair comprised of
AuthorizedRequest
: This represents a possibly updated AuthorizedRequest
with a refreshed access_token
DeferredCredentialQueryOutcome
: This is the response of the deferred endpoint and it could be one of
Issued
: Deferred credentials were issued and optionally a notification_id
IssuancePending
: Deferred credential was not readyErrored
: Credential issuer doesn't recognize the transaction_id
val authorizedRequest = // has been retrieved in a previous step
val deferredCredential = // has been retrieved in a previous step. Holds the transaction_id
val (updatedAuthorizedRequest, outcome) =
with(issuer) {
with(authorizedRequest) {
queryForDeferredCredential(deferredCredential).getOrThrow()
}
}
Wallet/caller wants to suspend an issuance process, store its context and query issuer at a later time. There are limitations for this use case
transaction_id
is bound to the expiration of the access_token
.access_token
can be refreshed, the library transparently does this, only if credential issuer has provided a refresh_token
This means that wallet/caller can query for deferred credentials as long as it has a non-expired
access_token
or refresh_token
.
As per query for deferred credentials
Issuer
instance obtains a DeferredIssuanceContext
. That's a minimum set of data (configuration options and state) that are needed to query again the credential issuerDeferredIssuanceContext
. How this is done is outside the scope of the libraryDeferredIssuanceContext
. That's also outside the scope of the libraryDeferredIssuer
DeferredIssuanceContxt?
and the DeferredCredentialQueryOutcome
DeferredIssuanceContxt
to query again, later onThe outcome of placing this query is a pair comprised of
DeferredIssuanceContext
: This represents a possibly new state of authorization carrying a refreshed access_token
DeferredCredentialQueryOutcome
: This is the response of the deferred endpoint and it could be one of
Issued
: One or more deferred credentials were issuedIssuancePending
: One or more deferred credentials were not readyErrored
: Credential issuer doesn't recognize the transaction_id
val authorizedRequest = // has been retrieved in a previous step
val deferredCredential = // has been retrieved in a previous step. Holds the transaction_id
// Step 1
val deferredCtx =
with(issuer) {
with(authorizedRequest) {
deferredContext(deferredCredential).getOrThrough()
}
}
// Store context
// Load context
// Step 4
val (updatedDeferredCtx, outcome) =
DeferredIssuer.queryForDeferredCredential(deferredCtx).getOrThrough()
DeferredIssuanceContext
How waller/caller stores and loads the DeferredIssuanceContext
is out of scope
of the library.
There is though an indicative implementation that serializes the context as a JSON object.
Wallet/caller wants to notify the credential issuer about the overall outcome of the issuance, using one of the defined notifications:
notificationId
Issuer
and AuthorizedRequest
instancesThe use case always succeeds, even in the case of an unexpected error.
Issuer
instance places the notification to the credential issuerval authorizedRequest = // has been retrieved in a previous step
val notificationId = // has been provided by the issuer
// Step 1
// Other events are Deleted and Failed
val event =
CredentialIssuanceEvent.Accepted(notificationId, "Got it!")
// Step 2
with(issuer){
with(authorizedRequest){
notify(event).getOrNull()
}
}
The options available for the Issuer
are represented by OpenId4VCIConfig
data class OpenId4VCIConfig(
val client: Client,
val authFlowRedirectionURI: URI,
val keyGenerationConfig: KeyGenerationConfig,
val credentialResponseEncryptionPolicy: CredentialResponseEncryptionPolicy,
val authorizeIssuanceConfig: AuthorizeIssuanceConfig = AuthorizeIssuanceConfig.FAVOR_SCOPES,
val dPoPSigner: PopSigner.Jwt? = null,
val parUsage: ParUsage = ParUsage.IfSupported,
val clock: Clock = Clock.systemDefaultZone(),
)
Options available:
client authentication method
in the OAUTH2 sense while interacting with the Credential Issuer.
redirect_uri
parameter that will be included in a PAR or simple authorization request.credential_response_encryption
credential_response_encyrption
or notscope
or authorization_details
during authorization code flowimport eu.europa.ec.eudi.openid4vci.*
val openId4VCIConfig = OpenId4VCIConfig(
client = Client.Public("wallet-dev"), // the client id of wallet (acting as an OAUTH2 client)
authFlowRedirectionURI = URI.create("eudi-wallet//auth"), // where the Credential Issuer should redirect after Authorization code flow succeeds
keyGenerationConfig = KeyGenerationConfig.ecOnly(Curve.P_256), // what kind of ephemeral keys could be generated to encrypt credential issuance response
credentialResponseEncryptionPolicy = CredentialResponseEncryptionPolicy.SUPPORTED, // policy concerning the wallet's requirements for encryption of credential responses
)
val credentialOfferUri: String = "..."
val issuer = Issuer.make(openId4VCIConfig, credentialOfferUri).getOrThrow()
Library supports RFC 9126 OAuth 2.0 Pushed Authorization Requests To use the PAR endpoint
Library will automatically use the PAR endpoint during the authorization code flow, otherwise it will fall back to a regular authorization request.
The current version of the library supports JWT proofs
Library supports RFC9449. In addition to bearer authentication scheme, library can be configured to use DPoP authentication provided that the authorization server, that protects the credential issuer, supports this feature as well.
If wallet configuration provides a DPoP Signer and if the credential issuer advertises DPoP with
algorithms supported by wallet's DPoP Signer, then library will transparently request
for a DPoP access_token
instead of the default Bearer token.
Furthermore, all further interactions will use the correct token type (Bearer or DPoP)
Library supports both
Authorization Server-Provided Nonce and
Resource Server-Provided Nonce. It
features automatic recovery/retry support, and is able to recover from use_dpop_nonce
errors given that a new DPoP
Nonce is provided in the DPoP-Nonce
header. Finally, library refreshes DPoP Nonce whenever a new value is provided,
either by the Authorization Server or the Credential Issuer, using the DPoP-Nonce
header, and all subsequent
interactions use the newly provided DPoP Nonce value.
Library supports RFC7636 by default while performing Authorization code flow. This feature cannot be disabled.
Library supports OAUTH2 Attestation-Based Client Authentication - Draft 03
To enable this, caller must have obtained a Client/Wallet Attestation JWT How this is done, it is outside the scope of the library.
Furthermore, caller must provide a specification on how to produce the Client Attestation PoP JWT
val clientAttestationJWT: ClientAttestationJWT("...")
val popJwtSpec: ClientAttestationPoPJWTSpec = ClientAttestationPoPJWTSpec(
signingAlgorithm = JWSAlgorithm.ES256, // Algorithm to sign the PoP JWT
duration = 1.minutes, // Duration of PoP JWT. Used for `exp` claim
typ = null, // Optional, `typ` claim in the JWS header
jwsSigner = signer, // Nimbus signer
)
val wallet = Client.Attested(clientAttestationJWT, popJwtSpec)
val openId4VCIConfig = OpenId4VCIConfig(
client = wallet
authFlowRedirectionURI = URI.create("eudi-wallet//auth"), // where the Credential Issuer should redirect after Authorization code flow succeeds
keyGenerationConfig = KeyGenerationConfig.ecOnly(Curve.P_256), // what kind of ephemeral keys could be generated to encrypt credential issuance response
credentialResponseEncryptionPolicy = CredentialResponseEncryptionPolicy.SUPPORTED, // policy concerning the wallet's requirements for encryption of credential responses
)
With this configuration library is able to
Library will check that the authorization server of the issuer,
includes method attest_jwt_client_auth
in claim token_endpoint_auth_methods_supported
of its metadata.
Specification recommends the use of header Accept-Language
to indicate the language(s) preferred for display.
Current version of the library does not support this.
Specification details the metadata an issuer advertises through its metadata endpoint.
Current version of the library supports all metadata specified there except signed_metadata
attribute.
Specification defines that a credential's issuance
can be requested using authorization_details
or scope
parameter when using authorization code flow. The current version of the library supports usage of both parameters.
Though for authorization_details
we don't support the format
attribute and its specializations per format.
Only credential_configuration_id
attribute is supported.
We welcome contributions to this project. To ensure that the process is smooth for everyone involved, follow the guidelines found in CONTRIBUTING.md.
Copyright (c) 2023 European Commission
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.