Developers · OpenID Connect · EU Wallet

Add the EU Wallet to your sign-in: quickstart

To your application, the CodeB Relying Party Gateway is a standard OpenID Connect provider. You add one parameter and check one claim – the wallet protocols stay out of your code.

The two rules: 1. Send acr_values=urn:codeb:acr:eudi-wallet in the authorization request. 2. Accept only a validated id_token whose acr is urn:codeb:acr:eudi-wallet or urn:codeb:acr:eudi-wallet-linked.

Authorization code + PKCE (S256) · acr / amr · RS256 id_token · JWKS · RP-initiated logout

Closed beta, in testing. Not certified.

At a glance

Issuer
https://phone.aloaha.com (discovery at /.well-known/openid-configuration)
Flow
Authorization code with PKCE (S256), state, nonce
Force the wallet
acr_values=urn:codeb:acr:eudi-wallet (eID card: urn:codeb:acr:eidcard)
Check
acr in the id_token; amr contains vc (wallet) or sc + pop (eID card)
Scopes
openid, profile, email (also phone, address)
Client authentication
client_secret_basic, client_secret_post, or a public client with PKCE

Four steps

Get a client

Tell us your redirect URI(s), for example https://your-app.example/callback. You get a client_id and a client_secret; your relying party details go into the registration certificate.

Redirect with the wallet acr

Start an authorization code flow with PKCE (S256), state and nonce, and add acr_values=urn:codeb:acr:eudi-wallet.

Exchange the code

On your server, swap the code for tokens at the token endpoint and validate the id_token: signature, issuer, audience, expiry, nonce.

Check acr

Accept the sign-in only if acr is urn:codeb:acr:eudi-wallet or urn:codeb:acr:eudi-wallet-linked. Done.

Code samples

Compact samples; complete, tested apps for six stacks (including Java and Go) are in the integration cookbook. Replace YOUR_CLIENT_ID, your-app.example and so on with your values.

Discovery

All endpoints and keys come from one document; every OpenID Connect library reads it.

shell
# Every endpoint comes from the discovery document.
curl -s https://phone.aloaha.com/.well-known/openid-configuration

# The parts you need:
#   authorization_endpoint   https://phone.aloaha.com/oauth2/v1/authorize
#   token_endpoint           https://phone.aloaha.com/oauth2/v1/token
#   userinfo_endpoint        https://phone.aloaha.com/oauth2/v1/userinfo
#   end_session_endpoint     https://phone.aloaha.com/oauth2/v1/logout
#   jwks_uri                 (keys to check the id_token signature)
#   code_challenge_methods_supported  ["S256"]
#   acr_values_supported     ... urn:codeb:acr:eudi-wallet, urn:codeb:acr:eudi-wallet-linked, urn:codeb:acr:eidcard

The button on your page

A plain link to your own server. For the official look, use the button and QR kit.

html
<!-- The button only links to YOUR server; your server starts the sign-in. -->
<a href="/login" class="eudi-signin">Sign in with your EU Digital Identity Wallet</a>

<style>
  .eudi-signin { display:inline-block; padding:.75rem 1.2rem; border-radius:10px;
                 background:#003399; color:#fff; font:600 1rem system-ui, sans-serif;
                 text-decoration:none; }
  .eudi-signin:focus-visible { outline:3px solid #ffcc00; outline-offset:2px; }
</style>

Without a library: the raw HTTP flow

Useful to understand the flow, or for stacks without an OpenID Connect library.

http + shell
# 1. Send the browser to the authorization endpoint (one line, URL-encoded):
https://phone.aloaha.com/oauth2/v1/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fyour-app.example%2Fcallback
  &scope=openid%20profile%20email
  &acr_values=urn%3Acodeb%3Aacr%3Aeudi-wallet      # straight to the wallet sign-in
  &state=RANDOM_STATE
  &nonce=RANDOM_NONCE
  &code_challenge=BASE64URL_SHA256_OF_VERIFIER
  &code_challenge_method=S256

# 2. The browser comes back to https://your-app.example/callback?code=...&state=...
#    Check state, then exchange the code on your server:
curl -s https://phone.aloaha.com/oauth2/v1/token \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code=THE_CODE \
  -d redirect_uri=https://your-app.example/callback \
  -d code_verifier=THE_PKCE_VERIFIER

# 3. Validate the id_token (signature via jwks_uri, iss, aud, exp, nonce) and
#    accept the sign-in only if acr is urn:codeb:acr:eudi-wallet
#    or urn:codeb:acr:eudi-wallet-linked.

Node.js (Express + openid-client)

javascript
// npm install express express-session openid-client@5
const express = require('express');
const session = require('express-session');
const { Issuer, generators } = require('openid-client');

const ISSUER = 'https://phone.aloaha.com';
const BASE = process.env.APP_BASE_URL;            // e.g. https://your-app.example
const WALLET_ACRS = ['urn:codeb:acr:eudi-wallet', 'urn:codeb:acr:eudi-wallet-linked'];

const app = express();
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }));

