OpenID for Verifiable Credential Issuance (OpenID4VCI) backend for issuing credentials to EUDI Wallets and other compatible wallet implementations.
This service uses VC-K as the credential, OpenID, and status-list toolkit. It wires VC-K's issuer agent, OpenID4VCI issuer, OAuth2 authorization service, OpenID4VP verifier, credential scheme mapper, and token status list support into a Spring Boot application that can authenticate users, derive credential claims, and deliver wallet-ready credentials.
| This service is intended as a Technology Demonstrator! |
- Issues credentials over OpenID4VCI with authorization code, pre-authorized code, pushed authorization requests, token exchange, nonce, and credential endpoints.
- Supports the Digital Credentials API for browser-based issuance without a redirect.
- Uses VC-K's issuer APIs to sign and deliver credentials as JWT VC, SD-JWT VC, and ISO mdoc where supported by the configured credential scheme.
- Publishes issuer, OAuth2/OIDC, and JWT VC metadata for wallet discovery.
- Authenticates users through Spring Security, external OIDC providers such as ID Austria, or EU PID presentation via OpenID4VP.
- Converts authenticated identity data into EUDI Wallet credential payloads.
- Tracks issued credentials and writes token status lists for revocation/status checks.
- Runs as a standard Spring Boot service with JPA persistence, H2 for local development, PostgreSQL for deployments, actuator endpoints, and optional Spring Boot Admin integration.
VC-K is the core interoperability layer in this repository:
| Capability | How this backend uses VC-K |
|---|---|
| Credential issuance | IssuerAgent signs credentials and binds them to the holder key supplied by the wallet. |
| OpenID4VCI | CredentialIssuer implements the issuer metadata, nonce, and credential response logic. |
| OAuth2 for issuance | SimpleAuthorizationService handles PAR, authorization, token issuance, DPoP nonce handling, and credential authorization. |
| Credential formats | The configured VC-K credential schemes expose JWT VC, SD-JWT VC, and ISO mdoc representations where available. |
| OpenID4VP login | OpenId4VpVerifier verifies EU PID presentations used as an identity source for issuance. |
| Status lists | StatusListAgent creates token status lists and status-list aggregation metadata for issued credentials. |
| Metadata mapping | DefaultCredentialSchemeMapper maps VC-K credential schemes to OpenID4VCI supported credential formats. |
The backend registers credential schemes from the A-SIT Plus credentials collection:
- EU PID
- EU PID SD-JWT
- Mobile Driving Licence
- Power of Representation
- Certificate of Residence
- Tax ID
- European Health Insurance Card
- Age Verification
The exact format availability depends on each scheme's VC-K representation support.
Wallet
| OpenID4VCI / OpenID4VP
v
Spring Boot HTTP service
|-- OAuth2Controller PAR, authorize, token, OAuth/OIDC metadata
|-- OpenId4VciController issuer metadata, nonce, credential endpoint
|-- PublicController login, OIDC login choices, EU PID OpenID4VP login
|-- StatusListController status list and aggregation endpoints
|-- RevocationController debug self-revocation UI
|
|-- VC-K IssuerAgent / CredentialIssuer / SimpleAuthorizationService
|-- DataExtractor and OidcIssuerCredentialDataProvider
|-- JPA repositories for prepared, issued, and revoked credentials
- JDK 21
- A database supported by Spring Data JPA
- H2 works for local development and tests
- PostgreSQL is the intended production option
This repository is useful as a reference implementation and test issuer, but it is not suitable to run unchanged as a production service. The current code has several deliberate development shortcuts and operational limitations:
- Demo authentication requires explicit configuration. A form-login demo user is only created when
spring.security.user.passwordis set; without it no demo user exists and the only way to log in is through a configured OIDC provider or an EU PID presentation. CSRF protection is disabled, and every request is permitted except/authorizeunless method-level annotations add further checks. - Sessions and transient issuance state are in memory. Spring sessions use
MapSessionRepository, and credential offers plus OpenID4VP login transactions useDefaultMapStore. Login state, offers, and transactions are lost on restart and are not shared across multiple service instances. - Default keys are ephemeral.
backend.issuer-key.type,backend.iso-mdoc-issuer-key.typewhen set, andbackend.verifier-key.typedefault toMEMORY, which creates fresh self-signed key material on startup. A restart changes issuer/verifier identity unless file or keystore-backed keys are configured. - Credential claims are partly synthetic. The claim builders in
DataExtractor.ktandFakeDataExtractor.ktfill many fields with random or placeholder values, including document numbers, tax data, driving privileges, address fallbacks, biometrics, and development-user identity data. - Subject matching is not production-grade. Revocation ownership maps ID Austria users by
givenName familyName birthDate; the code comments note that this is only a development-stage workaround and that collisions may happen. - Status-list storage is local filesystem based.
RevocationListWriterwrites JWT and CWT status lists underbackend.revocation-list.path;StatusListControllerserves those files directly. This needs shared durable storage or another publication strategy for clustered deployments. - Scheduling and locking are single-node oriented. Status-list refresh state is held in memory, and
credential repository updates rely on a JVM-local
synchronizedlock. That does not coordinate multiple running instances. - Database management is minimal. The README examples use
hibernate.ddl-auto: update; there are no explicit schema migrations. Identity-column recovery is implemented only for H2 and PostgreSQL and should not replace normal migration and database operations. - Debug and demo surfaces are exposed. The root page generates credential-offer QR codes, including
pre-authorized offers for logged-in users, and
/revocationis explicitly described in code as a debug self-revocation mechanism. - Logging may expose personal or credential data. Several controllers and data providers log request bodies, authentication subjects, credential data, status-list content, and issuance details at info, debug, or verbose levels.
- Credential support is not complete for every scheme/format combination. Unsupported combinations in
the claim builders currently reach
TODO(...), which can fail at runtime if new metadata exposes formats without matching claim construction. - Trust and authorization policy are application-specific. The service demonstrates OIDC and EU PID input, but production deployments still need issuer policy, claim provenance checks, audit rules, rate limiting, secret management, TLS/proxy hardening, and incident operations around this code.
Run the HTTP service locally:
./gradlew :http:bootRunThe service starts on port 8080 by default. Open:
http://localhost:8080/for the issuer UIhttp://localhost:8080/loginfor OIDC and EU PID login optionshttp://localhost:8080/.well-known/openid-credential-issuerfor OpenID4VCI issuer metadata
Run the test suite:
./gradlew testBuild the bootable service artifact:
./gradlew :http:bootJarBuild a container image:
docker build -t wallet-issuing-backend .Run it locally:
docker run -p 8080:8080 wallet-issuing-backendPass JVM options or change the port through environment variables the image exposes:
docker run -p 9090:9090 \
-e PORT=9090 \
wallet-issuing-backendAny application.yml property can be supplied as an environment variable using Spring Boot's
relaxed binding
(e.g. BACKEND_PUBLIC_CONTEXT=https://issuer.example.com).
| Endpoint | Purpose |
|---|---|
/.well-known/openid-credential-issuer |
OpenID4VCI credential issuer metadata |
/.well-known/openid-configuration |
OpenID provider metadata |
/.well-known/oauth-authorization-server |
OAuth2 authorization server metadata |
/.well-known/jwt-vc-issuer |
JWT VC issuer metadata |
/par |
Pushed authorization request endpoint |
/authorize |
Authorization endpoint |
/token |
Token endpoint |
/nonce |
Credential proof nonce endpoint |
/credential |
OpenID4VCI credential endpoint |
/credentials/status/current |
Current status-list aggregation |
/credentials/status/{timePeriod} |
Status list for a time period |
/offer/{nonce} |
Credential offer JSON resolved by nonce (linked from QR code) |
/dcapi/create-request/{nonce} |
Digital Credentials API issuance payload resolved by nonce |
/transaction/result |
OpenID4VP transaction result callback |
/transaction/get |
OpenID4VP transaction state polling |
/login |
User login page for OIDC and EU PID based identity input |
/logout |
Session logout |
/revocation |
Debug self-revocation view |
Custom properties live under the backend prefix and are defined in
BackendConfigurationProperties.kt.
backend:
public-context: "http://localhost:8080/"
credentials:
lifetime: P7D
revocation-list:
lifetime: P7D
regular-write-timeout: P5D
dirty-check-rate: PT10M
regular-check-rate: PT1H
path: cache/revocation-lists/
metadata:
name: "A-SIT Plus Wallet Issuer"
logo: "https://wallet.a-sit.at/assets/images/logo.svg"
issuer-key:
type: MEMORY
iso-mdoc-issuer-key:
type: MEMORY
verifier-key:
type: MEMORYKey settings:
backend.public-contextis the externally reachable base URL. It is used in wallet-facing metadata, credential offers, redirects, and status-list URLs.backend.credentials.lifetimecontrols issued credential validity as an ISO-8601 duration, for examplePT60M,P7D, orP180D.backend.metadata.nameandbackend.metadata.logopopulate OpenID4VCI display metadata.backend.issuer-keysigns JWT VC and SD-JWT VC credentials and status lists.backend.iso-mdoc-issuer-keyoptionally signs ISO mdoc credentials. When omitted, ISO mdoc credentials are signed withbackend.issuer-key.backend.verifier-keysigns OpenID4VP authentication requests for EU PID login.
For local development, MEMORY creates an ephemeral key pair with a self-signed certificate:
backend:
issuer-key:
type: MEMORY
iso-mdoc-issuer-key:
type: MEMORY
verifier-key:
type: MEMORYFor deployments, load PEM encoded key material:
backend:
issuer-key:
type: FILE
file:
private-key: file:issuer-key-private.pem
public-key: file:issuer-key-public.pem
certificate: file:issuer-cert.pem
iso-mdoc-issuer-key:
type: FILE
file:
private-key: file:mdoc-issuer-key-private.pem
public-key: file:mdoc-issuer-key-public.pem
certificate: file:mdoc-issuer-cert.pemOr load a Java KeyStore:
backend:
issuer-key:
type: KEYSTORE
keystore:
path: file:/some/path/keystore.p12
type: PKCS12
provider: BC
password: changeit
alias: key1
alias-password: changeit
iso-mdoc-issuer-key:
type: KEYSTORE
keystore:
path: file:/some/path/mdoc-keystore.p12
type: PKCS12
provider: BC
password: changeit
alias: mdoc-key
alias-password: changeitToken status list output is controlled by backend.revocation-list:
lifetime: lifetime of a generated status list credential, defaultP7D.regular-write-timeout: maximum age before a list is written again, defaultP5D.dirty-check-rate: interval for writing lists after revocation changes, defaultPT10M.regular-check-rate: interval for refreshing outdated lists, defaultPT1H.path: directory where status lists are written, defaultcache/revocation-lists/.
Credentials are derived from the authenticated user's identity data.
The service supports:
- A development username/password user configured by Spring Security.
- Any Spring Security OAuth2 client registration.
- EU PID login through OpenID4VP presentation.
A form-login demo user is only created when spring.security.user.password is explicitly set.
Without it no local user exists and login is only possible through a configured OIDC provider or
an EU PID presentation.
spring:
security:
user:
name: alice # defaults to "user" when omitted
password: changeitThe password is stored in plain text internally (no hashing), so this account is intended only for development and testing. Do not use it in production.
Example OIDC client registration for ID Austria:
spring:
security:
oauth2:
client:
registration:
ida:
client-id: "https://example.com"
client-secret: "your-client-secret"
client-name: "IDA"
scope: "openid, profile"
client-authentication-method: client_secret_post
authorization-grant-type: authorization_code
redirect-uri: "https://example.com/login/oauth2/code/ida"
provider:
ida:
issuer-uri: "https://idp.id-austria.gv.at"Claim extraction and credential payload construction are implemented in
DataExtractor.kt and
OidcIssuerCredentialDataProvider.kt.
The service includes an endpoint for browser-based issuance via the W3C Digital Credentials API. The index page generates DC API payloads alongside the standard OpenID4VCI credential offer QR codes.
/dcapi/create-request/{nonce}returns a pre-authorizedCredentialCreationOptionspayload that a browser page can pass directly tonavigator.credentials.create().- The static file
dcapi.jscontains the client-side logic for triggering the DC API call from the index page.
This path skips the redirect-based authorization code flow and is suited for same-device issuance in a browser that supports the Digital Credentials API.
The service uses custom URL schemes that follow the HAIP (High Assurance Interoperability Profile) specification for wallet invocation:
haip-vci://— credential offer deep links for HAIP-compliant walletshaip-vp://— verifiable presentation request deep links (EU PID login)av://— credential offer deep links for Age Verification
These are used in the QR codes and redirect URIs generated by the index and login pages.
For local development and tests, H2 is enough:
spring:
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
jdbc:
lob:
non_contextual_creation: true
datasource:
url: "jdbc:h2:mem:userstore"For deployments, configure PostgreSQL:
spring:
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
jdbc:
lob:
non_contextual_creation: true
datasource:
url: "jdbc:postgresql://server:port/db_name"
driver-class: "org.postgresql.Driver"
username: username
password: password
hikari:
auto-commit: falseSpring Boot server settings can be configured in the usual way:
server:
port: 8080
servlet:
context-path: /
forward-headers-strategy: frameworkIf the service is deployed under a context path, some wallet metadata lookups may still need to be available from the web server root. One Apache rewrite setup is:
RewriteRule ^jwt-vc-issuer/(.*)$ /$1/.well-known/jwt-vc-issuer [L]
RewriteRule ^mdoc-issuer/(.*)$ /$1/.well-known/jwt-vc-issuer [L]
RewriteRule ^jar-issuer/(.*)$ /$1/.well-known/jwt-vc-issuer [L]
RewriteRule ^oauth-authorization-server/(.*)$ /$1/.well-known/oauth-authorization-server [L]
RewriteRule ^openid-credential-issuer/(.*)$ /$1/.well-known/openid-credential-issuer [L]
RewriteRule ^openid-configuration/(.*)$ /$1/.well-known/openid-configuration [L]Logging example:
logging:
level:
at.asitplus: DEBUG
file:
name: service.logSpring Boot Admin client example:
spring:
application:
name: "Wallet Backend"
boot:
admin:
client:
url: http://localhost:9900
instance:
metadata:
user.name: actuator
user.password: changeit
management:
endpoint:
health:
show-details: always
endpoints:
web:
exposure:
include: "*"The service protects /actuator/** with a dedicated security filter chain:
- SBA not configured (
spring.boot.admin.client.enabledabsent orfalse): all actuator access is denied with HTTP 403. - SBA configured (
spring.boot.admin.client.enabled=true) and bothuser.name/user.passwordset: the actuator endpoints require HTTP Basic auth using exactly those credentials. Requests without credentials receive HTTP 401; wrong credentials receive HTTP 401; authenticated users without theACTUATORrole (including the demouseraccount) receive HTTP 403.
Spring Boot Admin reads the same user.name / user.password metadata when it polls the actuator, so
the credentials only need to be configured once.
The service includes the Spring Cloud Config client, but it stays inactive unless you enable a config import explicitly. This keeps local development and standalone deployments working without a config server.
To enable remote configuration from an internal Spring Cloud Config Server, set these environment variables:
SPRING_CLOUD_CONFIG_ENABLED=true
SPRING_CLOUD_CONFIG_URI=http://internal-config-service:8888Recommended additions:
SPRING_CONFIG_IMPORT=optional:configserver:if you want to override the default import value explicitlySPRING_APPLICATION_NAME=wallet-issuing-backendif your deployment overrides the application name and you want the config server lookup key to stay stable.SPRING_PROFILES_ACTIVE=<profile>if the remote config is profile-specific.
Behavior notes:
- With
optional:configserver:, the service still starts if the internal config service is unavailable. - If you want startup to fail when the config service cannot be reached, remove
optional:and useSPRING_CONFIG_IMPORT=configserver:.
http/src/main/kotlin/at/asitplus/wallet/backend
config/ Spring and VC-K wiring, key loading, credential claim builders
controller/ OpenID4VCI, OAuth2, login, status-list, and revocation endpoints
data/ JPA entities, repositories, and VC-K credential data provider
service/ Revocation and status-list scheduling services
See CHANGELOG.md for version history.
External contributions are greatly appreciated! Be sure to observe the contribution guidelines (see CONTRIBUTING.md). In particular, external contributions to this project are subject to the A-SIT Plus Contributor License Agreement (see also CONTRIBUTING.md).
The Apache License does not apply to the logos, (including the A-SIT logo) and the project/module name(s), as these are the sole property of A-SIT/A-SIT Plus GmbH and may not be used in derivative works without explicit permission!