Keycloak Issuer Integration
This guide explains how to use Keycloak as the identity provider for credential issuance, allowing authenticated Keycloak users to receive digital credentials in their SIROS ID wallet. After reading this guide, you will understand how to:
- Configure Keycloak as the authentication backend for credential issuance
- Map Keycloak user attributes to credential claims
- Set up OIDC or SAML integration
- Trigger credential issuance from Keycloak workflows
Overview
When using Keycloak as the identity provider for credential issuance, users authenticate through Keycloak first, then receive credentials based on their verified identity attributes. This is ideal for organizations that already manage user identities in Keycloak.
This guide works with both the SIROS ID hosted issuer and self-hosted deployments. The examples use issuer.example.org as a placeholder—replace with your actual issuer URL. See Issuer Deployment Options for more information.
If using the SIROS-hosted issuer service, your URL will follow the pattern:
https://<tenant>.issuer.id.siros.org
For example: https://main.demo.issuer.id.siros.org
Prerequisites
- Keycloak 22+ installed and running
- Admin access to your Keycloak realm
- A SIROS ID issuer (hosted or self-hosted)
- Users with attributes you want to include in credentials
Step 1: Register the Issuer as a Keycloak Client
Create an OIDC client in Keycloak for the SIROS ID issuer.
- Open Keycloak Admin Console
- Select your realm
- Navigate to Clients → Create client
Client Configuration
| Setting | Value | Description |
|---|---|---|
| Client type | OpenID Connect | Protocol type |
| Client ID | siros-issuer | Identifier for the issuer |
| Name | SIROS ID Credential Issuer | Display name |
| Always display in UI | OFF | Hide from account console |
Capability Config
| Setting | Value |
|---|---|
| Client authentication | ON |
| Authorization | OFF |
| Authentication flow | ✅ Standard flow |
Access Settings
Configure the redirect URLs for your issuer:
| Setting | Value |
|---|---|
| Root URL | https://issuer.example.org |
| Valid redirect URIs | https://issuer.example.org/oidcrp/callback |
| Valid post logout redirect URIs | https://issuer.example.org |
| Web origins | https://issuer.example.org |
For self-hosted issuers, replace with your issuer URL.
Credentials
After creating the client, go to the Credentials tab and copy the Client secret.
Step 2: Configure User Attributes
Ensure Keycloak users have the attributes needed for credential claims.
Built-in Attributes
These attributes are available by default:
| Keycloak Attribute | OIDC Claim | Description |
|---|---|---|
username | preferred_username | User's username |
email | email | Email address |
firstName | given_name | First name |
lastName | family_name | Last name |
Custom User Attributes
For credential-specific claims (birthdate, nationality, etc.):
- Navigate to Users → Select a user → Attributes
- Add custom attributes:
| Key | Example Value |
|---|---|
birthdate | 1990-01-15 |
nationality | SE |
personal_id | 199001150123 |
Bulk Import Attributes
For existing users, use the Admin REST API:
curl -X PUT "https://keycloak.example.com/admin/realms/myrealm/users/{user-id}" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"attributes": {
"birthdate": ["1990-01-15"],
"nationality": ["SE"],
"personal_id": ["199001150123"]
}
}'
Step 3: Configure Client Scopes
Create scopes to expose user attributes as OIDC claims.
Create a Credential Scope
- Navigate to Client scopes → Create client scope
- Configure:
| Setting | Value |
|---|---|
| Name | credential-claims |
| Type | Default |
| Display on consent screen | ON |
| Consent screen text | Share identity attributes for credential issuance |
Add Attribute Mappers
In the new scope, go to Mappers → Configure a new mapper → User Attribute
Birthdate Mapper
| Setting | Value |
|---|---|
| Name | birthdate |
| User Attribute | birthdate |
| Token Claim Name | birthdate |
| Claim JSON Type | String |
| Add to ID token | ON |
| Add to access token | OFF |
| Add to userinfo | ON |
Nationality Mapper
| Setting | Value |
|---|---|
| Name | nationality |
| User Attribute | nationality |
| Token Claim Name | nationality |
| Claim JSON Type | String |
| Add to ID token | ON |
| Add to access token | OFF |
| Add to userinfo | ON |
Personal ID Mapper
| Setting | Value |
|---|---|
| Name | personal_id |
| User Attribute | personal_id |
| Token Claim Name | personal_id |
| Claim JSON Type | String |
| Add to ID token | ON |
| Add to access token | OFF |
| Add to userinfo | ON |
Assign Scope to Client
- Go to Clients → siros-issuer → Client scopes
- Click Add client scope
- Select
credential-claims→ Add → Default
Step 4: Configure the SIROS ID Issuer
Configure the issuer to use Keycloak as its authentication backend.
For Hosted Issuer
Contact SIROS ID support to configure your Keycloak as an IdP, providing:
- Keycloak realm URL:
https://keycloak.example.com/realms/myrealm - Client ID:
siros-issuer - Client Secret: (from Step 1)
- Required scopes
For Self-Hosted Issuer
Create or update config.yaml:
common:
mongo:
uri: mongodb://mongo:27017
credential_metadata:
pid:
vctm_file_path: "/metadata/vctm_pid.json"
format: "dc+sd-jwt"
apigw:
api_server:
addr: :8080
public_url: "https://issuer.example.com"
auth_providers:
# Keycloak as the OIDC Provider
oidc:
enable: true
issuer_url: "https://keycloak.example.com/realms/myrealm"
redirect_uri: "https://issuer.example.com/oidcrp/callback"
registration:
preconfigured:
enable: true
client_id: "siros-issuer"
client_secret: "keycloak-client-secret"
scopes:
- openid
- profile
- email
- credential-claims
# Normalise Keycloak's claim names where they differ from the VCTM's
attribute_mapping:
birthdate:
claim: "birth_date"
# The OIDC claims ARE the credential data for this scope
data_sources:
assertion:
scopes:
pid:
auth_provider: oidc
PKCE is always used by the OIDC RP; there is no use_pkce switch. APIGW serves
the callback at /oidcrp/callback, so redirect_uri must be that path on your
public_url, and the same value must be registered in Keycloak.
Docker Compose with Keycloak
A working issuer needs the apigw, issuer and registry services plus MongoDB; the OIDC client to Keycloak lives in apigw. Use the Compose file from Deployment unchanged and put the Keycloak client credentials in config.yaml as shown above. The config file is not environment-interpolated, so keep the client secret inline or in the file named by common.secret_file_path.
Step 5: Configure Credential Types
Map Keycloak claims to specific credential types.
Person Identification Data (PID)
common:
credential_metadata:
pid:
vctm_file_path: "/metadata/vctm_pid.json"
format: "dc+sd-jwt"
apigw:
data_sources:
assertion:
scopes:
pid:
auth_provider: oidc
# Value transformations are derivations on the scope
derivations:
- country_alpha2:
input: resident_country
auth_providers:
oidc:
attribute_mapping:
# Required PID claims — only entries that need renaming;
# standard OIDC claim names pass through unchanged.
birthdate:
claim: "birth_date"
# Optional PID claims released by Keycloak under a different name
country:
claim: "resident_country"
# A claim every credential should carry, even if the IdP omits it
issuing_country:
claim: "issuing_country"
default: "SE"
Claims the IdP does not release at all (issuance_date, issuing_authority)
are filled in by the issuer at signing time, not by configuration here.
European Health Insurance Card (EHIC)
EHIC data does not come from the IdP — the health insurance institution is the
authentic source. Declare the type, then bind the scope to the datastore
data source, using OIDC only to identify the person:
common:
credential_metadata:
ehic:
vctm_file_path: "/metadata/vctm_ehic.json"
format: "dc+sd-jwt"
apigw:
data_sources:
datastore:
scopes:
ehic:
auth_provider: oidc
# Claims used to look the person up in the datastore. Names are
# matched verbatim against the stored identity fields.
auth_claims: ["given_name", "family_name", "birth_date"]
auth_providers:
oidc:
attribute_mapping:
# Keycloak releases `birthdate`; the stored field is `birth_date`
birthdate:
claim: "birth_date"
The institution then pushes each person's EHIC document through the Datastore API.
Step 6: Test the Integration
Using the Demo Wallet
- Go to id.siros.org and create a wallet
- Navigate to Add Credential
- Enter your issuer URL or scan the QR code
- Authenticate with your Keycloak credentials
- Approve the credential issuance
- Verify the credential contains correct claims
Direct Testing
The wallet-facing authorization flow is OpenID4VCI (pushed authorization request at /op/par, then /authorize), so it cannot be exercised with a bare browser URL. Create a credential offer from https://issuer.example.org/offers/pid and open it in a wallet.
Verify Claim Mapping
Check that Keycloak claims appear correctly:
This check uses the password grant, which requires Direct access grants to be enabled on the client in Keycloak (turn it on temporarily on a test client; the production client JSON below leaves it off).
# Get a token from Keycloak directly
TOKEN=$(curl -s -X POST \
"https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token" \
-d "client_id=siros-issuer" \
-d "client_secret=${CLIENT_SECRET}" \
-d "grant_type=password" \
-d "username=testuser" \
-d "password=testpassword" \
-d "scope=openid profile credential-claims" | jq -r '.id_token')
# Decode and inspect claims
echo $TOKEN | cut -d'.' -f2 | base64 -d | jq
Advanced Configuration
SAML Integration
If your organization uses SAML instead of OIDC, configure it under
apigw.auth_providers.saml (not issuer.authentication, which no longer
exists). Like the OIDC provider, attribute_mapping is keyed by the SAML
attribute identifier, with the target claim name as the value:
apigw:
auth_providers:
saml:
enable: true
entity_id: "https://issuer.example.com/sp"
acs_endpoint: "https://issuer.example.com/samlsp/acs"
certificate_path: "/pki/sp-cert.pem"
private_key_path: "/pki/sp-key.pem"
attribute_mapping:
"urn:oid:2.5.4.42":
claim: "given_name"
"urn:oid:2.5.4.4":
claim: "family_name"
"urn:oid:0.9.2342.19200300.100.1.3":
claim: "email"
"urn:oid:1.3.6.1.5.5.7.9.1":
claim: "birth_date"
data_sources:
assertion:
scopes:
pid:
auth_provider: saml
Keycloak's SAML descriptor is discovered via mdq_server or a single
static_idp_metadata entry, not metadata_url — see
Using SAML Authentication
for the full SAML provider field reference.
Conditional Credential Issuance
There is no config-level "issue credential X only to role/group Y" gate —
issuer.credential_policies (and required_roles/required_groups) do not
exist in vc. What Keycloak roles and groups control is which OIDC scopes and
attributes end up in the ID token in the first place (via client scope and
mapper assignment, Step 3 above); apigw then only ever sees the claims
Keycloak chose to release. Two ways to get role/group-gated behavior in
practice:
- Give each population its own
credential-claims-style client scope in Keycloak, and map each scope to a differentcommon.credential_metadataentry — a user without the scope simply cannot obtain that credential type, because APIGW never sees the OIDC scope requested. - For issuance triggered from your own backend rather than by the wallet, use the pre-authorized flow below and decide eligibility in your own code before calling the API.
Multi-Realm Support
apigw.auth_providers.oidc configures exactly one OIDC provider — there is
no providers: list, and issuer.authentication does not exist. A
deployment cannot register two Keycloak realms as two separate OIDC
providers in the same apigw instance today.
To serve users from more than one Keycloak realm, use Keycloak's own
Identity Brokering: configure one realm (e.g. myrealm) as the OIDC
provider apigw talks to, and add the other realms to it as brokered identity
providers. Keycloak handles the realm selection and federates the login;
apigw still only ever sees one issuer_url.
Automated Credential Provisioning
Automatically issue credentials when users are created or updated:
// Keycloak Event Listener SPI
public class CredentialProvisioningListener implements EventListenerProvider {
@Override
public void onEvent(AdminEvent event, boolean includeRepresentation) {
if (event.getResourceType() == ResourceType.USER &&
event.getOperationType() == OperationType.CREATE) {
// Call POST /api/v1/datastore/preauth_offer (see
// "Pre-Authorized Code Flow" below) with this user's identity
// and the scope to issue. issuerClient is your own HTTP client,
// not part of vc.
issuerClient.createCredentialOffer(
event.getResourcePath(),
"pid"
);
}
}
}
Pre-Authorized Code Flow
For server-initiated issuance (e.g., batch provisioning), skip OIDC entirely
and create the offer through the datastore API — the scope's datastore
entry must declare auth_provider: preauth (see
API Integration):
curl -X POST "https://issuer.example.com/api/v1/datastore/preauth_offer" \
-H "Authorization: Bearer ${SERVICE_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"authentic_source": "hr.example.org",
"scope": "pid",
"document_id": "keycloak-user-uuid"
}'
The response contains a credential offer that can be sent to users via email
or displayed as a QR code, plus a tx_code when
apigw.auth_providers.preauth.enable_pin is set.
Troubleshooting
Authentication Fails
Error: invalid_client or unauthorized_client
Solutions:
- Verify client ID matches exactly
- Check client secret is correct
- Ensure Client authentication is ON in Keycloak
- Verify redirect URI matches configured values
Claims Missing from Token
Symptoms: Credential issued but missing attributes
Solutions:
- Verify user has the attributes set in Keycloak
- Check mapper configuration in client scope
- Ensure scope is assigned to client as Default
- Verify Add to ID token is ON for each mapper
- Test with userinfo endpoint:
curl -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"https://keycloak.example.com/realms/myrealm/protocol/openid-connect/userinfo"
Credential Has Wrong Values
Symptoms: Credential contains incorrect or default values
Solutions:
- Check the
attribute_mappingentries (a flat claim-name to canonical-name rename; there is no JSONPath) - Verify Keycloak attribute names match mapper configuration
- Enable issuer debug logging to see raw claims
- Test token claims directly from Keycloak
CORS Errors
Error: Browser blocks cross-origin requests
Solutions:
- Add issuer URL to Keycloak client Web origins
- Configure CORS headers on issuer reverse proxy
- Verify both HTTP and HTTPS origins if needed
Configuration Reference
Complete Keycloak Client JSON
Import this client configuration:
{
"clientId": "siros-issuer",
"name": "SIROS ID Credential Issuer",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"https://issuer.example.com/oidcrp/callback"
],
"webOrigins": [
"https://issuer.example.com"
],
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"publicClient": false,
"protocol": "openid-connect",
"defaultClientScopes": [
"openid",
"profile",
"email",
"credential-claims"
],
"optionalClientScopes": []
}
Complete Configuration
A complete config.yaml for the apigw, issuer and registry services with Keycloak as the OIDC provider (as in Deployment):
common:
production: true
mongo:
uri: mongodb://mongo:27017
credential_metadata:
pid:
vctm_file_path: "/metadata/vctm_pid.json"
format: "dc+sd-jwt"
issuer:
issuer_url: "https://issuer.example.org"
api_server:
addr: :8080
grpc_server:
addr: :8090
registry_client:
addr: registry:8090
key_config:
private_key_path: "/pki/signing_ec_private.pem"
chain_path: "/pki/signing_ec_chain.pem"
jwt_attribute:
issuer: "https://issuer.example.org"
verifiable_credential_type: "urn:eudi:pid:1"
apigw:
api_server:
addr: :8080
public_url: "https://issuer.example.org"
key_config:
private_key_path: "/pki/signing_ec_private.pem"
chain_path: "/pki/signing_ec_chain.pem"
issuer_client:
addr: issuer:8090
registry_client:
addr: registry:8090
auth_providers:
oidc:
enable: true
issuer_url: "https://keycloak.example.com/realms/myrealm"
redirect_uri: "https://issuer.example.org/oidcrp/callback"
registration:
preconfigured:
enable: true
client_id: "siros-issuer"
client_secret: "keycloak-client-secret"
scopes:
- openid
- profile
- email
- credential-claims
attribute_mapping:
birthdate:
claim: "birth_date"
data_sources:
assertion:
scopes:
pid:
auth_provider: oidc
delivery:
openid4vci:
token_endpoint: "https://issuer.example.org/token"
clients:
"1003":
type: "public"
redirect_uri: "https://wallet.example.com"
scopes:
- "pid"
credential_offers:
issuer_url: "https://issuer.example.org"
wallets:
my_wallet:
label: "My Wallet"
redirect_uri: "https://wallet.example.com/credential-offer"
trust:
pdp_url: "http://go-trust:6001"
registry:
api_server:
addr: :8080
public_url: "https://registry.example.org"
grpc_server:
addr: :8090
token_status_lists:
key_config:
private_key_path: "/pki/signing_ec_private.pem"
chain_path: "/pki/signing_ec_chain.pem"
Credential validity is taken from the VCTM / issuance logic, not from a
validity_days config key.
Next Steps
- Issuer Configuration – Full issuer documentation
- Trust Services – Configure trust framework integration
- Keycloak Verifier Integration – Use credentials for Keycloak login
- Quick Start Guide – Get started in 15 minutes