let client;
async function codeb() {
  if (!client) {
    const issuer = await Issuer.discover(ISSUER);
    client = new issuer.Client({ client_id: process.env.CODEB_CLIENT_ID,
      client_secret: process.env.CODEB_CLIENT_SECRET, redirect_uris: [BASE + '/callback'], response_types: ['code'] });
  }
  return client;
}

app.get('/login', async (req, res, next) => {
  try {
    const c = await codeb(), verifier = generators.codeVerifier();
    req.session.oidc = { verifier, state: generators.state(), nonce: generators.nonce() };
    res.redirect(c.authorizationUrl({ scope: 'openid profile email', acr_values: WALLET_ACRS[0],
      code_challenge: generators.codeChallenge(verifier), code_challenge_method: 'S256',
      state: req.session.oidc.state, nonce: req.session.oidc.nonce }));
  } catch (e) { next(e); }
});

app.get('/callback', async (req, res, next) => {
  try {
    const c = await codeb(), o = req.session.oidc || {};
    const tokens = await c.callback(BASE + '/callback', c.callbackParams(req),
      { code_verifier: o.verifier, state: o.state, nonce: o.nonce });   // checks signature, iss, aud, exp, nonce
    const claims = tokens.claims();
    if (!WALLET_ACRS.includes(claims.acr)) return res.status(403).send('EU Digital Identity Wallet sign-in required');
    req.session.regenerate(err => {
      if (err) return next(err);
      req.session.user = { sub: claims.sub, given_name: claims.given_name, family_name: claims.family_name };
      res.redirect('/');
    });
  } catch (e) { next(e); }
});

app.listen(3000);

Python (Flask + Authlib)

python
# pip install flask authlib requests
import os
from flask import Flask, redirect, session
from authlib.integrations.flask_client import OAuth

WALLET_ACRS = ("urn:codeb:acr:eudi-wallet", "urn:codeb:acr:eudi-wallet-linked")
BASE = os.environ["APP_BASE_URL"]                 # e.g. https://your-app.example

app = Flask(__name__)
app.secret_key = os.environ["SESSION_SECRET"]
oauth = OAuth(app)
oauth.register("codeb",
    client_id=os.environ["CODEB_CLIENT_ID"], client_secret=os.environ["CODEB_CLIENT_SECRET"],
    server_metadata_url="https://phone.aloaha.com/.well-known/openid-configuration",
    client_kwargs={"scope": "openid profile email", "code_challenge_method": "S256"})

@app.route("/login")
def login():
    return oauth.codeb.authorize_redirect(BASE + "/callback", acr_values=WALLET_ACRS[0])

@app.route("/callback")
def callback():
    token = oauth.codeb.authorize_access_token()   # checks state, nonce, signature, iss, aud, exp
    claims = token["userinfo"]
    if claims.get("acr") not in WALLET_ACRS:
        return "EU Digital Identity Wallet sign-in required", 403
    session.clear()
    session["user"] = {"sub": claims["sub"], "given_name": claims.get("given_name")}
    return redirect("/")

PHP (jumbojett/openid-connect-php)

php
<?php
// composer require jumbojett/openid-connect-php
require __DIR__ . '/vendor/autoload.php';
use Jumbojett\OpenIDConnectClient;

session_start();
$oidc = new OpenIDConnectClient('https://phone.aloaha.com',
    getenv('CODEB_CLIENT_ID'), getenv('CODEB_CLIENT_SECRET'));
$oidc->setRedirectURL(getenv('APP_BASE_URL') . '/callback.php');
$oidc->addScope(['openid', 'profile', 'email']);
$oidc->setCodeChallengeMethod('S256');
$oidc->addAuthParam(['acr_values' => 'urn:codeb:acr:eudi-wallet']);

$oidc->authenticate();          // redirects to CodeB, and on the way back validates the id_token

$acr = $oidc->getVerifiedClaims('acr');
if (!in_array($acr, ['urn:codeb:acr:eudi-wallet', 'urn:codeb:acr:eudi-wallet-linked'], true)) {
    http_response_code(403);
    exit('EU Digital Identity Wallet sign-in required');
}
session_regenerate_id(true);
$_SESSION['user'] = ['sub' => $oidc->getVerifiedClaims('sub'),
                     'given_name' => $oidc->getVerifiedClaims('given_name')];
header('Location: /');

