Skip to content

About

Generic Java SDK for M2M (client_credentials) authentication against OID4VP-style Verifiers using private_key_jwt + VP-JWT proof of possession

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

EUDIStack Verifier Client (Java)

Quality Gate Status Coverage Security Rating Maintainability Rating

A small, dependency-light Java SDK for machine-to-machine (M2M) authentication against OID4VP-style Verifiers, using the client_credentials grant with private_key_jwt (RFC 7523) and a self-signed Verifiable Presentation JWT as proof of possession.

It is not tied to any single Verifier implementation — any Verifier that accepts this wire protocol (self-signed VP-JWT + client_assertion carrying vp_token, X-Tenant header, standard OAuth2 token response) can be used as the target.

No Spring, no reactive stack, no DI framework. Just Nimbus JOSE+JWT for JWS/JWK handling and the JDK's own java.net.http.HttpClient.

Who this is for

Any service that holds:

  1. An EC (P-256) private key, and
  2. A machine credential (e.g. a LEARCredentialMachine) that binds that key as its confirmation key (cnf),

and needs to obtain an OAuth2 access token from a Verifier that trusts that credential.

Installation

Gradle

dependencies {
    implementation 'net.eudistack:eudistack-verifier-client-java:<version>'
}

Maven

<dependency>
    <groupId>net.eudistack</groupId>
    <artifactId>eudistack-verifier-client-java</artifactId>
    <version>&lt;version&gt;</version>
</dependency>

Quick start

You configure 3 values — your private key, your machine credential, and the Verifier's base URL. The tenant and client_id are derived automatically from the credential, so there's nothing else to keep in sync.

Programmatic

VerifierM2MClient client = VerifierM2MClient.builder()
    .verifierUrl("https://verifier.example.org")
    .privateKey(privateKey)             // JWK JSON or a raw hex scalar — see below
    .credentialJwt(machineCredentialJwt)   // or .credentialJwtFile(Path.of("credential.jwt"))
    .build();

AccessToken token = client.authenticate();
System.out.println(token.accessToken());

build() validates that your private key matches the credential's cnf immediately, so a misconfigured key/credential pair fails at startup — not on the first request that happens to need a token.

From a config file

VerifierM2MClient client = VerifierM2MClient.fromConfig("verifier-client.yaml");
AccessToken token = client.authenticate();

Configuration reference

verifier.url is the remote service you're connecting to. Everything under verifier.client.* is yours — the private key and credential you authenticate with, not anything belonging to the Verifier.

Key Required Meaning
verifier.url yes The Verifier's base URL, e.g. https://verifier.example.org
verifier.client.private-key one of these two Your private key, inline — see accepted formats below
verifier.client.private-key-path Path to a file containing the private key (relative to the config file)
verifier.client.credential-jwt one of these two Your machine credential, inline compact JWT
verifier.client.credential-jwt-path Path to a file containing the credential JWT (relative to the config file)

Set exactly one of the inline/path variants for the key, and exactly one for the credential.

Accepted private key formats

Both the inline value and the file content accept either:

  • JWK JSON (RFC 7517): {"kty":"EC","crv":"P-256","x":"...","y":"...","d":"..."}

  • Raw hex scalar: 0xb8c069add118093e5c0d192a8edd64b426a898c933089703e63b595f74b3edd6 or the same without the 0x prefix — just the private value d, with or without leading-zero padding to the full 32 bytes. Some issuers hand out the key this way instead of as a JWK; the public (x, y) coordinates are derived from it automatically, with no extra step on your side.

    If you set this inline in a .yaml file, quote it (verifier.client.private-key: '0xb8c0...'). An unquoted value that looks numeric is parsed by YAML as a number, not a string — this SDK rejects that with a clear error rather than silently loading the wrong key, but quoting avoids the error entirely. .properties files and the programmatic builder are unaffected.

.properties

verifier.url=https://verifier.example.org
verifier.client.private-key-path=key.jwk.json
verifier.client.credential-jwt-path=credential.jwt

.yaml

verifier:
  url: https://verifier.example.org
  client:
    private-key-path: key.jwk.json
    credential-jwt-path: credential.jwt

YAML support requires org.yaml:snakeyaml on your classpath — it's an optional dependency of this SDK, only needed if you load a .yaml/.yml file. If you only use .properties files or the programmatic builder, you don't need it.

Where do the inputs come from?

  • The private key is generated when you (or your identity provider) create the key pair the mandate is bound to. It must be a P-256 EC key.
  • The machine credential is issued to you by an EUDIStack-compatible Issuer (or any compliant OID4VCI issuer) as a LEARCredentialMachine-shaped Verifiable Credential, with a cnf claim binding your public key.

This SDK does not issue credentials or generate keys — it only consumes them to authenticate.

The cnf binding check

Before making any network call, VerifierM2MClient verifies that the public counterpart of your configured private key matches the credential's confirmation key. Two cnf encodings are supported, both per RFC 7800 (cnf is always a JSON object — never a bare string):

"cnf": { "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." } }
"cnf": { "kid": "did:key:zDnaek9hf761hzFqLFZvTntvhEKfYKiCPaQbV4uXifYV8eHw6#zDnaek9hf761hzFqLFZvTntvhEKfYKiCPaQbV4uXifYV8eHw6" }

The second form (RFC 7800 §3.4) identifies the key by reference: a DID URL — the did:key followed by a #-fragment naming a verification method in its DID document, which for did:key is always identical to the DID's own method-specific identifier.

If they don't match, construction throws CredentialKeyMismatchException — this is a client-side safety check, independent of whatever the Verifier itself validates server-side, meant to catch a misconfigured deployment immediately instead of via an opaque rejection on first use.

Error handling

Exception Thrown when
InvalidConfigurationException A required value is missing/blank, a JWK/JWT is malformed, or a config file is invalid
CredentialKeyMismatchException The private key does not match the credential's cnf
TokenRequestFailedException The Verifier returned a non-2xx response, or was unreachable — carries httpStatus() and responseBody()

All extend VerifierClientException.

Troubleshooting

  • "Verifier response is missing the 'access_token' field" / tenant mismatch errors — the Verifier resolves the tenant from the credential's mandate.power[].domain and compares it against the X-Tenant header this SDK sends automatically. If your credential's mandate doesn't declare the domain you expect, the grant is rejected.
  • CredentialKeyMismatchException on startup — you're pointing at the wrong private key file, or the wrong credential. Confirm the key pair used to request the credential is the one configured here.
  • Encoding gotchas this SDK handles for you — the vp_token claim inside client_assertion must be standard Base64 (not URL-safe), and all iat/exp/nbf claims must be seconds since epoch, not milliseconds. If you're implementing your own client against the same Verifier from another language, these are the two most common interop mistakes.

License

Apache License 2.0 — see LICENSE.

About

Generic Java SDK for M2M (client_credentials) authentication against OID4VP-style Verifiers using private_key_jwt + VP-JWT proof of possession

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages