Bridge OIDC - EUDI Wallet
Add identity verification or age verification (18+) with an EUDI wallet to your application using OpenID Connect. The bridge handles the wallet interaction and credential verification; your application receives a signed OIDC result and manages its own accounts, permissions and session.
We host the public bridge on Amazon Web Services (AWS) in Europe, in the Paris
region (eu-west-3, France). See Hosting and security
for the infrastructure and data-location details.
Use this guide with WordPress, Moodle, Keycloak, a reverse proxy, a CMS, or any compatible server-side OIDC client. Start by registering a client and configuring Discovery. Then follow the recipe for your software. No Wallet protocol implementation is needed in your application.
| Your integration | Start here |
|---|---|
| WordPress / WooCommerce | WordPress setup |
| Keycloak as an identity broker | Keycloak setup |
| Protect an existing website with a proxy | OAuth2 Proxy setup |
| Nextcloud | Nextcloud setup |
| Moodle — dedicated EUDI Wallet authentication plugin | Moodle setup and plugin download |
| Other CMS, identity software or custom application | Common OIDC settings and OIDC client libraries |
| Check age without identifying a user | Age verification |
| Use France Identité Partenaires | France Identité compatibility |
| Understand data retention | Privacy and data retention |
Dedicated EUDI Wallet Login plugins are available for WordPress / WooCommerce and Moodle. Download and installation instructions are included below. Moodle provides identity sign-in; the WordPress plugin also offers an age-verification mode. Other software recipes are based on publisher documentation. Validate the complete Wallet login in your own environment before opening access to users.
1 Register your application
Create a free application client. The form works for an application, CMS, plugin, identity broker such as Keycloak, or other OIDC software.
- Enter the company name, the contact's first and last name, and the developer contact email. This address is used to manage the client; it is separate from the attributes of people who will sign in.
- Enter a public application name and the exact HTTPS callback URL
(
redirect_uri) supplied by your software, including its path and any query string. The callback must process OIDC responses. A website homepage is suitable only if it actually implements that callback. - Choose Identity verification or Age verification (18+). For identity verification, you may also authorize a separately verified user contact email. An age client receives no name, birth date or user email.
- Confirm that you have read the linked terms of use and privacy policy, then request the email code.
- Check the application/service/callback summary and enter the six-digit code sent to the developer contact. It expires after five minutes, permits five attempts, and can be resent after 60 seconds. You can correct the contact details before completing registration.
- After successful email verification, the service creates the client and
generates its
client_idand secret. Copy the issuer, Client ID and secret into your application. The secret appears only in this response, for at most five minutes with JavaScript enabled. Close removes it from the page; the client remains active. Reopening the form does not reveal the secret.
You can also select Download configuration (JSON) before the five-minute display expires. The file contains the issuer, Discovery URL, Client ID, client secret, callback, scopes, token authentication method, S256 setting and identity/age purpose. Use these values in your application's settings; in EUDI Wallet Login, select Choose the configuration file (JSON) to load the file. Other software may require manual entry. Keep the file private. Closing the page or expiration of the display does not delete a downloaded file. The browser generates it from this one-time response; there is no later secret-download endpoint.
Company/contact details and the record of document acknowledgement are available only to the service's authenticated administrators. They are not sign-in claims. No client is created while the email challenge is pending. Registration does not create or link accounts inside your application.
A free client uses Authorization Code, client_secret_basic, PKCE S256 and one
registered HTTPS callback. An identity client requests openid profile for first
and last name, optionally email for email / email_verified. An age client
requests openid age_verification for age_over_18=true. Use separate clients
for identity and age verification, staging and production.
| Free-service limit | Default |
|---|---|
| Login starts | 100 per UTC day, shared by free clients with the same callback hostname |
| Concurrent logins | 3 per callback hostname |
| Client registrations | 3 per UTC day for each callback hostname, source IP and verified developer email |
| Email code | 5 minutes, 5 attempts, 60-second resend cooldown |
The form displays the service's configured limits. Failed and abandoned login starts count. Daily allowances reset at midnight UTC. Reaching a limit does not trigger a charge; wait for capacity or the next day. Changing a callback hostname can also change user identifiers; plan this before moving an application.
Registration validates callback syntax and restrictions, without probing the
callback over HTTP. A working callback may reject a request without code and
state, while an ordinary page may return HTTP 200 without supporting OIDC.
Validate it through a complete login.
For lost secrets, additional callbacks or a different client authentication method, contact the service operator. PKCE S256 is required for every client, including confidential clients. The form is an interactive website, not a Dynamic Client Registration API. Discovery does not create a client.
2 Configure using discovery
Issuer:
https://login.openid4vc-hub.com
Discovery document:
https://login.openid4vc-hub.com/.well-known/openid-configuration
Configure an OIDC client library with the issuer (or its discovery URL), your client ID, registered callback and, for a confidential client, its secret. Enable Authorization Code and PKCE S256. Use discovery to locate authorize, token, UserInfo and JWKS instead of hardcoding those endpoints. Require the metadata issuer to equal the configured issuer exactly and retrieve it over verified HTTPS.
The document advertises:
{
"issuer": "https://login.openid4vc-hub.com",
"authorization_endpoint": "https://login.openid4vc-hub.com/authorize",
"token_endpoint": "https://login.openid4vc-hub.com/token",
"userinfo_endpoint": "https://login.openid4vc-hub.com/userinfo",
"jwks_uri": "https://login.openid4vc-hub.com/jwks",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"response_modes_supported": ["query", "form_post"],
"subject_types_supported": ["pairwise"],
"id_token_signing_alg_values_supported": ["RS256"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"]
}
Use the actual document, not this illustrative subset. registration_endpoint,
refresh-token and logout endpoints are intentionally absent. Discovery and JWKS
are public; no verification service key or client secret is required to read them. For local
deployment, substitute the separately configured local HTTPS issuer everywhere.
3 Start an authorization
Create new random values for each login:
stateto bind the callback to the initiating browser transaction;nonceto bind the ID Token to that authentication request;code_verifier, a high-entropy string of 43–128 characters fromA–Z a–z 0–9 - . _ ~;code_challenge = BASE64URL(SHA256(ASCII(code_verifier))), without padding.
Keep state, nonce and code_verifier in your application's server-side transaction store, with an expiry and browser binding. Do not put the verifier in the authorization URL. Do not reuse it across tabs or login attempts.
Example authorization parameters (URL-encode all values):
GET <authorization_endpoint>?
response_type=code&
client_id=<registered-client-id>&
redirect_uri=https%3A%2F%2Fapplication.example%2Foidc%2Fcallback&
scope=openid%20profile&
state=<random-state>&
nonce=<random-nonce>&
code_challenge=<S256-challenge>&
code_challenge_method=S256&
response_mode=query
redirect_uri is required and must match an entry exactly. openid is
required. profile and optional email are allowed only if registered for your client.
The provider advertises email only when email delivery is enabled.
query is the default response mode; form_post returns an auto-submitted HTML
form to your callback instead. Handle SameSite cookies correctly if choosing
form_post. Use HTTPS for callbacks in production and phone/same-device tests.
A separately configured loopback-only development installation can use HTTP
callbacks for desktop development; see README.md. That mode is never used for
the public issuer.
The bridge displays the application, requests the identity credential via the verification service, and
returns the browser automatically after a successful verification. The Wallet
authorizes the presentation; there is no second consent screen on the bridge.
A Return to application button is available if automatic return fails; it does
not request additional consent. If a login has already completed, start a new
authorization request rather than resubmitting it or retrying a used code.
Mobile return requires the browser
that initiated the login. Desktop browsers automatically start a cross-device QR presentation; smartphones
start a same-device presentation and display an explicit Wallet deeplink button.
The choice uses browser hints, not a security boundary. Operators can explicitly
disable cross-device presentation with cross_device_enabled: false.
There is no persistent bridge SSO session. prompt=none fails with
login_required; a fresh login uses a new Wallet presentation. The implementation
accepts prompt=login, consent or select_account; account selection is made
in the Wallet. It validates nonnegative max_age and performs a fresh
presentation. Unsupported claims/request-object parameters are rejected rather
than forwarded to the verification service.
4 Process the callback and exchange the code
The successful callback contains code and the original state. Before calling
the token endpoint, validate state, browser binding, expiry and one-time use.
Handle error callbacks too, always checking their state. A callback alone does
not establish a session.
Exchange the code promptly (60-second lifetime) at the discovered token endpoint:
POST <token_endpoint>
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <base64(client_id:client_secret)>
grant_type=authorization_code&code=<code>&redirect_uri=<exact-registered-uri>&code_verifier=<original-verifier>
For client_secret_post, omit Basic authentication and include client_id and
client_secret in the form. For a public client, include client_id and no secret.
PKCE remains required in all cases. Do not mix authentication methods. The token
endpoint accepts form-encoded POST bodies, not query parameters or JSON.
Success returns an RS256-signed id_token, an opaque Bearer access_token,
expires_in=300 and the granted scope. There is no refresh token. Codes are
one-time: a lost token response requires a new login, not repeated redemption.
Use an OIDC library to verify the ID Token against the discovered JWKS. Validate
its signature and permitted algorithm, iss, aud, expiry and the exact nonce.
For multiple audiences, validate azp as required by OIDC. Never accept alg=none,
an unverified decoded JWT or an audience belonging to another application.
The bridge returns an at_hash binding the access token to the ID Token.
Then establish your application's own session and rotate its session identifier. The bridge does not manage the application's accounts, roles or permissions.
5 UserInfo and attributes
GET <userinfo_endpoint>
Authorization: Bearer <access_token>
POST is also supported with the same Authorization header. Do not place access
tokens in a URL. Check that UserInfo sub equals the ID Token's sub.
The current endpoints are intended for server-side HTTP exchanges; browser SPA cross-origin Token/UserInfo requests are not enabled by CORS in this MVP. Public clients can use a server callback/BFF without a client secret. A direct browser-only SPA needs a separately reviewed CORS integration.
| Scope | Possible authorized claims |
|---|---|
openid |
Pairwise sub, with standard ID Token protocol claims |
profile |
given_name, family_name, birthdate, place_of_birth, restricted by client policy |
email |
email, email_verified: true, separately verified by the bridge and authorized for this client |
place_of_birth is a bridge profile extension, not an OIDC Core standard claim.
With the Rulebook policy, it is a JSON object containing at least one of country, region or locality,
for example {"locality": "Paris", "country": "FR"}. It is included only when
profile is requested and the client is authorized for this claim. The same
projection is applied to the ID Token and UserInfo.
Email is optional and is verified separately by the bridge, never certified by France Identité or obtained from its identity credential. Nationality is neither requested nor used by the bridge. The verification service verifies the four configured identity attributes, independently of the subset shared with your application. No raw identity credential, CBOR, JWT credential, Wallet proof or key is returned.
5.1 Optional email verification
Register the email scope and both email claims, then request
scope=openid profile email. After Identity verification, the bridge asks for a contact
address and a six-digit email code unless a contact for this application sector
was verified within 30 days. The code is entered in the original browser, not
returned to your application. The ID Token and UserInfo include the same verified
email. email_verified: true means the bridge verified control of the mailbox;
it does not assert that the identity credential provider certified this address or that it is
unique to this legal person.
Use prompt=consent with the email scope to show the saved contact and offer a
change verified by a new code. Email never changes sub. Do not link or merge
accounts automatically by email. If email delivery is unavailable or the client
is not authorized, handle the OIDC error; never invent a fallback address.
An email-flow login may take up to 15 minutes including the identity verification stage, so retain
your state/nonce/PKCE transaction long enough (900 seconds) without changing the
short authorization-code lifetime. Cancellation still returns an OIDC error.
The bridge's registration, PKCE and signature-validation requirements are unchanged.
6 User identifiers and existing accounts
Store the identity key as (iss, sub). The sub is an opaque stored identifier
for the subject and the application's sector. In this MVP the sector is the host
of the registered callback URLs: different sectors receive different subs;
clients sharing a sector can share the same sub. Changing an application's
callback hostname can change its sub and needs a planned account migration.
At the operator's chosen policy, the internal lookup uses HMAC-SHA256 over canonical given name, family name, birth date and birthplace, with an explicit identity domain and derivation version 2. The input is never stored as plain identity data in PostgreSQL. The pairwise sub is random and persisted; neither the HMAC nor the canonical identity is exposed to the client.
This policy has an explicit limit: identical four-field identities are merged, and a correction, name change or different birthplace representation can produce another subject. Credential renewal preserves sub only when these inputs and the sector remain the same. It is not a guaranteed unique legal-person identifier. Do not automatically link existing accounts by email, name, date of birth or a newly received sub.
The application owns account linking: first authenticate the existing local
account, require an explicit recent authorization to link, then complete a fresh
OIDC login and store (iss, sub). Prevent linking an already attached identity to
another account.
7 Verify age without identifying a user
Choose Age verification (18+) in the
client registration form.
Use a dedicated client and the same OIDC endpoints and validation steps as for
Identity verification, with scope=openid age_verification and mandatory PKCE S256.
Do not request profile or email for this client.
The Wallet presents a Proof of Age mdoc (eu.europa.ec.av.1) disclosing only the
boolean age_over_18. The bridge accepts only true after the verification service has verified
the credential, holder binding, validity and established issuer trust. The
presentation uses the dedicated CORE flow with an unsigned request, av://
and direct_post; it is not a HAIP presentation. This follows the request shape
shown in the Age Verification profile,
without claiming exhaustive implementation of that profile.
A successful ID Token and UserInfo include age_over_18: true. The OIDC sub
is a random identifier for this verification, changes on the next verification,
and is not stored as a persistent bridge identity. Do not use this subject to
create, link or recover an account. No identity credential, name, birth date, birthplace or email
is returned. A refusal, an invalid result or a missing/false age claim must not
grant access. The bridge does not infer age from a date of birth.
Your application controls the short-lived access permission after validating the OIDC response. The WordPress plugin retains a protected browser age session for five minutes, without creating or signing into a WordPress account. The plugin's age mode gates public WordPress pages and REST access. Exclude protected content from page/CDN caches: caches can serve a response before PHP runs. Public media URLs and files served directly by the web server require their own access rules. This feature alone does not establish regulatory compliance or prevent shared-device use.
8 Connect your software
For WordPress / WooCommerce and Moodle, install our dedicated plugins using the instructions below. They handle the OIDC connection to the bridge; no additional OIDC extension or identity broker is required.
For other software, choose an OIDC client extension that signs users into your application through an external provider. Check support for Authorization Code, PKCE S256, signed ID Token validation and nonce validation before installing or purchasing it. An extension that turns the CMS into an OIDC provider serves a different purpose.
8.1 Settings shared by OIDC clients
First copy the callback URL generated by your software and open the
free client registration form.
Supply the exact URL, including path and query string, and verify the developer
contact email. Use a separate registration for staging and production. The free
form issues a confidential client using client_secret_basic; other client
types require an arrangement with the operator.
| Application setting | Value |
|---|---|
| Provider / issuer | https://login.openid4vc-hub.com |
| Discovery / configuration URL | https://login.openid4vc-hub.com/.well-known/openid-configuration |
| Authorization endpoint, if entered manually | https://login.openid4vc-hub.com/authorize |
| Token endpoint, if entered manually | https://login.openid4vc-hub.com/token |
| UserInfo endpoint, if entered manually | https://login.openid4vc-hub.com/userinfo |
| JWKS URL | https://login.openid4vc-hub.com/jwks |
| Grant / response type | Authorization Code / code |
| PKCE | Enabled, method S256, including for confidential clients |
| Token authentication | The registered method: client_secret_basic, client_secret_post or none |
| Client ID / secret | Values supplied for this application; keep the secret server-side |
| Scopes | openid profile; add email only when authorized and needed |
| Signature algorithm | RS256, verified using JWKS |
| External account identifier | sub, stored together with the issuer |
| Response mode | query for the initial integration |
| Logout / refresh | Leave provider logout unset; do not request offline_access or require refresh tokens |
| Login transaction timeout | 300 seconds for identity only; 900 seconds if requesting email |
Prefer Discovery when the extension supports it. Keep HTTPS certificate, state,
nonce, issuer, audience and expiry validation enabled. A client secret does not
replace PKCE. The authorization code must still be exchanged within 60 seconds.
If your CMS requires an email to create accounts, request openid profile email
and have both email and email_verified authorized for the client. Email is
verified separately after the Wallet step; it is not an address certified by the identity provider.
8.2 Keycloak identity broker
Use Identity Providers → Add provider → OpenID Connect v1.0 in your realm.
Choose an alias such as eudi-wallet and copy the displayed Redirect URI for
registration. It commonly ends in /realms/<realm>/broker/eudi-wallet/endpoint;
use the actual value, including any reverse-proxy path. Create an Identity
verification client using the registration form,
verify your developer email, and copy the issued Client ID and one-time secret.
Then enter those credentials in Keycloak.
Your applications remain clients of Keycloak; Keycloak is the client of the bridge.
| Keycloak provider setting | Value |
|---|---|
| Discovery / issuer | Bridge Discovery URL / issuer from section 2 |
| Client ID / secret | Credentials issued for this Keycloak callback |
| Client Authentication | Client secret sent as Basic authentication |
| Use PKCE / PKCE Method | On / S256 (pkceEnabled=true, pkceMethod=S256) |
| Validate Signatures / Use JWKS URL | On / On; use the bridge JWKS URL |
| Default Scopes | openid profile; add email only when authorized |
| Disable nonce | Off |
| Access token is JWT | Off: the bridge access token is opaque |
| Store Tokens | Off unless your integration explicitly needs token storage |
| Logout URL / backchannel logout | Unset / Off |
Forward prompt=none |
Off: a new Wallet presentation is required |
Map given_name and family_name; use the external sub association for
identity. Configure first broker login to require authentication to an existing
local account before linking; avoid automatic linking by email. Assign realm
roles locally. An age client's changing sub is unsuitable for ordinary brokered
account creation; this recipe is for identity verification.
Sources: Keycloak OIDC identity brokering and first login, Keycloak outbound PKCE configuration.
8.3 OAuth2 Proxy
Use the generic oidc provider to protect an existing server-side website.
First choose the proxy's exact HTTPS callback, normally /oauth2/callback.
Open the registration form, register
that callback as an Identity verification client with verified user email
authorized, and verify your developer email. Copy the issued Client ID and
one-time secret before configuring the proxy.
Configure provider=oidc, oidc_issuer_url, client_id, a privately stored client
secret, redirect_url, scope="openid profile email",
code_challenge_method="S256" and insecure_oidc_skip_nonce=false.
Keep issuer, signature and email verification enabled, and set an explicit email
access policy for your application. Do not require groups or refresh tokens.
Use secure proxy session cookies and your own session expiry. This is an
integration starting point, not a tested turnkey proxy deployment.
Sources: OIDC provider, PKCE, nonce and configuration options.
8.4 Nextcloud
Install a compatible user_oidc app. Add the bridge provider in Nextcloud's
OpenID Connect administration and copy its callback URL. Before entering client
credentials, open the registration form,
register that exact callback for Identity verification, and verify your
developer email. Copy the issued Client ID and one-time secret into Nextcloud,
then configure Discovery. Map user ID to sub; authorize/request user
email only if needed. Keep the provider-specific user-ID prefix enabled.
Keep use_pkce=true; set send_userinfo_claims=false because this bridge rejects
OIDC claims requests. Set enable_default_claims=false, map only attributes
provided by the bridge, and disable single_logout. Review provisioning before
launch: soft_auto_provision=false avoids automatic matching to accounts in
other backends. Existing accounts need an explicitly authenticated linking process.
Source: Nextcloud user_oidc configuration.
8.5 Other OIDC applications and libraries
First define your application's exact HTTPS callback and open the registration form. Choose identity or age verification, verify your developer email, and save the issued Client ID and one-time secret on your server. Then configure your OIDC library with these credentials and the common settings. These are library choices, not bridge-specific connectors or completed interoperability certifications:
| Stack | OIDC client | Configuration to map |
|---|---|---|
| JavaScript / Node.js | openid-client | Discovery, client authentication, fresh state/nonce/verifier, S256 and callback validation |
| Python, including Flask / Django / ASGI | Authlib | Discovery, openid profile, S256, server-side state and ID Token validation |
| Java / Spring | Spring Security OAuth2 Login | Issuer URI, client registration, callback and S256 for the confidential client |
| ASP.NET Core | OpenID Connect handler | Authority, Code response type, PKCE and the exact registered token authentication method |
Confirm outbound S256 even for confidential clients. Some libraries default
to client_secret_post; the free registration form issues client_secret_basic.
Configure the library accordingly, or arrange a matching client method with the
service operator. Request only supported scopes, disable automatic
offline_access, keep nonce/signature/issuer/audience checks enabled and store
the secret server-side. A browser-only SPA needs a server callback/BFF because
the bridge does not enable cross-origin Token/UserInfo exchanges.
For another identity broker, configure its external OIDC source with the common settings and enable outbound S256, nonce and signed ID Token validation. SAML, LDAP, SCIM and Client Credentials are not direct sign-in interfaces of this bridge. A broker can translate into other protocols on its own side after OIDC login.
8.6 WordPress setup
Download EUDI Wallet Login 0.1.7 — WordPress plugin ZIP. Install the ZIP directly. WordPress.org publication is being prepared; the plugin is not yet available through the WordPress.org directory. Requires WordPress 6.5 or later and PHP 8.1 or later with OpenSSL and Sodium.
See the WordPress plugin demonstration and installation guide for an explanation in French and a real age-verification button. This public demonstration uses age mode; it does not demonstrate identity sign-in. An English version is also available, with installation instructions and Talao's official contact form.
In WordPress, open Plugins → Add New Plugin → Upload Plugin, select the ZIP, install it and select Activate. Then open Settings → EUDI Wallet Login. The setup screen displays five numbered steps in French or English according to the browser's language preferences (English fallback):
- Copy your return URL with Copy URL.
- Select Register my site to open the
registration form in a new tab.
Paste that exact URL into the return URL (
redirect_uri) field. - Complete the form and verify your developer email using the received code.
- On the confirmation page, select Download configuration (JSON). Keep the file confidential: it contains the client secret.
- Return to WordPress and choose the downloaded configuration file. A separate screen shows its filename and the public settings with the secret hidden. Select Save configuration. No manual Client ID or secret entry is required.
After saving, the settings page opens directly on the saved configuration. Use Import another configuration to return to setup. The imported filename is displayed during review and is not stored in WordPress.
New installations default to Age verification (18+). Choose Identity verification if you need user sign-in. Existing configured modes are preserved. Manual configuration is collapsed by default for users who need to enter credentials themselves. Advanced settings shows Issuer, Callback, Token authentication and Scopes. Import validates this WordPress installation's exact callback, purpose and scopes. The JSON is read locally in the browser and is not uploaded to the provider. Saving applies the settings; it does not test Wallet login. Enable contact email only if it was granted to the client. Choose whether to allow new accounts, save, and test the login button. Existing users can link their Wallet from their own WordPress profile after confirming their local password. Select Identity verification to sign users in, or Age verification (18+) to gate public WordPress content without an account. The same Client ID and Client secret fields apply to the selected mode; previously configured identity and age credentials remain separate internally. PKCE and token validation have no disable switch. Test each mode separately with your Wallet and registered client, including the callback and final result in WordPress. Saving settings or importing JSON does not validate this full flow.
The plugin adds Sign in with your EUDI Wallet to the WordPress login form
and the WooCommerce login form when present. To add it to another page, use the
[eudi_wallet_login] shortcode. In age verification mode, public content is gated
without creating or signing into a WordPress account.
For a public explanation or demonstration site, version 0.1.7 adds Advanced settings → Public demonstration site (age mode only). This explicit option keeps all public pages and public REST content accessible without age proof, including to search engines. The button still performs real age verification and displays the success confirmation. It does not restrict any content. Leave it unchecked on a site whose content must require age verification; protection remains enabled by default, including after an upgrade.
After a successful age verification, version 0.1.6 returns to the local page where verification was started and displays Bravo! Your age has been verified. Select Continue to the site to close the confirmation. This message appears once for that return, not on every page refresh; closing it keeps the temporary five-minute age access. It is shown only after a verified positive result, never on cancellation or from a public URL success parameter. Without JavaScript, a confirmation message remains visible instead of the popup.
The QR screen's Cancel and return to application button sends an OIDC cancellation to the registered callback. EUDI Wallet Login 0.1.5 then returns to the local page from which login was started and displays a one-time message. It keeps that page in the encrypted temporary WordPress transaction, separately from the callback, and never accepts an external return URL. Callback errors also use this return after state/browser validation. Missing, expired or invalid transactions show the safe recovery page. A successful login keeps its existing destination. No callback re-registration or database migration is required.
Account association uses the issuer and sub; the plugin handles the supported
name and email claims automatically. Existing accounts are never linked by email.
New accounts are disabled by default; if enabled, choose Subscriber or Customer
when WooCommerce provides that role. The plugin never assigns an administrator
role. Keep an independent WordPress administrator login while testing.
PKCE S256 and signed token validation are built in; no additional OIDC plugin is needed. Follow the complete login checks in section 8.10 after configuration.
8.7 Drupal setup
With a compatible OpenID Connect / OpenID Client Advanced installation, open
Configuration → People → OpenID Connect, then add an OAuth 2.0 Advanced
client and copy its generated callback URL. First open the
registration form, register that exact
callback for Identity verification, and verify your developer email. Return
to Drupal with the issued Client ID and one-time secret. Set those credentials,
enable automatic discovery with the bridge
issuer, choose scopes, and enable Use PKCE, Send nonce parameter and
Validate ID token signature. Restrict signatures to RS256. Follow the
module instructions
for supplying public keys or JWKS JSON; do not assume a field accepting a JSON
document accepts a JWKS URL. Arrange key updates if the module cannot fetch JWKS.
Use the generated callback for registration and preserve the external sub
association rather than matching existing accounts by email.
8.8 TYPO3 and MediaWiki
For TYPO3, first choose the exact HTTPS redirectUri for your installation.
Open the registration form, register
that callback for Identity verification, and verify your developer email.
Copy the issued Client ID and one-time secret. Then configure the provider in
config/system/oidc.yaml, populate its
clientKey, clientSecret, redirectUri, endpoint and scope settings, and
enable enableCodeVerifier. Map the username from <sub> and the display name
from <given_name> and <family_name>. Leave revokeAccessTokenAfterLogin
disabled: the bridge has no public token-revocation endpoint. Validate the
signature, nonce and S256 checks before enabling frontend or backend login.
For MediaWiki, install compatible OpenID Connect and PluggableAuth extensions
and obtain the callback URL documented by the installed extension. First open
the registration form, register that
exact callback for Identity verification, and verify your developer email.
Copy the issued Client ID and one-time secret. Then, in the OpenID Connect provider's data configuration, set providerURL to the
issuer, clientID and clientsecret to the registered credentials, and
codeChallengeMethod to S256. Register the callback documented by the installed
extension. Keep automatic migration by username/email and single logout disabled;
use the extension's issuer/subject association for account identity.
Sources for the third-party CMS procedures: Drupal Advanced, TYPO3, MediaWiki.
8.9 Moodle setup
EUDI Wallet Login for Moodle is available for download. It adds Sign in with your EUDI Wallet to Moodle's login page so students can identify themselves with their Wallet and sign in to their Moodle account.
Download EUDI Wallet Login 0.1.3 — Moodle plugin ZIP.
The dedicated plugin is auth_eudiwallet, currently an alpha for
Moodle 4.5 LTS, PHP 8.1 or later, with OpenSSL, Sodium and cURL.
The test environment uses Moodle 4.5.15+, PHP 8.3 and MariaDB 11.4.
It is distributed directly from this documentation and is not yet published in
the Moodle plugins directory. The connection is Moodle → Bridge → Wallet →
Moodle; no Keycloak or additional OIDC plugin is required.
Install and register your Moodle application
- In Site administration → Plugins → Install plugins, upload the ZIP and
confirm its installation. Keep a local administrator login for recovery.
If your hosting provider makes the Moodle source directory read-only, ask
its administrator to install the ZIP under
auth/eudiwallet/and run Moodle's plugin upgrade procedure. - Open Plugins → Authentication → EUDI Wallet Login → Configure EUDI Wallet
Login. Copy the exact HTTPS callback displayed by the plugin, normally
https://moodle.example/auth/eudiwallet/callback.php. - Before configuring the connection, open
free client registration.
Select Identity verification and register that exact callback. Authorize
openid profile emailand the verified contact email claim (email_verified). Verify your developer email, then download the private configuration JSON. The developer contact identifies who registers the application; it does not set the email address of students who will sign in. - Import the JSON in Moodle, review its public settings and save. No manual Client ID or secret entry is required. The secret is encrypted in Moodle and used automatically. Keep the downloaded JSON private.
- The page confirms Client configured and shows whether Wallet login is enabled. Registration and replacement JSON controls are collapsed; you do not need to import the file again after saving.
- To allow students without an existing account to register, select Allow Moodle accounts to be created and save. This is off by default and also respects Moodle's site-wide account-creation policy. Keep it off when only explicitly linked existing accounts should be accepted.
- Follow Open Manage authentication, then enable EUDI Wallet Login using the eye icon. Saving a client configuration alone does not enable the authentication method.
Choose the student's starting point
There are two initial journeys. Use Flow 1 if the student does not have a Moodle account yet. Use Flow 2 if they already have a manual-password account and want to attach their Wallet to it. Once the Wallet is associated with an account, subsequent sign-ins use the Wallet button directly; the student does not repeat account creation or linking.
Flow 1 — First Wallet sign-in for a new student
Administrator prerequisite: enable Allow Moodle accounts to be created in the plugin and ensure Moodle's site-wide policy also permits account creation. This option is disabled by default. It is not needed for linking an existing account through Flow 2.
- Open Moodle's login page while signed out. For a test, keep your administrator session in its normal window and use a private browsing window for the student.
- Select Sign in with your EUDI Wallet (Se connecter avec mon Wallet EUDI).
- Present the requested identity in the Wallet and authorize the presentation.
- If the bridge requests a contact email, enter the student's address and confirm the emailed code. For a new test student, use an address that is not already assigned to the administrator or another Moodle account.
- Return to Moodle in the same browser where the sign-in started. On success, Moodle creates the student's account, associates it with the Wallet and signs the student in. The account has no usable local password; future sign-ins use the Wallet.
Creating the account does not enrol the student in a course or grant teacher or administrator privileges. Moodle continues to manage enrolments and roles.
If Moodle displays An account already uses this email, it has not merged or signed into that account. If it is the student's existing account, follow Flow 2. If the address belongs to someone else, use the student's own contact address. Do not attach a student's Wallet to the administrator account to bypass this error.
Flow 2 — Link a Wallet to an existing Moodle account
This flow supports an existing manual-password account. It keeps that account, its profile, course enrolments and permissions. It does not create a second account.
- Sign in to the existing Moodle account using its usual username and password. Check that you are in the student's account, not the administrator's account.
- Open Preferences → Link my EUDI Wallet (Préférences → Lier mon Wallet EUDI).
The linking page is
/auth/eudiwallet/link.phpon your Moodle installation, for examplehttps://moodle.example/auth/eudiwallet/link.php. - Enter that account's local Moodle password to confirm the linking request, then select Sign in with your EUDI Wallet. The password stays in Moodle; it is not sent to the bridge or the Wallet.
- Present your identity with the Wallet. Complete the bridge's separate contact email verification if requested, then return in the same signed-in browser.
- Moodle displays Your Wallet is linked to this Moodle account (Votre Wallet est lié à ce compte Moodle) and returns to Preferences.
- To test the association, sign out, open Moodle's login page and select Sign in with your EUDI Wallet. Present the same identity: Moodle signs you into the existing account, with its existing courses and permissions.
Keep the local login available for recovery. If the password is unknown, use Moodle's password-recovery process or contact the administrator before linking. LDAP, SAML and other external-account linking methods are outside this alpha. A Wallet already associated with another account cannot be reassigned through this flow. Accounts are never associated automatically by matching email addresses.
Sign in again after either flow — email verification
After Flow 1 or Flow 2, the normal return journey is Moodle login page → Sign in with your EUDI Wallet → identity presentation → the same Moodle account. There is no need to reopen the linking page or re-enter the local password for an ordinary Wallet sign-in.
The contact-email challenge is performed separately by the bridge. The address
is not certified by the identity Wallet. This plugin requires
email_verified=true in the signed token.
Students do not have to enter an email code at every login:
- While the Moodle session remains active, no new Wallet presentation or email challenge is needed. Moodle controls its own session duration.
- When a new Moodle login is needed, the student presents their identity with the Wallet again. The bridge can reuse the verified contact email for the same identity and sector for 30 days from its verification.
- Reusing the email does not extend those 30 days. After expiry, or if the stored contact has been removed or is unavailable, a new email code is needed. Requesting a contact-email change also requires a new verification.
Moodle finds the existing account using (issuer, sub), not the email address.
A newly verified email therefore does not create another account as long as
that identity binding remains unchanged. It does not automatically overwrite
an existing Moodle profile email either. The bridge's subject-derivation limits
still apply if identity attributes or the application's sector change.
Accounts are associated by (issuer, sub), never by email. Existing local
profile data is not overwritten on subsequent logins. A verified identity does
not itself prove student status: course enrolments and roles remain controlled
in Moodle. Identity attributes do not grant teacher or administrator privileges.
The plugin implements PKCE S256, nonce, signed token validation, UserInfo binding and expiring single-use state. Configure Moodle cron and disable caching/logging of callback query strings. Tokens, codes and identity documents must not enter access/debug logs. Saving a configuration does not validate a complete Wallet flow. Validate your installation using section 8.10 before giving access to students.
This plugin authenticates students; issuing learning certificates into a Wallet is planned as a separate plugin and is not included in this authentication plugin. Age verification is not a mode of this Moodle authentication plugin.
Sites already using Keycloak can retain an identity-broker setup using section 8.2 and Moodle's OAuth 2 services; that is a separate integration from this plugin. See Moodle authentication configuration.
8.10 Verify your integration
Use staging and a new non-privileged account first. Complete a Wallet login, the CMS callback and code exchange, then verify that a second login returns to the same account. If email is requested, verify that a changed contact address does not change the account's identity binding. Also check cancellation, expired transactions and replayed callbacks: none should create a logged-in session.
The CMS must validate the signed ID Token and match UserInfo sub to its subject.
Keep tokens, codes, verifiers and identity values out of diagnostic logs.
Successful Discovery or a plugin's connection test alone does not validate login.
Record the CMS/module versions and completed tests for your deployment.
9 Errors and limits
Some products require more than Authorization Code login. For example, Shopify's custom identity-provider requirements include RP-Initiated Logout, which the bridge does not implement. Such a product needs an adapter or additional service capabilities before direct integration. The bridge does not supply refresh tokens, SAML, SCIM, groups or roles.
| Error | Action |
|---|---|
| Local HTTP 400 before redirect | Fix client ID, exact callback or duplicated parameters |
invalid_request |
Check S256 challenge/verifier, response mode and supported parameters |
invalid_scope |
Request only registered scopes, including openid |
login_required |
Restart with interactive login rather than prompt=none |
access_denied |
User declined; do not silently restart indefinitely |
invalid_client |
Check authentication method, secret and client enabled state |
invalid_grant |
Expired/replayed code, wrong callback or PKCE mismatch; start again |
| HTTP 429 | Respect Retry-After and back off |
| HTTP 503 | Service unavailable; back off, then start a new login |
Never log codes, tokens, verifiers, callback query strings or identity credential claims. Client revocation prevents further authorize/code/token/UserInfo use. It does not instantly invalidate an ID Token already received by your application or close your application's existing session. Plan your own session expiry and recovery.
10 Diagnostic downloads
For support, the original browser can access /login/<transaction>/trace; the
hosted login does not display a technical diagnostic-download link. It exports the
bridge stages, verification service HTTP/verification outcomes and attribute presence/types rather
than identity values. The sandbox test client offers its own trace on success or
failure, covering Discovery, callback state, code exchange, ID Token validation
and UserInfo. Both contain the same hashed-state correlation when the request
has state, allowing support to match the two files without sharing raw state.
Download from the browser that initiated the login. Traces are encrypted at rest, expire after ten minutes and require the owning browser cookie. They never contain tokens, authorization codes, nonce, PKCE verifier, credentials, identity credential values, API keys or signing keys. A bridge trace also carries the verification service presentation ID for an authorized operator to find the corresponding verification service trace. Exported files are technical diagnostics, not proof of successful authentication. The generic bridge cannot capture errors internal to an arbitrary relying party; that application must supply its own diagnostics. The included sandbox implements this independent relying-party trace as a reference.
11 France Identité compatibility
The official France Identité Partenaires overview
describes a partner test wallet with mdoc identity/age credentials and OID4VP 1.0
online presentation on Android and iOS, including same-device and cross-device
flows. It also documents support for the European av:// age-verification
blueprint on Android 1.3.5078 or later, while marking that capability as in
progress on iOS. These documented formats and flows explain the protocol basis
of the bridge's integration. We have successfully tested identity verification with
x509_hash in our environment, as detailed below; this is not a
certification of every Wallet release or deployment.
Use the official OID4VP guide and age verification guide for Wallet setup, supported channels and release limitations. The Partenaires build is distributed to testers through Firebase App Distribution / TestFlight; these capabilities must not be assumed for every consumer app release.
11.1 Attestations and requested attributes
This section describes the Wallet-facing exchange for protocol specialists. Applications integrating WordPress, Keycloak or another OIDC client still use Discovery and the OIDC endpoints above. The bridge obtains the Wallet request from its verification service and displays it as a QR code or deep link; your application does not build that request.
| Parameter | Identity verification — France Identité compatibility | Age verification — 18+ |
|---|---|---|
| Attestation | Personal Identification Data (PID) mdoc | Proof of Age mdoc, separate from the PID |
| OID4VP credential format | mso_mdoc |
mso_mdoc |
Document type (doctype_value) |
eu.europa.ec.eudi.pid.1 |
eu.europa.ec.av.1 |
| Namespace | eu.europa.ec.eudi.pid.1 |
eu.europa.ec.av.1 |
| DCQL credential query ID | pid_mdoc |
proof_of_age |
| Requested attributes | family_name, given_name, birth_date, birth_place |
age_over_18, required to be boolean true |
| Attribute representation | Names and birth_place are text; birth_date is a date string |
Boolean age threshold; no name or birth date |
| Retention intent in each claim request | intent_to_retain: false |
intent_to_retain: false |
| Cryptographic holder binding | Required | Required |
The identity query uses the explicitly selected France Identité compatibility
policy. Its text birth_place is distinct from the current EU PID Rulebook's
structured place_of_birth; the two encodings are not silently substituted.
Neither query requests nationality, portrait or email. Optional OIDC contact
email is collected and verified separately by the bridge after identity verification.
11.2 Identity request: OID4VP 1.0 and HAIP 1.0
Our reference is the successful France Identité identity test — HAIP #3.
We compared its generated request with the example below: the credential query,
x509_hash identifier, encrypted response mode and client metadata agree.
Session URLs, nonces, timestamps, certificate hash and ephemeral keys vary per request.
The test and bridge use distinct verifier instances; their transaction URLs differ.
The configured identity verifier uses
OID4VP 1.0 Final
with the redirect flow of
HAIP 1.0 Final, section 5.1:
a signed Request Object retrieved through request_uri, x509_hash verifier
identification, and an encrypted direct_post.jwt response. Public response
encryption keys are ephemeral and advertised in client_metadata.jwks.
No separate relying-party registration certificate is included in this
compatibility request. This does not mean that the request is certificate-free:
the signing certificate chain is carried in the JWS x5c header, and the
x509_hash client identifier binds to the leaf certificate. An OIDC application's
registration and Client ID are a separate, mandatory step.
Our tested configuration: we have successfully tested France Identité
identity verification with x509_hash, OID4VP 1.0 and the HAIP 1.0 redirect flow,
without a separate relying-party registration certificate. This result applies
to our tested environment. A change of Wallet build, trust environment or
verifier configuration requires a new integration test.
The following is an illustrative request shape from the configured generator,
not a captured France Identité transaction. Angle-bracket values are placeholders;
handles, nonces, certificate hashes and keys are generated afresh per transaction.
The configured invocation scheme is haip-vp://; a different invocation scheme
requires explicit compatibility with the target Wallet.
haip-vp://?client_id=x509_hash%3A<CERTIFICATE_SHA256_BASE64URL>&request_uri=https%3A%2F%2Fverifier.example%2Fverifier%2Fidentity%2Frequest%2F<REQUEST_HANDLE>&request_uri_method=post
The Wallet retrieves the signed JWT with an HTTPS POST to request_uri, using
Content-Type: application/x-www-form-urlencoded and
Accept: application/oauth-authz-req+jwt. If supplied, its wallet_nonce is
included in the signed payload. Relevant decoded payload fields are shown below;
Only JWT timing/ID fields and optional wallet_nonce are omitted for readability.
client_metadata includes the configured format capabilities, the ephemeral
P-256 public response-encryption key and the supported content-encryption
algorithms. The example public key was generated solely for this documentation;
its private counterpart is not retained. A real transaction uses its own fresh
key pair; only the public JWK is sent to the Wallet. The -7 COSE identifier
means ES256 for mdoc issuer and device authentication. Advertising both supported
formats does not change the DCQL request: this transaction asks only for an mdoc.
{
"iss": "x509_hash:<CERTIFICATE_SHA256_BASE64URL>",
"aud": "https://self-issued.me/v2",
"client_id": "x509_hash:<CERTIFICATE_SHA256_BASE64URL>",
"response_type": "vp_token",
"response_mode": "direct_post.jwt",
"response_uri": "https://verifier.example/verifier/identity/response/<RESPONSE_HANDLE>",
"nonce": "<FRESH_NONCE>",
"dcql_query": {
"credentials": [
{
"id": "pid_mdoc",
"format": "mso_mdoc",
"meta": {
"doctype_value": "eu.europa.ec.eudi.pid.1"
},
"claims": [
{
"id": "family_name",
"path": [
"eu.europa.ec.eudi.pid.1",
"family_name"
],
"intent_to_retain": false
},
{
"id": "given_name",
"path": [
"eu.europa.ec.eudi.pid.1",
"given_name"
],
"intent_to_retain": false
},
{
"id": "birth_date",
"path": [
"eu.europa.ec.eudi.pid.1",
"birth_date"
],
"intent_to_retain": false
},
{
"id": "birth_place",
"path": [
"eu.europa.ec.eudi.pid.1",
"birth_place"
],
"intent_to_retain": false
}
],
"require_cryptographic_holder_binding": true
}
]
},
"client_metadata": {
"vp_formats_supported": {
"dc+sd-jwt": {
"sd-jwt_alg_values": [
"ES256"
],
"kb-jwt_alg_values": [
"ES256"
]
},
"mso_mdoc": {
"issuerauth_alg_values": [
-7
],
"deviceauth_alg_values": [
-7
]
}
},
"jwks": {
"keys": [
{
"crv": "P-256",
"kty": "EC",
"x": "917gXN1LScPlIm4Jr7yA9sjnAjTcC6fnLaNxoUQT97Y",
"y": "e8AYcCurA_shZKJOA7ZgcjbCALGmPiMcVphVjYkX3Ug",
"kid": "example-ephemeral-response-key",
"use": "enc",
"alg": "ECDH-ES"
}
]
},
"encrypted_response_enc_values_supported": [
"A128GCM",
"A256GCM"
]
}
}
The Wallet returns an encrypted response JWT to response_uri. The verification
service decrypts it, verifies the credential and holder proof, and binds the result
to this transaction. Only a successful trusted result can lead to an OIDC code.
The JWS signing chain, issuer trust anchors, and response-encryption key have
separate roles; none replaces the others.
11.3 Age request: European Age Verification Blueprint
Our reference request is Age verification — CORE #4.
We checked its generated request: it uses the av:// unsigned URL flow below,
with proof_of_age and only age_over_18, without client_metadata.
This request comparison does not constitute a new completed Wallet presentation.
The age flow follows the mdoc OID4VP fallback in the European Commission's
Age Verification Blueprint — Annex A, AV Profile.
It uses av://, unsigned URL query parameters, a redirect_uri: client identifier,
vp_token, DCQL and direct_post. There is no request or request_uri in this
flow. It is the dedicated CORE age path, separate from the HAIP identity path.
The current blueprint also specifies Digital Credentials API / zero-knowledge presentation mechanisms. This bridge path implements the mdoc OID4VP fallback; it does not implement those mechanisms or establish full blueprint conformity. See also the European Commission overview and the France Identité age integration guide.
The reference test sends the following DCQL query. The dedicated bridge verifier
uses the same selection and additionally makes
require_cryptographic_holder_binding: true explicit; this is also the OID4VP
default when the property is omitted. Both require a valid holder proof:
{
"credentials": [
{
"id": "proof_of_age",
"format": "mso_mdoc",
"meta": {
"doctype_value": "eu.europa.ec.av.1"
},
"claims": [
{
"path": [
"eu.europa.ec.av.1",
"age_over_18"
],
"intent_to_retain": false
}
]
}
]
}
This illustrative Python example produces the URL-encoded invocation with that query. Its example endpoint and nonce are placeholders, not a live transaction:
import json
from urllib.parse import urlencode
response_uri = "https://verifier.example/verifier/age/response/EXAMPLE_HANDLE"
dcql_query = {
"credentials": [{
"id": "proof_of_age",
"format": "mso_mdoc",
"meta": {"doctype_value": "eu.europa.ec.av.1"},
"claims": [{
"path": ["eu.europa.ec.av.1", "age_over_18"],
"intent_to_retain": False,
}],
}],
}
invocation = "av://?" + urlencode({
"client_id": "redirect_uri:" + response_uri,
"response_type": "vp_token",
"response_mode": "direct_post",
"response_uri": response_uri,
"nonce": "EXAMPLE_FRESH_NONCE",
"dcql_query": json.dumps(dcql_query, separators=(",", ":")),
})
print(invocation)
Like CORE #4, this example has no client_metadata. The dedicated bridge
verifier may advertise supported credential formats when configured to include
metadata; this does not add encryption to direct_post. It has no response-encryption
JWK or encrypted-response algorithm list. The Wallet posts vp_token to response_uri. Although the request is
unsigned and the response uses direct_post, the attestation and device proof
remain cryptographically verified: issuer trust, signature, digests, validity,
holder binding and transaction transcript must all pass. Only boolean true is
accepted for age_over_18; identity attributes are not requested or released.
11.4 OIDC attribute mapping and subject continuity
The service can use the France Identité compatibility profile for an identity
client. Your application still uses the same OIDC endpoints and validation rules. This policy handles the observed mdoc response using DCQL query
pid_mdoc and namespace attribute birth_place as text, separately from the
current Rulebook policy. It requires a completed, transaction-bound verification service result,
established issuer trust and strict verification service verification. A signer subject name
alone is never sufficient; no bridge certificate allowlist is introduced.
For this policy only, the outgoing bridge-specific place_of_birth claim is a
string when authorized under profile. For example, a fictitious value could
be "DEMO CITY". It is not converted into locality, country or region.
Clients consuming the claim must handle a string for this explicitly selected
policy or an object for the Rulebook policy. The ID Token and UserInfo use the
same value and type. Clients not authorized for birthplace do not receive it.
Our successful France Identité identity test applies to our tested environment. It does not establish a separate age-verification test result or validate another deployment.
11.5 How we derive the subject identifier (sub)
We resolve the identity only after the Wallet presentation has passed signature, issuer trust, holder-binding and transaction checks. For the France Identité policy, the input consists of four verified attributes:
| Credential attribute | Internal canonical field | Treatment |
|---|---|---|
given_name |
given_name |
Trim leading/trailing whitespace, then Unicode NFC |
family_name |
family_name |
Trim leading/trailing whitespace, then Unicode NFC |
birth_date |
birthdate |
Valid calendar date in YYYY-MM-DD form |
birth_place |
place_of_birth |
Opaque text, trimmed and normalized with Unicode NFC |
We do not case-fold names, remove accents, collapse internal whitespace or infer geographical components from birthplace text. Nationality, email, portrait, credential number and signing-certificate fingerprint do not participate.
There are two separate steps:
- Resolve an internal subject. We serialize the four canonical fields inside
an envelope containing
version: 2and the configured identitydomain. Serialization uses UTF-8 JSON with sorted keys, compact separators and literal Unicode characters (ensure_ascii=False). We calculate a keyed HMAC-SHA256 with our private identity key. This digest is an internal lookup alias; it identifies a persistent, randomly generated subject record. - Resolve the application's
sub. For that subject and the application's sector, we retrieve or create a separate cryptographically random identifier, persist it and return it as the OIDCsub. We do not return the HMAC digest. The sector is currently the hostname of the registered callback URLs, rather than the Client ID. Clients on the same callback hostname can therefore receive the samesubfor the same identity; different sectors receive different subs.
Conceptually, the implementation follows:
canonical = {given_name, family_name, birthdate, place_of_birth}
message = canonical_json_utf8({version: 2, domain: identity_domain, identity: canonical})
lookup_alias = HMAC_SHA256(private_identity_key, message)
internal_subject = persistent_lookup_or_create(lookup_alias)
sub = persistent_random_identifier(internal_subject, callback_hostname)
The application receives only the sector identifier sub, together with the
issuer iss; it must store (iss, sub) as its external account key. Our
persistent identity registry stores lookup digests, random subject identifiers
and sector associations, not the original four identity values or full PID.
The HMAC key stays private. The HMAC is pseudonymization, not proof of anonymity;
requested and authorized profile claims can still disclose identity to the application.
Limits and account continuity:
- Two people with exactly the same four canonical values in the same identity
domain resolve to the same internal subject, and to the same
subwithin a sector. This is a collision in the identifying attributes, not a required break of HMAC-SHA256. We do not guarantee a unique legal-person identifier. - A corrected name, changed spelling, capitalization or birthplace representation
can produce another lookup alias and another
sub. Renewal of the credential preserves continuity only if the canonical inputs, domain, sector and registry remain compatible. A renewed signing certificate alone is not an identity input. - France Identité's textual
birth_placeand the EU Rulebook's structuredplace_of_birthare distinct inputs. Changing policy between these encodings can change the subject; we do not automatically equate them. - Changing the callback hostname changes the sector. A new Client ID on the same
hostname does not, by itself, create a new sector. Changing the OIDC issuer also
changes the application's
(iss, sub)account key. - Changing the identity HMAC key, identity domain or serialization version requires
an explicit continuity strategy. Losing the persisted alias/subject/sector
registry prevents reconstruction of the original random
subfrom the PID alone. Retaining historical aliases does not automatically reconcile new inputs. - If a
subchanges, the application must authenticate the existing local account and require explicit authorization before linking the new(iss, sub). Never merge or recover accounts automatically by name, birth date or email.
This derivation applies to identity verification. For age verification,
we request no identity attributes and do not use this identity registry: the age
result carries a transaction-specific, ephemeral sub, unsuitable for persistent
user-account matching.
12 Privacy and data retention
We request the minimum attributes for the selected purpose: four verified
identity attributes for identity login, or only age_over_18 for age verification.
This section states the current technical storage behaviour. Our published
Privacy Policy and
Terms remain the reference notices. Encryption and
pseudonymization do not make personal data anonymous.
12.1 What the bridge retains
| Data | Storage and purpose | Retention / deletion |
|---|---|---|
Identity lookup aliases, internal random subject IDs, sector-specific sub associations |
Persistent bridge database, for account continuity; no raw name, birth date or birthplace in this registry | No automatic expiry or self-service identity deletion is implemented. Revoking or archiving a client does not erase this registry. |
| Application registration | Client ID, public name, exact callbacks, scopes/claims, policy, sector, status/revision, creation/archive dates and free-client registration record | Persistent; archiving disables access and reserves the Client ID rather than deleting the registration row. No automatic retention deadline. |
| Client secret | Only a password hash is stored in the database; the plaintext is returned once in the successful registration/rotation POST response | The secret screen clears after five minutes or Close. This does not revoke the client. Rotation replaces the hash; archiving clears it. The developer must protect any saved copy. |
| Developer contact | Company, first/last name, verified email, verification date, acknowledgement date and policy URLs, encrypted in bridge storage; accessible to authorized client administrators | Kept with the client, without automatic expiry; the contact row is removed when the client is archived. Separate from an end user's identity/email. |
| Administrative audit | Actor reference, client ID, action, revision and timestamp | Persistent; no automatic purge is implemented. It contains neither client secrets nor Wallet identity values. |
| Optional verified end-user contact email | Encrypted email bound to the pairwise sub, plus verified_at; only for authorized identity clients requesting scope email |
Reusable for 30 days from verification, without extending that date on reuse. Expired data is unusable; physical deletion requires the installed daily purge timer. Controlled deletion can remove the email without changing sub. |
The bridge does not persist a full PID or raw identity attributes in its identity registry. It does temporarily process authorized claims for the OIDC exchange. This distinction also applies to an age result: no persistent identity subject is created for it, but its short-lived transaction and tokens still exist.
12.2 Temporary browser and protocol state
Protocol records are encrypted in the bridge's dedicated Redis and have TTLs. Counters and concurrency leases also have expirations, but are not identity records. A browser cookie binds the browser to its own transactions; the signup secret is never placed in that cookie.
| Temporary data | Current lifetime |
|---|---|
| Wallet transaction and authorized claims | Up to 300 seconds for identity-only or age verification; the optional email step can add up to 600 seconds after identity verification |
| OIDC authorization code and associated claims | 60 seconds, single-use |
| Access-token state for UserInfo | 300 seconds; no refresh token |
| Nonce replay marker | 600 seconds |
| Browser-owned bridge diagnostic download | 600 seconds from the last recorded diagnostic event |
| Pending developer registration, including contact fields | Up to 30 minutes; consumed on successful registration or cancelled through the form |
| Email verification challenge | Code valid five minutes, at most five attempts; only a keyed digest is stored in the encrypted challenge, with at least 60 seconds between sends |
| Rate-limit counters | Depending on the endpoint: 60 seconds, 15 minutes, one hour or 24 hours from the first increment; daily quota buckets change at midnight UTC |
| Concurrency bookkeeping | Redis keys expire after at most 900 seconds from their last update; expired transaction leases no longer count as active |
Expiry prevents further protocol use; it does not invalidate copies already received by a client or erase storage backups. Closing the secret screen or a browser tab does not itself revoke a client or erase all transaction state. The application receives identity claims only within its authorized scopes and claims, and owns its session, account and token-retention policy. Do not store ID Tokens indefinitely or use email as an automatic account-recovery key.
12.3 Verification service, diagnostics and infrastructure
The bridge's verification service has separate storage. The current verifier
implementation persists normalized presentation results, including disclosed
claims and verification outcomes, in its database. Transaction expiry does not
purge those results, and no automatic result-purge deadline is implemented here.
This includes the four disclosed identity fields, or the disclosed age result
for an age presentation. Consequently, bridge Redis TTLs and
intent_to_retain: false must not be read as a guarantee that every component
automatically erases the presentation after five minutes. A retention/deletion
policy for this persistent verification history must be defined and applied to
the deployed service before making a broader deletion guarantee.
The downloadable bridge diagnostics contain stages, statuses, attribute presence/types and technical correlation references, never identity values, email addresses, codes, tokens, nonce, PKCE verifier, cookies or secrets. Verification-service traces are separate and follow their configured retention and redaction settings; they are not covered by the bridge's ten-minute trace TTL.
Web-server access logs, SMTP delivery records and database/Redis backups are additional infrastructure records. Their retention is configured separately; this implementation does not set a universal deletion deadline for them. IP addresses are used for abuse controls, with hashed Redis key identifiers. Do not enable proxy logging of callback query strings, POST bodies, tokens or secrets. SMTP providers process the recipient address and verification message; that is separate from a Wallet identity attestation.
For an account or data-deletion request, use the contact details in our published Privacy Policy. There is currently no public API for deleting an identity or its verification history. Client revocation, contact-email deletion and deletion of an application's local user account are different operations; none alone promises deletion across all services and backups.
12.4 Hosting and security
We host the public bridge on AWS EC2 in the Europe (Paris) region,
eu-west-3, in France, within the European Union. AWS identifies this region
in its official region list.
The public endpoint is https://login.openid4vc-hub.com.
Our deployment uses the following protections:
- HTTPS access: TLS protects exchanges between applications, browsers and the public bridge endpoint. The application process is reached through a reverse proxy; its listening port is bound to the local interface.
- Separate bridge storage: the bridge has its own PostgreSQL database and dedicated or access-restricted Redis storage. Its database credentials and private configuration are separate from those of the verification service.
- Protected application data: transient protocol payloads are encrypted before Redis storage. Developer contacts and optional verified end-user email are encrypted before database storage. Client secrets are stored as password hashes; the identity registry uses keyed HMAC aliases and random identifiers.
- Restricted administration: client-management operations require private loopback access and dedicated authentication. The public reverse proxy blocks the internal administration paths. The runtime uses a dedicated service account; signing, encryption and SMTP credentials remain server-side.
This location statement covers our public bridge hosting. SMTP delivery, application-side data, logs and backup copies have their own processing and retention arrangements, described above; their location is not established by the bridge's AWS region alone. Hosting in Europe does not change which identity attributes are requested, the application's authorized claims or the retention limits described in this guide.