Entwickler · OpenID Connect · EU-Wallet

Die EU-Wallet in Ihre Anmeldung einbauen: Schnellstart

Das CodeB Relying Party Gateway ist für Ihre Anwendung ein normaler OpenID-Connect-Anbieter. Sie fügen einen Parameter hinzu und prüfen einen Claim – die Wallet-Protokolle bleiben außerhalb Ihres Codes.

Das Wichtigste: 1. Senden Sie acr_values=urn:codeb:acr:eudi-wallet im Authorization-Request mit. 2. Akzeptieren Sie nur ein geprüftes id_token, dessen acr urn:codeb:acr:eudi-wallet oder urn:codeb:acr:eudi-wallet-linked ist.

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

Geschlossene Beta, im Test. Nicht zertifiziert.

Auf einen Blick

Issuer
https://phone.aloaha.com (Discovery unter /.well-known/openid-configuration)
Ablauf
Authorization Code mit PKCE (S256), state, nonce
Wallet erzwingen
acr_values=urn:codeb:acr:eudi-wallet (eID-Ausweis: urn:codeb:acr:eidcard)
Prüfen
acr im id_token; amr enthält vc (Wallet) bzw. sc + pop (Ausweis)
Scopes
openid, profile, email (weitere: phone, address)
Client-Authentisierung
client_secret_basic, client_secret_post oder öffentlicher Client mit PKCE

In vier Schritten

Client erhalten

Nennen Sie uns Ihre Redirect-URI(s), etwa https://your-app.example/callback. Sie erhalten client_id und client_secret; Ihre Relying-Party-Angaben kommen ins Registrierungszertifikat.

Mit dem Wallet-acr weiterleiten

Starten Sie einen Authorization-Code-Ablauf mit PKCE (S256), state und nonce und ergänzen Sie acr_values=urn:codeb:acr:eudi-wallet.

Code einlösen

Tauschen Sie auf Ihrem Server den Code am Token-Endpunkt gegen Tokens und prüfen Sie das id_token: Signatur, Aussteller, Empfänger, Ablauf, Nonce.

acr prüfen

Akzeptieren Sie die Anmeldung nur, wenn acr gleich urn:codeb:acr:eudi-wallet oder urn:codeb:acr:eudi-wallet-linked ist. Fertig.

Code-Beispiele

Kompakte Beispiele; vollständige, getestete Apps für sechs Umgebungen (auch Java und Go) stehen im Integrations-Kochbuch. Ersetzen Sie YOUR_CLIENT_ID, your-app.example usw. durch Ihre Werte.

Discovery-Dokument

Alle Endpunkte und Schlüssel stehen in einem Dokument; jede OpenID-Connect-Bibliothek liest es.

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

Der Button auf Ihrer Seite

Ein einfacher Link auf Ihren eigenen Server. Für das offizielle Aussehen gibt es das Button- und 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>

Ohne Bibliothek: der HTTP-Ablauf

Zum Verstehen des Ablaufs oder für Umgebungen ohne OpenID-Connect-Bibliothek.

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();

Was Ihre Anwendung erhält

Dekodiertes id_token nach einer Wallet-Anmeldung (Beispielwerte). Welche persönlichen Angaben Sie erhalten, hängt von den Scopes ab und davon, was der Betreiber für Ihren Client freigibt.

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
}

Anmeldung mit dem nationalen eID-Ausweis

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.

Abmelden

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

Löschantrag (ARF)

Der Endpunkt hinter dem Link „Löschung beantragen“, den Wallets zu einem Relying Party anzeigen.

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 }

Häufige Fragen

Welche OpenID-Connect-Bibliothek sollen wir nehmen?

Jeder verbreitete OpenID-Connect-Client funktioniert: openid-client (Node.js), Authlib (Python), jumbojett/openid-connect-php, Microsoft.AspNetCore.Authentication.OpenIdConnect, Spring Security, go-oidc. Nichts CodeB-Spezifisches ist nötig.

Warum müssen wir acr selbst prüfen?

Weil ein Browser von jeder Anmeldung zu Ihrem Callback zurückgeschickt werden kann. acr_values fordert die Wallet-Anmeldung an; der acr-Claim im geprüften id_token belegt, dass sie stattfand (OpenID Connect Core 3.1.2.1). Jedes Beispiel prüft ihn.

Geht es auch mit einer Single-Page-App ohne Backend?

Ja, als öffentlicher Client mit PKCE (ohne Client-Secret), siehe das OIDC-Anmelde-Kochbuch. Für den Produktivbetrieb ist ein Backend mit Client-Secret die sicherere Wahl.

Was bedeutet eudi-wallet-linked?

Die Person hat ihre Wallet vorgelegt und diese Vorlage wurde einem bestehenden Konto auf der Seite zugeordnet; sonst lautet der acr urn:codeb:acr:eudi-wallet. Behandeln Sie beide als Wallet-Anmeldung.

Wie testen wir ohne produktive Wallet?

Mit der CodeB Wallet oder der EU-Referenz-Wallet und Testnachweisen, dazu der Verifier-Selbsttest, der zeigt, welche HAIP-Prüfungen eine Wallet besteht.

Gibt es eine Testumgebung?

Ja: Testclients registrieren wir zuerst auf unserem Testmandanten. Fragen Sie uns mit Ihrer Redirect-URI nach einem Testclient.

Testclient anfragen

Schicken Sie uns Ihre Redirect-URI und den Namen Ihres Dienstes; Sie erhalten Client-ID und Secret für den Testmandanten.