Nexiel Login quickstart

Nexiel Login is Sign in with EUDI Wallet, packaged as a standard OpenID Connect identity provider. Your own Keycloak, Auth0, or Okta broker adds Nexiel as one more login connection, the same motion as adding Google or Microsoft. It never speaks OpenID4VP or DCQL itself. Nexiel runs the wallet exchange internally, using the same Verify API engine every other Nexiel Verify check already runs, and hands your broker back an ordinary signed id_token.

Production status.Sandbox mode is fully real and usable today, including a genuine repeat-login round trip and the returning-login shortcut described below. Production is not available yet, for two real, external reasons, neither of them a code gap: registering "authentication / account login for third-party services" as a new Relying Party intended use under Commission Implementing Regulation (EU) 2025/848 has not been filed yet, and, independent of that filing, PID-based claim disclosure already fails closed in production across every Nexiel checkType that needs it, because the EU's own eIDAS PID-Provider trust infrastructure is not live yet. Nexiel Login's wallet leg requests a PID claim, so it inherits that same gate. See who registers as the relying party for the second point in full. Neither gate is specific to Nexiel; nothing in the EU can complete a production PID-based wallet check today.

New to EUDI wallets? Read what an EU Digital Identity Wallet is, and how a session works before this page assumes you already know what OpenID4VP, DCQL, and SD-JWT VC mean.

What this is, and what this isn't

This is not a new identity-verification engine. It is Nexiel Verify's existing OpenID4VP/DCQL engine, wearing a different front door: a dedicated login-authentication check that requests only the PID's birthdateclaim, the smallest non-empty claim set Nexiel's template validator allows, and never a name. The point of this check is proving a real wallet answered, not learning who is behind it. Recognizing the same person on a later login does not come from that claim either: it comes from the wallet presentation's own holder key-binding public key, a value that is cryptographically verified on every completed check and is not personal data at all.

This is also not the drop-in QR widget you may already use elsewhere on your site for a one-off Nexiel Age or Nexiel Verify AML identity check. That widget has no reason to remember a returning visitor. A login flow does, so Nexiel Login is a hosted redirect page instead, matching how Auth0's own Universal Login and Keycloak's own /realms/{realm}/protocol/openid-connect/auth both work: your broker redirects the browser to Nexiel, Nexiel redirects it back once the wallet check completes.

A secondary, embeddable "Sign in with EUDI Wallet" button for sites with no OIDC broker of their own is a real, deliberately deferred future mode, not something silently dropped: it needs the same returning-login recognition described below, which packages/verify-widget's existing one-off QR widget has no reason to carry today. It is not built in this release. Every customer today integrates Nexiel Login as the hosted redirect above, through an OIDC broker.

Prerequisite: KYB approval and a registered connection

Accepting inbound OAuth trust from your broker is production API access, so it sits behind the same KYB gate every other Nexiel production credential does. Unlike Screen and Verify API keys, a Nexiel Login connection is not yet self-serve from the dashboard: once your KYB application is approved, contact Nexiel onboarding with a name for the connection and the exact https:// redirect URI(s) your broker will use (an exact-match allowlist, never a wildcard). You receive a client_id and client_secret pair, shown once, the same Stripe-style delivery every other Nexiel credential uses. A self-serve dashboard page for this is planned, matching the existing API Keys page for Screen and Verify.

The discovery document

Every endpoint below is also published as a standard OpenID Connect Discovery 1.0 document, the one input most identity providers, including Keycloak, need to configure everything else automatically:

curl -s https://compliance.nexiel.io/v1/login/.well-known/openid-configuration
{
  "issuer": "https://compliance.nexiel.io/v1/login",
  "authorization_endpoint": "https://compliance.nexiel.io/v1/login/authorize",
  "token_endpoint": "https://compliance.nexiel.io/v1/login/token",
  "jwks_uri": "https://compliance.nexiel.io/v1/login/.well-known/jwks.json",
  "response_types_supported": [
    "code"
  ],
  "subject_types_supported": [
    "pairwise"
  ],
  "id_token_signing_alg_values_supported": [
    "ES256"
  ],
  "scopes_supported": [
    "openid"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post"
  ],
  "code_challenge_methods_supported": [
    "S256"
  ],
  "claims_supported": [
    "sub"
  ],
  "grant_types_supported": [
    "authorization_code"
  ]
}