ASP.NET Core 8 (C#)

csharp
// ASP.NET Core 8: dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

var walletAcrs = new[] { "urn:codeb:acr:eudi-wallet", "urn:codeb:acr:eudi-wallet-linked" };
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(o =>
{
    o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(o =>
{
    o.Authority = "https://phone.aloaha.com";
    o.ClientId = builder.Configuration["CodeB:ClientId"];
    o.ClientSecret = builder.Configuration["CodeB:ClientSecret"];
    o.ResponseType = "code";
    o.UsePkce = true;
    o.CallbackPath = "/callback";
    o.MapInboundClaims = false;                 // keep "acr", "sub", "given_name" as they are
    o.Scope.Clear(); o.Scope.Add("openid"); o.Scope.Add("profile"); o.Scope.Add("email");
    o.Events.OnRedirectToIdentityProvider = ctx =>
    {
        ctx.ProtocolMessage.AcrValues = walletAcrs[0];   // straight to the wallet sign-in
        return Task.CompletedTask;
    };
    o.Events.OnTokenValidated = ctx =>
    {
        var acr = ctx.Principal?.FindFirst("acr")?.Value;
        if (acr is null || !walletAcrs.Contains(acr)) ctx.Fail("EU Digital Identity Wallet sign-in required");
        return Task.CompletedTask;
    };
});
builder.Services.AddAuthorization();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/", (HttpContext h) => $"Hello {h.User.FindFirst("given_name")?.Value}").RequireAuthorization();
app.Run();

What your app receives

Decoded id_token after a wallet sign-in (illustrative values). Which personal attributes you get depends on the scopes and on what the site operator allows for your client.

json
{
  "iss": "https://phone.aloaha.com",
  "aud": "YOUR_CLIENT_ID",
  "sub": "eu_3f8a1d7e",                       // stable per person for your client's tenant
  "acr": "urn:codeb:acr:eudi-wallet",         // HOW the person signed in: check it
  "amr": ["vc"],                              // vc = verifiable credential from the wallet
  "auth_time": 1792490000,
  "nonce": "RANDOM_NONCE",
  "given_name": "Erika",                      // with scope profile, from the presented PID
  "family_name": "Mustermann",
  "birthdate": "1964-08-12",
  "email": "erika@example.com"                // with scope email, when the person has one
}

Sign-in with a national eID card

text
# Same client, same code - only the acr changes:
acr_values=urn:codeb:acr:eidcard

# and accept the sign-in only if the id_token says
#   "acr": "urn:codeb:acr:eidcard"     (amr contains "sc" and "pop")
#
# The user signs in with the authentication key of a national eID card,
# read over NFC by the CodeB Wallet app.

Sign-out

http
# After clearing your own session, sign the person out at CodeB too
# (RP-initiated logout); the return URL must be registered for your client:
https://phone.aloaha.com/oauth2/v1/logout
  ?client_id=YOUR_CLIENT_ID
  &post_logout_redirect_uri=https%3A%2F%2Fyour-app.example%2F

Data deletion request (ARF)

The endpoint behind the “request deletion” link that wallets show for a relying party.

shell
# A person (or you, as data controller) asks CodeB to delete the wallet sign-in
# records about them. Public endpoint, no sign-in; reviewed by an operator.
curl -s -X POST 'https://phone.aloaha.com/oidc.ashx?action=data-deletion-request' \
  -H 'Content-Type: application/json' \
  -d '{
        "sub": "eu_3f8a1d7e",
        "requester_type": "controller",
        "contact": "privacy@your-app.example",
        "reason": "Customer closed the account"
      }'
# -> 202 { "request_id": "...", "status": "queued", "eta_days": 30 }

Frequently asked questions

Which OpenID Connect library should we use?

Any widely used OpenID Connect client works: openid-client (Node.js), Authlib (Python), jumbojett/openid-connect-php, Microsoft.AspNetCore.Authentication.OpenIdConnect, Spring Security, go-oidc. Nothing CodeB-specific is needed.

Why do we have to check acr ourselves?

Because a browser can be sent back to your callback from any sign-in. acr_values asks for the wallet sign-in; the acr claim in the validated id_token proves it happened (OpenID Connect Core 3.1.2.1). Every sample checks it.

Can a single-page app use it without a back end?

Yes, as a public client with PKCE (no client secret), see the OIDC sign-in cookbook. A back end that keeps a client secret is the safer choice for production.

What does eudi-wallet-linked mean?

The person presented their wallet and that presentation was matched to an existing account on the site; otherwise the acr is urn:codeb:acr:eudi-wallet. Treat both as a wallet sign-in.

How do we test without a production wallet?

Use the CodeB Wallet or the EU reference wallet with test credentials, and the verifier self-test to see which HAIP checks a wallet passes.

Is there a sandbox?

Yes: we register test clients on our test tenant first. Ask us for a test client together with your redirect URI.

Request a test client

Send us your redirect URI and the name of your service; you get a client ID and secret on the test tenant.