âť— Important! Before you proceed, please read the EUDI Wallet Reference Implementation project description
- Overview
- Disclaimer
- Use cases supported
- Configuration options
- Other features
- Features not supported
- How to contribute
- License
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:
- The initial development release is an early endeavor reflecting the efforts of a short timeboxed period, and by no means can be considered as the final product.
- The initial development release may be changed substantially over time, might introduce new features but also may change or remove existing ones, potentially breaking compatibility with your existing code.
- The initial development release is limited in functional scope.
- The initial development release may contain errors or design flaws and other problems that could cause system or other failures and data loss.
- The initial development release has reduced security, privacy, availability, and reliability standards relative to future releases. This could make the software slower, less reliable, or more vulnerable to attacks than mature software.
- The initial development release is not yet comprehensively documented.
- Users of the software must perform sufficient engineering and additional testing in order to properly evaluate their application and determine whether any of the open-sourced components is suitable for use in that application.
- We strongly recommend to not put this version of the software into production use.
- Only the latest version of the software will be supported
As a wallet/caller having an out-of-band knowledge of a credential issuer, use the library to initiate issuance:
- Fetch and validate the Credential Issuer Metadata
- Fetch and validate the OAUTH2 or OIDC metadata used by the Credential Issuer
- Ensure that out-of-band knowledge is valid
This is equivalent to resolving a credential offer, having authorization code grant, without issuer state.
- The wallet knows the credential issuer id
- The wallet knows one or more credential configuration ids
- The wallet has prepared an issuance configuration, describing the capabilities & policies of the wallet.
Same as outcome, as if a equivalent offer was resolved.
- Wallet/caller using the library to instantiate an Issuer
- If checks pass, an
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
- to make sure that it is a valid offer,
- comes from an issuer which advertises properly its metadata and that
- this offer is compatible with wallet configuration.
- The wallet has got this URI. Typically, either scanning a QR Code or in response to a custom URI.
- The wallet has prepared an issuance configuration, describing the capabilities & policies of the wallet.
This resolution includes the following
- Check and validate the structure of the URI
- Fetch the actual contents of the offer, in case URI points by reference to the offer
- Fetch and validate the Credential Issuer Metadata
- Fetch and validate the OAUTH2 or OIDC metadata used by the Credential Issuer
- Ensure that credential offer is aligned to both the above metadata & wallet issuance configuration
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.
- The issuer's identifier
- The selected authorization server that will authorize the issuance
- The specific credentials that will be requested
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
- A credential offer has been resolved, and as a result
- An instance of the
Issuer
interface has been instantiated
There are three distinct cases, depending on the content of the credential offer
- Use authorization code flow
- Use pre-authorized code flow
- A credential offer supporting both flows. In this case, the caller/wallet must select which flow to use. This decision is out of the scope of the library, although pre-authorized code flow perhaps is more convenient since it includes fewer steps.
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 ac_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
- The credential offer specifies authorization code flow, or
- Wallet/Caller decides to use this flow
- Wallet/caller using asks the
Issuer
instance to prepare a URL where the mobile device browser needs to be pointed to. Library prepares this URL as follows - Wallet/Caller opens the mobile's browser to the URL calculated in the previous step
- User interacts with the authorization server via mobile device agent, typically providing his authorization
- On success, authorization redirects to a wallet provided
redirect_uri
, providing thecode
and astate
parameters - Using the
Issuer
instance exchange theauthorization code
for anaccess_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 the access_token
by passing authorization_details
also to the token endpoint.
Function authorizeWithAuthorizationCode
supports this via parameter authDetailsOption: 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
- The credential offer specifies pre-authorized code flow or
- Wallet/caller decided to use this flow
- Optionally, wallet has received via another channel a
tx_code
- Wallet has gathered from the user this
tx_code
value, if needed
Steps:
- Using the
Issuer
instance exchange the pre-authorized code & optionally thetx_code
with anaccess_token
- Library will place an adequate request the token endpoint of the credential issuer
- Library will receive token endpoint response and map it to a
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 the access_token
by passing authorization_details
to the token endpoint.
Function authorizeWithPreAuthorizationCode
supports this via parameter authDetailsOption: 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
.
- An instance of the
Issuer
interface has been instantiated - Wallet authorization has been performed and as a result
- An instance of
AuthorizedRequest
is available - Wallet/Caller has decided for which
credential_configuration_id
- found in the offer - the request will be placed for - Wallet/Caller has decided which
credential_identifier
- optional attribute found in theAuthorizedRequest
- the request will be placed for - Wallet/Caller has decided if a subset of the claims will be requested or all.
- Wallet/Caller is ready to provide one or more suitable Proof signers for JWT proofs, if applicable
- Wallet/caller using the library assemble the request providing a
credential_configuration_id
and optionally acredential_identifier
- Wallet/caller using the
Issuer
andAuthorizedRequest
place the request - Library places the appropriate request against the Credential Endpoint of the Credential Issuer
- Library receives the Credential Issuer response and maps it to a
SubmissionOutcome
- Wallet/caller gets back the
SubmissionOutcome
for further processing - Wallet/caller may have to introspect the outcome to assemble a fresh
AuthorizedRequest
carrying possibly a freshc_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 atransaction_id
SubmissionOutcome.Failed
indication that credential issuer rejected the request, including theinvalid_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 function request
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 by SubmissionOutcome
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 error InvalidProof
.
That's typical if credential issuer's token endpoint doesn't provide a c_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
invoking request()
. In case, that this second request fails with invalid_proof
library will report as IrrecoverableInvalidProof
.
- Validate credential and store it. That's out of library scope, or
- Query for credential, or
- Query for credentials at later time, or
- Notify credential issuer
Wallet/caller wants to query credential issuer for credentials, while still holding
an AuthorizedRequest
and an Issuer
instance.
- Wallet/caller has placed a credential request
- Wallet/caller has received a deferred outcome carrying a
transaction_id
- Wallet/caller has an instance of
AuthorizedRequest
- Wallet/caller issuing the
Issuer
instance places the query providingAuthorizedRequest
andtransaction_id
- Library checks if
access_token
inAuthorizedRequest
is expired - If
access_token
is expired it will automatically be refreshed, if credential issuer has given arefresh_token
- Library places the query against the Deferred Endpoint of the credential issuer
- Library gets credential issuer response and maps it into
DeferredCredentialQueryOutcome
- Caller gets back an
AuthorizedRequest
andDeferredCredentialQueryOutcome
The outcome of placing this query is a pair comprised of
AuthorizedRequest
: This represents a possibly updatedAuthorizedRequest
with a refreshedaccess_token
DeferredCredentialQueryOutcome
: This is the response of the deferred endpoint and it could be one ofIssued
: Deferred credentials were issued and optionally anotification_id
IssuancePending
: Deferred credential was not readyErrored
: Credential issuer doesn't recognize thetransaction_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()
}
}
- Validate credential and store it. That's out of library scope, or
- Query for credentials
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
- The lifecycle of
transaction_id
is bound to the expiration of theaccess_token
. - The
access_token
can be refreshed, the library transparently does this, only if credential issuer has provided arefresh_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
- Wallet/caller using the
Issuer
instance obtains aDeferredIssuanceContext
. That's a minimum set of data (configuration options and state) that are needed to query again the credential issuer - Wallet/caller stores the
DeferredIssuanceContext
. How this is done is outside the scope of the library - Wallet/caller loads the
DeferredIssuanceContext
. That's also outside the scope of the library - Wallet/caller queries the credential issuer issuing
DeferredIssuer
- Library performs all steps defined in Query for deferred credentials
- Library returns to the caller the
DeferredIssuanceContxt?
and theDeferredCredentialQueryOutcome
- Depending on the outcome, wallet/caller may choose to store the new
DeferredIssuanceContxt
to query again, later on
The outcome of placing this query is a pair comprised of
DeferredIssuanceContext
: This represents a possibly new state of authorization carrying a refreshedaccess_token
DeferredCredentialQueryOutcome
: This is the response of the deferred endpoint and it could be one ofIssued
: One or more deferred credentials were issuedIssuancePending
: One or more deferred credentials were not readyErrored
: Credential issuer doesn't recognize thetransaction_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()
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:
- Accepted: Credentials were successfully stored in the Wallet
- Deleted: Unsuccessful Credential issuance was caused by a user action.
- Failed: Other unsuccessful cases
- Credential issuer advertises the optional Notification Endpoint
- Wallet/caller has received a
notificationId
(via credential response, deferred response) - Wallet/caller has processed the issued credentials (verification & storage)
- Wallet/caller still has a reference to the
Issuer
andAuthorizedRequest
instances
The use case always succeeds, even in the case of an unexpected error.
- Wallet/caller using the library creates a notification event
- Wallet/caller using the
Issuer
instance places the notification to the credential issuer
val 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: Wallet
client authentication method
in the OAUTH2 sense while interacting with the Credential Issuer.- Either Public Client,
- Attestation-Based Client Authentication
- authFlowRedirectionURI: It is the
redirect_uri
parameter that will be included in a PAR or simple authorization request. - keyGenerationConfig: A way of generating ephemeral keys used for
credential_response_encryption
- credentialResponseEncryptionPolicy: A wallet policy in regard to whether it accepts credentials without
credential_response_encyrption
or not - authorizeIssuanceConfig: Preference on using
scope
orauthorization_details
during authorization code flow - dPoPSigner: An optional way of singing DPoP JWTs. If not provided DPoP is off. If provided, it will be used only if Credential Issuer advertises this feature
- parUsage: An indication to not use PAR endpoint or use it if advertised by the credential issuer
- clock: Wallet/Caller clock.
import 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
- wallet configuration shouldn't exclude its use (explicit configuration option) and
- PAR should be advertised by credential issuer's metadata
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
- Automatically generate the Client Attestation PoP JWT, with every call to the PAR and/or Token endpoint
- Populate the HTTP headers when accessing PAR and/or Token endpoints
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.
- OAUTH2 & OIDC Support: Nimbus OAuth 2.0 SDK with OpenID Connect extensions
- URI parsing: Uri KMP
- Http Client: Ktor
- Json: Kotlinx Serialization
- CBOR: Authlete CBOR
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.