You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
With this note we take a look at Keycloak's implementation for Issuance of Verifiable Credentials i.e. the OID4VCI subsystem. It is a working document that together with comments represents the common understanding the authors. We discuss functional areas of interest and (at the end) make recommendations for improvements and link them to tangible action items.
The following specs are related to this discussion
The only required parameter is credential_configuration_id in which case Keycloak will create a non pre-authorized offer that can be redeemed by any authorized user.
Pre-Authorized offers are targeted to a specific target_user. The resulting pre-authorized_code gives access to an authorized UserSession for
that target user with the same client_id that the caller of this endpoint used to create the offer.
Tip
A pre-authorized_code is equivalent to an authorization code resulting from an Authorization Code Flow (i.e. no previous or follow-up login is required from the target user
Parameter tx_code works in connection with pre_authorized=true, in which case Keycloak generates a tx_code, which is then included in the JSON Response.
Note
It is up for debate whether tx_code should realy be returned from a call to this endpoint. Alternatively, Keycloak may offer an alternative way of how to obtain the tx_code associated with a given credential offer state.
The caller can make a choice about the preferred response format
Format
Response
Content
uri
application/json
json with credential_offer_uri
qr
image/png
image with encoded credential_offer_uri
uri+qr
application/json
json with credential_offer_uri and qr_code
In case of a pre-authorized offer with tx_code, the requested response format must be one of the uri variants i.e. the tx_code cannot be embedded in the QR Code.
Warning
Access to this endpoint must be carefully guarded since it effectively allows the caller to grant pre-authorized accesses to any credential for any user associated with the current client.
Currently, we protect access by requiring the credential_offer_create role.
A Holder would normally NOT have the credential_offer_create role.
Note
It is up for debate whether we should allow self-serviced credential offers. In which case, we would allow anyone with an active user session to make a call to this endpoint and leave the target_user empty - no credential_offer_create role would be required.
The spec defines a credential_offer_endpoint that a Holder Wallet may expose to the Issuer 1. Currently we do not expect Keycloak to be aware of this Client Metadata nor use it to push the offer to the Wallet.
Instead we assume that there is a secure communication channel that makes the Credential Offer available to the Wallet. The entity that creates the Credential Offer receives the credential_offer_uri, a QRCode that encodes this credential_offer_uri (optionally) and also the server generated tx_code (optionally). It is the responsibility of that entity to pass on that information securely.
Note
It is up for debate whether and how Keycloak UI can display credential offer state
Credential Offer Livetime
Currently we assume that a Credential Offer is a one-time use object i.e. when the Wallet redeems the Credential associated with a Credential Offer, the offer becomes invalid and can be removed from the system.
Tip
It may take some time (i.e. days) for the Wallet to redeem the Credential Offer. Keycloak should therefore set an expiry date for the offer
Pre-Authorized & Tx Code
The Issuer may create a Credential Offer that is pre-authorized 2.
{
"credential_issuer": "https://credential-issuer.example.com",
"credential_configuration_ids": [
"UniversityDegreeCredential_JWT"
],
"grants": {
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "oaKazRN8I0IbtZ0C7JuMn5",
"tx_code": {
"length": 4,
"input_mode": "numeric",
"description": "Please provide the one-time code that was sent via e-mail"
}
}
}
}
That Authorization Code is equivalent to what the Wallet would get from Authorization Code Flow. When redeemed, the Wallet will get a Token Response that is no different.
Pre-authorized flows are designed for cases where:
The subject cannot authenticate interactively
Issuance is triggered out-of-band
UX must be frictionless (QR, NFC, paper, email)
Examples:
Government mailers
Kiosk scenarios
Batch provisioning
Impersonation/Insider Threat
The spec explicitly provides Tx-Code for this reason.
Warning
If a pre-authorized offer does not include a Tx-Code, then possession of the pre-authorized code is sufficient to get the associated credential
Tip
With a Tx-Code:
The attacker needs two factors
The offer + an out-of-band secret
Without it, impersonation risk is acknowledged.
Mandatory security mitigations
The issuer must bind the code to the wallet/subject key. This prevents post-issuance misuse, but not mis-issuance.
Short TTL
One-time redemption
Invalidation after use
The protocol cannot fully prevent an insider threat - no protocol can.
Mitigations are organizational:
Audit logs for offer creation
Separation of duties
Just-in-time authorization
Human approval or dual control
The spec implicitly assumes:
The issuer is a trusted authority
Offer creation is access-controlled
Pre-auth offers are not used casually
Warning
A pre-authorized credential offer is a bearer artifact.
If issued without Tx-Code, short TTL, subject binding, and auditing, it enables impersonation and insider abuse. These risks are known and must be mitigated by issuer policy and deployment choices
Miss-Issued Credential Offers
In case a credential offer gets miss-issued (malliciously) it still up to the Wallet whether it accepts the offer. A spec conformant wallet is likely to reject a credential offer and the resulting credential when it is issued to a subject that the wallet does not control. For example, the credential is issued to a foreign DID for which the Wallet does not have the associated key material.
Also, a Verifier would not accept a Credential from a Wallet that fails signature verification.
Note
Impersonation is perhaps too stong a term, when it only allows to get a specific credential that will likely turn out to be rejected by or useless for the Wallet.
A non pre-authorized offer is delivered to the target wallet in the same way as a pre-authorized offer. It is useful for wallet bootstraping i.e. when the wallet does not yet know the issuer. In contrary, when the wallet already knows the isser, can establish a login session and is aware how scopes maps to credential configurations, it can request a credential using a scope parameter on the Authorization Request - no credential offer would be needed.
By default, we set the credential_configuration_id equal to the scope.
How to choose the appropriate offer type
Use a non pre-authorized offer when ...
The Wallet does not know the issuer ahead of time
Issuance is triggered out-of-band
You want explicit issuer-driven intent
Multiple credential types or options need to be advertised
Use scoped OAuth without an offer when ...
Wallet and issuer already trust each other
Issuance is app-to-app or tightly integrated
You want a minimal OAuth flow
Credential Identifier
A Credential Identifier identifies a specific dataset that the Issuer can offer in the context of a given credential_configuration_id. It is different from the credential_configuration_id and also different from the id that identifies a specific credential instance.
With current user attribute to credential claim mapping capabilities in Keycloak, it is not obvious how we would derive a credential_identifier that is different from from the credential type (e.g. UniversityDegree)
The concept of "specific dataset" can also be applied to user attribute values at a given moment of time (i.e. when the offer was made). As mentioned above, it may take days before the Wallet eventually redeems the Credential Offer and requests the associated Credential. During that time, attribute values may have changed in such a way that the Issuer would no longer make that offer.
Despite existing deficiencies in attribute mapping capabilities, Keycloak could already make a snapshot of current attribute values and include that snapshot in its CredentialOfferState. This snapshot would be identified by credential_identifier. At the time of Credential Request, the Wallet would receive a Credential as it was intended by Issuer, rather than a Credential with attribute values as they happen to be at request time.
Various authorization flows exist and here we look at the most common ones relevant for credential issuance.
Authorization Code Flow
This is the standard OAuth Code Flow. It assumes that the User/Holder has an account with Keycloak and that the Wallet has UI capabilities such that the User can provide credentials to login page.
When the credential is issued/bound to a Holder provided DID, there is no built-in mechanism that proves possession of that DID.
Neither does it work in headless environments (i.e. machine-to-machine issuance)
Pre-Authorization Code
Commonly used with Credentials Offers and described above. The Pre-Authorization Code is equivalent to the Authorization Code above.
With this flow, there is also no built-in mechanism that proves possession of a given DID. Hence the Issuer, must verify proof of possession when the DID is registered as user attribute.
Note
It is up for debate whether we should require DID verification on register/update
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Credential Issuance in Keycloak
With this note we take a look at Keycloak's implementation for Issuance of Verifiable Credentials i.e. the OID4VCI subsystem. It is a working document that together with comments represents the common understanding the authors. We discuss functional areas of interest and (at the end) make recommendations for improvements and link them to tangible action items.
The following specs are related to this discussion
Other related material
Credential Offer
A Credential Offer can be Pre-Authorized or not, when Pre-Authorized, require a Tx-Code or not and may be issued in-time or deferred.
Creating an Offer
Tip
The following describes Credential Offer creation as it should be (and not how it is currently implemented)
Keycloak has this endpoint which can be used to create Credential Offers
The only required parameter is
credential_configuration_idin which case Keycloak will create a non pre-authorized offer that can be redeemed by any authorized user.Pre-Authorized offers are targeted to a specific
target_user. The resultingpre-authorized_codegives access to an authorized UserSession forthat target user with the same
client_idthat the caller of this endpoint used to create the offer.Tip
A
pre-authorized_codeis equivalent to an authorizationcoderesulting from an Authorization Code Flow (i.e. no previous or follow-up login is required from the target userParameter
tx_codeworks in connection withpre_authorized=true, in which case Keycloak generates atx_code, which is then included in the JSON Response.Note
It is up for debate whether
tx_codeshould realy be returned from a call to this endpoint. Alternatively, Keycloak may offer an alternative way of how to obtain thetx_codeassociated with a given credential offer state.The caller can make a choice about the preferred response format
uricredential_offer_uriqrcredential_offer_uriuri+qrcredential_offer_uriandqr_codeIn case of a pre-authorized offer with
tx_code, the requested response format must be one of theurivariants i.e. thetx_codecannot be embedded in the QR Code.Warning
Access to this endpoint must be carefully guarded since it effectively allows the caller to grant pre-authorized accesses to any credential for any user associated with the current client.
Currently, we protect access by requiring the
credential_offer_createrole.A Holder would normally NOT have the
credential_offer_createrole.Note
It is up for debate whether we should allow self-serviced credential offers. In which case, we would allow anyone with an active user session to make a call to this endpoint and leave the
target_userempty - nocredential_offer_createrole would be required.Tip
👉 Revisit /credential_offer_uri endpoint
Delivering the Credential Offer
The spec defines a
credential_offer_endpointthat a Holder Wallet may expose to the Issuer 1. Currently we do not expect Keycloak to be aware of this Client Metadata nor use it to push the offer to the Wallet.Instead we assume that there is a secure communication channel that makes the Credential Offer available to the Wallet. The entity that creates the Credential Offer receives the
credential_offer_uri, a QRCode that encodes thiscredential_offer_uri(optionally) and also the server generatedtx_code(optionally). It is the responsibility of that entity to pass on that information securely.Note
It is up for debate whether and how Keycloak UI can display credential offer state
Credential Offer Livetime
Currently we assume that a Credential Offer is a one-time use object i.e. when the Wallet redeems the Credential associated with a Credential Offer, the offer becomes invalid and can be removed from the system.
Tip
It may take some time (i.e. days) for the Wallet to redeem the Credential Offer. Keycloak should therefore set an expiry date for the offer
Pre-Authorized & Tx Code
The Issuer may create a Credential Offer that is pre-authorized 2.
{ "credential_issuer": "https://credential-issuer.example.com", "credential_configuration_ids": [ "UniversityDegreeCredential_JWT" ], "grants": { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": "oaKazRN8I0IbtZ0C7JuMn5", "tx_code": { "length": 4, "input_mode": "numeric", "description": "Please provide the one-time code that was sent via e-mail" } } } }That Authorization Code is equivalent to what the Wallet would get from Authorization Code Flow. When redeemed, the Wallet will get a Token Response that is no different.
{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..sHQ", "token_type": "Bearer", "expires_in": 86400, "authorization_details": [ { "type": "openid_credential", "credential_configuration_id": "UniversityDegreeCredential_JWT", "credential_identifiers": [ "CivilEngineeringDegree-2023", "ElectricalEngineeringDegree-2023" ] } ] }Why pre-authorized codes exist
Pre-authorized flows are designed for cases where:
Examples:
Impersonation/Insider Threat
The spec explicitly provides Tx-Code for this reason.
Warning
If a pre-authorized offer does not include a Tx-Code, then possession of the pre-authorized code is sufficient to get the associated credential
Tip
With a Tx-Code:
Without it, impersonation risk is acknowledged.
Mandatory security mitigations
The issuer must bind the code to the wallet/subject key. This prevents post-issuance misuse, but not mis-issuance.
The protocol cannot fully prevent an insider threat - no protocol can.
Mitigations are organizational:
The spec implicitly assumes:
Warning
A pre-authorized credential offer is a bearer artifact.
If issued without Tx-Code, short TTL, subject binding, and auditing, it enables impersonation and insider abuse. These risks are known and must be mitigated by issuer policy and deployment choices
Miss-Issued Credential Offers
In case a credential offer gets miss-issued (malliciously) it still up to the Wallet whether it accepts the offer. A spec conformant wallet is likely to reject a credential offer and the resulting credential when it is issued to a subject that the wallet does not control. For example, the credential is issued to a foreign DID for which the Wallet does not have the associated key material.
Also, a Verifier would not accept a Credential from a Wallet that fails signature verification.
Note
Impersonation is perhaps too stong a term, when it only allows to get a specific credential that will likely turn out to be rejected by or useless for the Wallet.
Tip
👉 Revisit Pre-Authorized Code handling
Non Pre-Authorized Offer
A non pre-authorized offer is delivered to the target wallet in the same way as a pre-authorized offer. It is useful for wallet bootstraping i.e. when the wallet does not yet know the issuer. In contrary, when the wallet already knows the isser, can establish a login session and is aware how scopes maps to credential configurations, it can request a credential using a scope parameter on the Authorization Request - no credential offer would be needed.
By default, we set the
credential_configuration_idequal to thescope.How to choose the appropriate offer type
Use a non pre-authorized offer when ...
Use scoped OAuth without an offer when ...
Credential Identifier
A Credential Identifier identifies a specific dataset that the Issuer can offer in the context of a given
credential_configuration_id. It is different from thecredential_configuration_idand also different from theidthat identifies a specific credential instance.With current user attribute to credential claim mapping capabilities in Keycloak, it is not obvious how we would derive a
credential_identifierthat is different from from the credential type (e.g. UniversityDegree)The concept of "specific dataset" can also be applied to user attribute values at a given moment of time (i.e. when the offer was made). As mentioned above, it may take days before the Wallet eventually redeems the Credential Offer and requests the associated Credential. During that time, attribute values may have changed in such a way that the Issuer would no longer make that offer.
Despite existing deficiencies in attribute mapping capabilities, Keycloak could already make a snapshot of current attribute values and include that snapshot in its CredentialOfferState. This snapshot would be identified by
credential_identifier. At the time of Credential Request, the Wallet would receive a Credential as it was intended by Issuer, rather than a Credential with attribute values as they happen to be at request time.Tip
👉 Revisit credential_identifier & authorization_details
Authorization Flows
Various authorization flows exist and here we look at the most common ones relevant for credential issuance.
Authorization Code Flow
This is the standard OAuth Code Flow. It assumes that the User/Holder has an account with Keycloak and that the Wallet has UI capabilities such that the User can provide credentials to login page.
When the credential is issued/bound to a Holder provided DID, there is no built-in mechanism that proves possession of that DID.
Neither does it work in headless environments (i.e. machine-to-machine issuance)
Pre-Authorization Code
Commonly used with Credentials Offers and described above. The Pre-Authorization Code is equivalent to the Authorization Code above.
With this flow, there is also no built-in mechanism that proves possession of a given DID. Hence the Issuer, must verify proof of possession when the DID is registered as user attribute.
Note
It is up for debate whether we should require DID verification on register/update
IDToken Authorization Flow
EBSI requires IDToken Authorization Flow 3
Instead of redirecting the Wallet to a login page ...
The provided Code is again equivalent to the Authorization Code above.
Conclusion & Recommendations
1. Revisit /credential_offer_uri endpoint
Tip
👉 [OID4VCI] Revisit and fix /credential_offer_uri endpoint (#45005)
2. Revisit Pre-Authorized Code handling
Tip
👉 [OID4VCI] Align Pre-Authorized Code with Authorization Code Flow (#45610)
👉 [OID4VCI] Add support for
tx_codein Access Token Request (#45611)3. Revisit credential_identifier & authorization_details
Tip
👉 [OID4VCI] Review authorization_details in Authorization Request, Code & Access Token (#45614)
👉 [OID4VCI] Review credential_identifier in Credential Offer & Request (#45617)
Related Issues
Footnotes
Verifiable Credential Issuance 1.0 - Client Metadata ↩
Verifiable Credential Issuance 1.0 - Credential Offer Parameters ↩
EBSI Conformance Guidelines - InTime Issuance ↩
All reactions