## Keycloak Credential Issuance - Implementation Spec
This note is a Keycloak Implementation Spec for the issuance of Verifieable Credentials by the OID4VCI subsystem. It represents the authors common understanding of related specs, looks at Keycloak's current implementation and finally propses a number of action items that follow this discussion.
* [OpenID for Verifiable Credential Issuance 1.0](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html)
* [EBSI - Issue Verifiable Credentials](https://hub.ebsi.eu/conformance/build-solutions/issue-to-holder-functional-flows)
## 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.
<!--
When HackMD -> PlantUML does not render check whether it works in the tutorial https://hackmd.io/c/tutorials/%2Fs%2Ffeatures#PlantUML
Otherwise create an image with https://editor.plantuml.com
-->
```plantuml
@startuml
title OID4VCI Credential Offer Variants
start
:Credential Offer;
if (Pre-Authorized) then (yes)
:Wallet uses pre-auth code;
if (Tx-Code required) then (yes)
:Offer includes tx_code metadata\n(e.g., length, input mode, description);
:Wallet receives Tx-Code out-of-band;
:Wallet gets Access Token\nfrom pre-auth code + Tx-Code;
else (no)
:Wallet gets Access Token\nfrom pre-auth code;
endif
else (no)
:Wallet uses Authorization Flow;
:Wallet gets Access Token\nfrom auth code;
endif
:Access Token;
if (Deferred issuance) then (yes)
:Issuer indicates deferred issuance\n(credential not yet available);
:Wallet calls Credential Endpoint;
:Credential returned when ready;
else (no)
:Wallet calls Credential Endpoint;
:Credential returned immediately;
endif
stop
@enduml
```
### Creating an Offer
:::info
:bulb: 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
```java
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("create-credential-offer")
public Response createCredentialOffer(
@QueryParam("credential_configuration_id") String configId,
@QueryParam("pre_authorized") @DefaultValue("false") boolean preAuthorized,
@QueryParam("tx_code") @DefaultValue("false") boolean withTxCode,
@QueryParam("target_user") String targetUser,
@QueryParam("format") @DefaultValue("uri") String format,
@QueryParam("width") @DefaultValue("200") int width,
@QueryParam("height") @DefaultValue("200") int height
)
```
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.
:::info
:bulb: 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.
:::warning
:construction: 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
:zap: 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.
:::
:::warning
:construction: 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.
:::
:::success
Conclusion & Recommendations
:point_right: [Revisit /credential_offer_uri endpoint](#1-Revisit-credential_offer_uri-endpoint)
:::
### Delivering the Credential Offer
The spec defines a `credential_offer_endpoint` that a Holder Wallet may expose to the Issuer [^credential_offer_endpoint]. 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.
:::warning
:construction: 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.
:::info
:bulb: 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 [^cred_offer_params].
```json
{
"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.
```json
{
"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:
* 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
:zap: 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.
:::
:::success
:zap: 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
:zap: 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.
:::info
:bulb: 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.
:::
:::success
Conclusion & Recommendations
:point_right: [Revisit Pre-Authorized Code handling](#2-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_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.
:::success
Conclusion & Recommendations
:point_right: [Revisit credential_identifier & authorization_details](#3-Revisit-credential_identifier-amp-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.
:::warning
:construction: It is up for debate whether we should require DID verification on register/update
:::
### IDToken Authorization Flow
[EBSI](https://hub.ebsi.eu/conformance/build-solutions/holder-wallet-functional-flows#in-time) requires IDToken Authorization Flow [^ebsi_issuance]
```plantuml
@startuml
title Credential Issuance Flow
participant "Credential Issuer" as CI
participant "Authorisation Server" as AS
participant "Holder Wallet" as HW
== Authorize ==
HW -> AS: Authorisation Request
== Authenticate with ID Token Request ==
AS --> HW: ID Token Request
HW -> AS: ID Token Response
AS --> HW: Authorisation Response
== Token Request ==
HW --> AS: Token Request
AS --> HW: Token Response
== Credential Request ==
HW -> CI: Credential Request
CI -> HW: Credential Response
@enduml
```
Instead of redirecting the Wallet to a login page ...
* The Keycloak sends an IDToken Request as response to the Authorization Request
* The Wallet provides proof of ownership for the DID
* The Keycloak verifies that the IDToken Response was signed correctly
* The Keycloak can now use the DID as the Subject Id in the Credential
* The Keycloak provides and Authorization Code
The provided Code is again equivalent to the Authorization Code above.
## Conclusion & Recommendations
:::warning
:construction: This section will eventually have links that point to issues and follow-up discussions resulting from this document
:::
<!-- Please don't break the anchor links that point to these headers -->
### 1. Revisit /credential_offer_uri endpoint
:::success
Addressed by
:point_right: [[OID4VCI] Revisit and fix /credential_offer_uri endpoint (#45005)](https://github.com/keycloak/keycloak/issues/45005)
:::
### 2. Revisit Pre-Authorized Code handling
:::success
Addressed by
:point_right: [[OID4VCI] Align Pre-Authorized Code with Authorization Code Flow (#45610)](https://github.com/keycloak/keycloak/issues/45610)
:point_right: [[OID4VCI] Add support for`tx_code` in Access Token Request (#45611)](https://github.com/keycloak/keycloak/issues/45611)
:::
### 3. Revisit credential_identifier & authorization_details
:::success
Addressed by
:point_right: [[OID4VCI] Review authorization_details in Authorization Request, Code & Access Token (#45614)](https://github.com/keycloak/keycloak/issues/45614)
:point_right: [[OID4VCI] Review credential_identifier in Credential Offer & Request (#45617)](https://github.com/keycloak/keycloak/issues/45617)
:::
### Related Issues
* [[#45231](https://github.com/keycloak/keycloak/issues/45231)] [OID4VCI] Generate pre-authorized codes using the JWT format
* [[#45026](https://github.com/keycloak/keycloak/issues/45026)] Prototype "required-action" based variant of pre-authorized code flow
* [[#45005](https://github.com/keycloak/keycloak/issues/45005)] [OID4VCI] Revisit and fix /credential_offer_uri endpoint
* [[#44961](https://github.com/keycloak/keycloak/issues/44961)] Authorization_details added to token-response even when should not be
* [[#44834](https://github.com/keycloak/keycloak/issues/44834)] [OID4VCI] Limit the audience of the access token to credential issuance
* [[#44745](https://github.com/keycloak/keycloak/issues/44745)] Impersonation possible in OID4VCI credential creation endpoint
* [[#44657](https://github.com/keycloak/keycloak/issues/44657)] [OID4VCI] Support IDToken authorization handshake for EBSI
[^credential_offer_endpoint]: [Verifiable Credential Issuance 1.0 - Client Metadata](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-client-metadata)
[^cred_offer_params]: [Verifiable Credential Issuance 1.0 - Credential Offer Parameters](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#section-4.1.1)
[^ebsi_issuance]: [EBSI Conformance Guidelines - InTime Issuance](https://hub.ebsi.eu/conformance/build-solutions/issue-to-holder-functional-flows#in-time-issuance)