The issuer is https://compliance.nexiel.io/v1/login, not the bare compliance.nexiel.io origin: OpenID Connect Discovery 1.0 forms the discovery URL by appending /.well-known/openid-configurationto the full issuer URL, so this path is the spec-correct location for an issuer with its own path segment, fully namespaced under Login's own routes alongside Verify API's and Credentials API's own well-known documents on the same shared host. There is deliberately no userinfo_endpoint: the id_token below already carries everything this deployment issues, so there is nothing a UserInfo call would ever return beyond it.

Adding Nexiel as an OpenID Connect identity provider in Keycloak

This is the whole integration on your side. Keycloak never learns about OpenID4VP, DCQL, or any wallet-specific concept; it only ever sees Nexiel as one more OIDC identity provider, the same way it already treats Google or Microsoft as one:

  1. In the Keycloak admin console, go to Identity Providers Add providerOpenID Connect v1.0. Give it an alias, for example nexiel-login. Keycloak generates a redirect URI from that alias, shown on the same form (it looks like https://auth.example.com/realms/{realm}/broker/nexiel-login/endpoint), and this is the exact value that must be added to your connection's registered redirect URI allowlist.
  2. Populate the endpoint fields. The simplest way: toggle Use discovery endpoint on and paste https://compliance.nexiel.io/v1/login/.well-known/openid-configuration from the previous section; Keycloak fetches it and fills in the rest. If your identity provider does not support discovery, enter each field by hand from that same document: Authorization URL, Token URL, and JWKS URL as shown above, and set Issuer to https://compliance.nexiel.io/v1/login exactly, since a signed id_token is rejected by a strict verifier if its iss claim does not match.
  3. Set Client Authenticationto "Client secret sent as basic auth" (client_secret_basic), and enter the client_id/client_secret pair from the previous section.
  4. Enable Use PKCE, method S256. Nexiel Login's /authorize endpoint rejects any request that omits a code_challenge, or that sets code_challenge_method to anything other than S256.
  5. Leave the default scope as openid. Nexiel Login's id_token carries no profile claims beyond sub, so there is nothing a wider scope would add.
  6. Keycloak's stock "First Broker Login" flow keys off the federated identity's email address by default, and Nexiel Login's id_token has no email claim at all, by design. Add an IdP Mapper of type "Username Template Importer" on this provider to synthesize a local username from the federated alias and sub (for example login-{ALIAS}-{CLAIM.sub}), or bind a narrower first-broker-login flow that skips the email-required review step, before you rely on this in a real realm.

Any other broker that speaks plain OpenID Connect, Auth0 and Okta included, follows the same shape: add Nexiel as a connection, point it at the discovery document, and configure S256 PKCE with client_secret_basic authentication.

What happens when someone signs in

Once configured, your broker redirects the browser to GET /v1/login/authorize with the standard OAuth 2.0 Authorization Code parameters: response_type=code, your client_id, a registered redirect_uri, scope=openid, a state, a nonce, and a PKCE code_challenge/code_challenge_method=S256pair your broker generated. This route returns human-facing HTML directly, matching Keycloak's own authorization endpoint shape, never a JSON API: a QR code built from a genuine wallet authorization request. The page's own script polls an unguessable, unauthenticated status route (GET /v1/login/authorize/{authorizationRequestId}/status, the same "the unguessable id is the real authorization" pattern Nexiel Verify's own wallet callback route already relies on) and, once the wallet completes, navigates the browser straight back to your redirect_uri with a real, single-use code and the original state attached.

An unresolvable client_id or a redirect_uri outside your registered allowlist renders a static error page rather than redirecting anywhere, the standard defense against an open redirect. Every other validation failure, an unsupported response_type, a missing PKCE challenge, and so on, redirects back to your (already-trusted) redirect_uri with ?error=...&error_description=...&state=... per RFC 6749, the same shape every OIDC broker already knows how to parse.

Token exchange

Your broker calls this server-to-server, immediately after the redirect, with the code_verifier matching the code_challenge it sent at /authorize:

curl -s -X POST https://compliance.nexiel.io/v1/login/token \
  -u "<client_id>:<client_secret>" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d code=<code> \
  -d redirect_uri=<redirect_uri> \
  -d code_verifier=<code_verifier>
200 response
{
  "access_token": "C6mE1z3q0J8v...",
  "token_type": "Bearer",
  "expires_in": 600,
  "id_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjIwMjYtMDgtcHJpbWFyeSJ9..."
}

client_secret_post works too: send client_id and client_secret as body fields instead of the Authorization header. The code is single-use and expires in 60 seconds; a second exchange attempt, or one after that window, returns invalid_grant. access_token is a bearer opaque to Nexiel Login itself; there is no /userinfo route it can be used against, since nothing in this deployment returns any claim beyond what the id_token already carries.

The id_token

A real, compact-serialized ES256 JWT, verifiable against GET /v1/login/.well-known/jwks.json. Its decoded payload:

{
  "iss": "https://compliance.nexiel.io/v1/login",
  "sub": "1b2f9e2f-8f42-4b8a-9b1e-1a11a2b3c4d5",
  "aud": "5c9b8e2f-3f9a-4b7a-9c33-6f7e8d9c0b1a",
  "iat": 1755000000,
  "exp": 1755000600,
  "nonce": "n-0S6_WzA2Mj"
}

sub is a pairwise identifier, stable across repeat logins for the same (connection, wallet holder) pair and different for every other connection, so one wallet holder cannot be trivially correlated across two different Nexiel Login customers by comparing raw subvalues. It is derived from the wallet presentation's own holder key-binding public key, never from a disclosed claim: there is no name, birthdate, or nationality anywhere in this token, or in Nexiel's own record of the login, by construction. One honestly disclosed limitation: the EUDI wallet ecosystem's own credential-batch-issuance model exists specifically to prevent this kind of correlation, so a privacy-conscious wallet may present a different key on a later presentation of the same underlying identity. If that happens, Nexiel Login does not fail; it simply does not recognize the return and falls back to a fresh first-login round trip. aud is your client_id. jwks.json publishes every currently-active and recently-rotated key, so a token signed just before a rotation stays verifiable through the overlap window.

Returning logins: skipping the wallet round trip

The first time someone completes a real wallet check for your connection, Nexiel sets an HttpOnly, SameSite=Lax session cookie scoped to /v1/login, valid for 12 hours from that check. The next time the same browser is sent to GET /v1/login/authorize for the same connection, and the cookie is still valid, Nexiel Login recognizes it, mints a fresh authorization code immediately, and responds with a redirect straight back to your redirect_uri, no QR page, no wallet interaction at all. Every other OAuth 2.0 requirement in the request, PKCE, state, nonce, is still fully enforced regardless; only the wallet leg is ever skipped. A cookie that does not recognize a returning login (missing, expired, or issued for a different connection) is treated exactly like no cookie at all, falling through to a real round trip. There is no logout or session-revocation endpoint yet; this is a genuine, disclosed gap, not a silent one.

This is a deliberate, near-term stand-in for the EU wallet ecosystem's own forthcoming pseudonym mechanism (a WebAuthn credential the wallet itself would act as authenticator for), which is still unfinalized upstream. Nexiel-issued recognition today, migration once that mechanism ships, without changing what your broker integrates against.

Data handling: what gets persisted

Calling the check directly

login-authentication is also a normal Verify API checkType, callable via POST /v1/verify/sessions exactly like age-verification or aml-identity, for a case where you want to drive the wallet round trip yourself rather than through the hosted OIDC bridge above. Its decision carries only booleanProofs: { claimsVerified: true }: no AML or PEP screening ever runs for this checkType, and no evidence pack is generated for it, since there is no identity claim in the decision worth encrypting a copy of.

Reference

Every Nexiel Login route above is documented under the Login tag on the Verify API reference (generated directly from the same live OpenAPI specification as the rest of Verify API), alongside the login-authenticationcheckType's own request/response shape.