OpenID Connect · OIDC4VP · Developer integration

Bridge OIDC - EUDI wallet

Connect an EUDI wallet with OpenID Connect.

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.

  1. 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.
  2. 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.
  3. 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.
  4. Confirm that you have read the linked terms of use and privacy policy, then request the email code.
  5. 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.
  6. After successful email verification, the service creates the client and generates its client_id and 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:

  1. state to bind the callback to the initiating browser transaction;
  2. nonce to bind the ID Token to that authentication request;
  3. code_verifier, a high-entropy string of 43–128 characters from A–Z a–z 0–9 - . _ ~;
  4. 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):

  1. Copy your return URL with Copy URL.
  2. Select Register my site to open the registration form in a new tab. Paste that exact URL into the return URL (redirect_uri) field.
  3. Complete the form and verify your developer email using the received code.
  4. On the confirmation page, select Download configuration (JSON). Keep the file confidential: it contains the client secret.
  5. 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

  1. 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.
  2. 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.
  3. Before configuring the connection, open free client registration. Select Identity verification and register that exact callback. Authorize openid profile email and 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.

  1. 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.
  2. Select Sign in with your EUDI Wallet (Se connecter avec mon Wallet EUDI).
  3. Present the requested identity in the Wallet and authorize the presentation.
  4. 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.
  5. 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.

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.

  1. 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.
  2. Open Preferences → Link my EUDI Wallet (Préférences → Lier mon Wallet EUDI). The linking page is /auth/eudiwallet/link.php on your Moodle installation, for example https://moodle.example/auth/eudiwallet/link.php.
  3. 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.
  4. Present your identity with the Wallet. Complete the bridge's separate contact email verification if requested, then return in the same signed-in browser.
  5. Moodle displays Your Wallet is linked to this Moodle account (Votre Wallet est lié à ce compte Moodle) and returns to Preferences.
  6. 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:

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:

  1. Resolve an internal subject. We serialize the four canonical fields inside an envelope containing version: 2 and the configured identity domain. 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.
  2. 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 OIDC sub. 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 same sub for 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:

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:

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.