EU-Wallet · Verifier-API

EU-Wallet-Verifier · öffentliche API

OID4VP-1.0-Verifier (Relying Party) für das EU Digital Identity Wallet und jede Wallet, die denselben Dialekt spricht. Jeder unten dokumentierte Endpunkt ist auf dem Live-Host www.aloaha.com erreichbar; die Antwortformate sind gegen den laufenden Build verifiziert. Wallets und Integratoren nutzen diese Seite als kanonische Integrationsreferenz.

Von Aloaha Limited gebaut. Kein Fork eines SDKs. Jeder unten dokumentierte Endpunkt ist intern implementiert — SD-JWT-VC-Parsing, JWS- + KB-JWT-Verifikation, OpenID4VP Request/Response, DCQL, JWE ECDH-ES, x5c-Chain-Validierung, StatusList-Revokation, OpenID Federation 1.0. Kein eudi-lib-*, kein Drittanbieter-EUDI-SDK im Dependency-Tree. Drei konkrete Zusagen: (1) der Code gehört uns — jede EUDI-Protokoll-Zeile prüfbar, keine geschlossenen EUDI-Bibliotheken, denen Ihr CISO blind vertrauen müsste (generische Infrastruktur-Libs sind Open-Source-Standardkomponenten); (2) eigener Release-Rhythmus — CVE-Patches und Features landen auf unserem Zeitplan, nicht abhängig von einer SDK-Version eines Konsortiums; (3) strikt standardbasierte Interoperabilität — Wallet, Verifier und Issuer sprechen ausschließlich über veröffentlichte RFCs und EUDI-Protokolle, jedes Stück austauschbar, kein Vendor-Lock-in auch zwischen unseren Komponenten.
Mit x509_hash arbeiten — dem Wallet‑bevorzugten Client‑Identifier‑Prefix. x509_hash (OID4VP 1.0 §5.9.3, base64url SHA‑256 des DER‑Leaf‑Zertifikats) ist die Form, an die sich reale EUDI‑Wallets primär binden. Der Verifier unterstützt zusätzlich x509_san_dns (HAIP 1.0 Final) als Kompatibilitätsoption. Der Aufrufer von vp‑start wählt das Prefix pro Session via ?client_id_prefix=x509_hash|x509_san_dns; Wallet-Integratoren sollten ausdrücklich x509_hash anfragen. Server-Standard ohne Parameter ist nun x509_hash — passend zum bevorzugten Wallet-Pfad. Einmal gewählt, ist das Prefix an die Session gebunden und wird persistiert, damit der client_id-Claim im JAR und die ExpectedClientId-Validierung konsistent bleiben.

GET /oidc.ashx?action=ping #

Health-Check und Live-Verifier-Metriken. Öffentlich, ohne Authentifizierung. Für Monitoring oder zur Bestätigung einer deployten Build-Version.

Response

JSON-Body. Felder im Detail:

  • oktrue, wenn der OIDC-Handler initialisiert ist.
  • build — Build-Version des Handlers. Format YYYY-MM-DD-slug. Wird bei jeder relevanten Änderung erhöht.
  • tenant — aufgelöster Tenant (Multi-Tenancy nach Domain).
  • now — aktuelle Serverzeit in Sekunden seit der Epoche.
  • vp_started — Zähler aller seit Service-Start angelegten vp‑start-Sessions.
  • vp_completed — Zähler aller vp‑response-Verifikationen, die mit ok=true abgeschlossen wurden.
  • vp_abandoned — Zähler abgebrochener Sessions (gestartet, keine Antwort innerhalb der TTL). Damit lässt sich die Abbruchrate ablesen.
  • vp_pending_or_inflight — Gauge der momentan auf eine Wallet-Antwort wartenden Sessions.

Beispiel

curl https://phone.aloaha.com/oidc.ashx?action=ping
{
  "ok": true,
  "build": "2026-06-09-vp-traceall",
  "tenant": "www.aloaha.com",
  "now": 1780995196,
  "vp_started": 2,
  "vp_completed": 2,
  "vp_abandoned": 0,
  "vp_pending_or_inflight": 0
}

GET /oidc.ashx?action=verifier-metadata #

Verifier-Deskriptor (Relying Party) — listet kryptographische Algorithmen, Response‑Encryption-Modi, unterstützte Credential-Formate und die Endpunkte, mit denen eine Wallet sprechen soll. Folgt dem von EUDI-Referenz-Deployments etablierten Verifier-Metadata-Muster.

Response (Live-Form)

{
  "client_id": "x509_hash:4Ns4_AZgRtUzCbECSdEGgIdz_EG0iVqO8INWyufppZw",
  "client_id_scheme": "x509_hash",
  "client_id_schemes_supported": ["x509_hash","x509_san_dns"],
  "x509_hash_value": "4Ns4_AZgRtUzCbECSdEGgIdz_EG0iVqO8INWyufppZw",
  "request_object_signing_alg_values_supported": ["ES256"],
  "vp_formats_supported": {
    "dc+sd-jwt": {
      "sd-jwt_alg_values": ["ES256"],
      "kb-jwt_alg_values": ["ES256"]
    }
  },
  "authorization_encrypted_response_alg": ["ECDH-ES"],
  "authorization_encrypted_response_enc": ["A128GCM"],
  "x509_thumbprint_sha1": "EC9DE3346BB69BBB52D0023BDF47D0E041A97A9D",
  "request_uri_endpoint":  "https://phone.aloaha.com/oidc.ashx?action=vp-request&id={id}",
  "response_uri_endpoint": "https://phone.aloaha.com/oidc.ashx?action=vp-response&id={id}",
  "vp_start_endpoint":     "https://phone.aloaha.com/oidc.ashx?action=vp-start",
  "status": "live",
  "notes": "OID4VP 1.0 end-to-end live. Beide Client-Identifier-Prefix-Schemata unterstützt."
}

Feldhinweise

  • x509_hash_value — base64url SHA‑256 des DER‑Leaf‑Zertifikats. Das ist der primäre Identifier, an den reale EUDI‑Wallets binden; x509_hash:<hash> ist die empfohlene Client‑Identifier‑Prefix‑Form.
  • client_id_schemes_supported — vollständige Liste der pro‑Request akzeptierten Prefixes (x509_hash empfohlen; x509_san_dns für HAIP 1.0 Final-Kompatibilität).
  • client_id — der prefixierte Identifier, wenn ?client_id_prefix= nicht an vp‑start übergeben wird. Server-Standard ist nun x509_hash (Wallet‑bevorzugte Form).
  • vp_formats_supported — CodeB liefert ausschließlich dc+sd-jwt. mdoc / ISO mDL ist nicht im Scope.
  • authorization_encrypted_response_alg / _enc — Wallets MÜSSEN die VP-Response verschlüsseln (JWE), wenn der Request direct_post.jwt wählt.

GET /.well-known/openid-configuration #

Standard-OpenID-Connect-Discovery-Dokument. Listet OIDC-Endpunkte, Claim-Namen, Signaturalgorithmen sowie die amr/acr-Werte, die der Verifier für Wallet-basierte Sessions ausgibt. Selber Handler wie der restliche OIDC-IdP — vollständige Hülle siehe oidc_api.html; die EU-Wallet-relevanten Teile unten.

EU-Wallet-relevante Felder

  • acr_values_supported enthält eudi:pid:high, eudi:pid:substantial, urn:codeb:vc:member — der Verifier markiert die resultierende OIDC-Session mit acr=urn:codeb:acr:eudi-wallet, wenn die Authentifizierung über den EU-Wallet-Flow erfolgte.
  • amr_values_supported enthält implizit vc — wird in der SSO-Assertion gemeldet (siehe §SSO-Assertion).
  • claims_supported deckt die Standard-PID-Claim-Form ab, die der EU-PID-Issuer liefert (given_name, family_name, birth_date, email_address, etc.).

Auch erreichbar als /oidc.ashx?action=discovery.

GET /oidc.ashx?action=vp-start #

Startet einen Verifiable-Presentation-Flow. Aufrufer ist üblicherweise eine CodeB‑gehostete Login-Seite (logineu.html) oder ein externer Integrator. Zurückgegeben werden die Session-ID, die URL, über die die Wallet den signierten Request abholt, und ein Wallet‑Invocation-Deep-Link.

Request

Kein Body. Query-Parameter:

  • client_id_prefix — optional, einer von x509_hash (empfohlen für reale EUDI‑Wallets gemäß OID4VP 1.0 §5.9.3) oder x509_san_dns (HAIP 1.0 Final-Kompatibilitätsprofil). Bindet das Client-Identifier-Prefix-Schema an die Session. Server-Standard ohne Parameter: x509_hash.

Response

{
  "id": "vp-7c5f0e3d4b1a4f9c",
  "request_uri": "https://phone.aloaha.com/oidc.ashx?action=vp-request&id=vp-7c5f0e3d4b1a4f9c",
  "deep_link": "openid4vp://?client_id=x509_hash%3A4Ns4_AZgRtUzCbECSdEGgIdz_EG0iVqO8INWyufppZw&request_uri=https%3A%2F%2Fwww.aloaha.com%2Foidc.ashx%3Faction%3Dvp-request%26id%3Dvp-7c5f0e3d4b1a4f9c",
  "client_id": "x509_hash:4Ns4_AZgRtUzCbECSdEGgIdz_EG0iVqO8INWyufppZw",
  "expires_in": 300,
  "status": "pending"
}

Das gewählte Prefix steckt im client_id-Wert: x509_hash:<base64url‑sha256‑des‑DER‑Leafs> (Wallet‑bevorzugt) bzw. x509_san_dns:www.aloaha.com (HAIP 1.0 Final). Aufrufer können am ersten : trennen, um das Prefix zu extrahieren. status startet bei pending und wird beim Wallet-Roundtrip fortgeschrieben.

Beispiele

# Wallet‑bevorzugt (x509_hash gemäß OID4VP 1.0 §5.9.3) — empfohlen für reale EUDI‑Wallets.
curl 'https://phone.aloaha.com/oidc.ashx?action=vp-start&client_id_prefix=x509_hash'

# Kompatibilitätsprofil (x509_san_dns gemäß HAIP 1.0 Final).
curl 'https://phone.aloaha.com/oidc.ashx?action=vp-start&client_id_prefix=x509_san_dns'

# Ohne Parameter: Server fällt jetzt auf x509_hash zurück (Wallet‑bevorzugt).
curl https://phone.aloaha.com/oidc.ashx?action=vp-start

Session-Lebenszyklus

Die Session-ID ist opak, ca. 16 hexadezimale Zeichen, tenant-skopiert. Der Session-Datensatz wird auf Platte unter App_Data/<tenant>/vp‑sessions/<id>.json persistiert, damit App-Pool-Recycles und Multi-Worker-IIS-Deployments den Zustand nicht verlieren. Sessions laufen nach expires_in Sekunden ab; abgebrochene Sessions zählen in die vp_abandoned-Metrik von ping.

GET /oidc.ashx?action=vp-request&id={id} #

Die Wallet ruft diesen Endpunkt auf, um den signierten Authorization-Request (JAR — JWT‑Secured Authorization Request, RFC 9101) zu erhalten. Der Body ist ein einzelnes ES256‑signiertes JWT mit der x5c-Zertifikatskette des Verifier-Leafs im JWS-Header.

Response-Header

Content-Type: application/oauth-authz-req+jwt
Cache-Control: no-store

Response-Body

ES256-signiertes JWT. Dekodierter Payload (exemplarisch):

{
  "iss":  "x509_hash:4Ns4_AZgRtUzCbECSdEGgIdz_EG0iVqO8INWyufppZw",
  "aud":  "https://self-issued.me/v2",
  "iat":  1780995100,
  "exp":  1780995400,
  "nonce": "fA72c1...zufälliges base64url",
  "client_id":         "x509_hash:4Ns4_AZgRtUzCbECSdEGgIdz_EG0iVqO8INWyufppZw",
  "client_id_scheme":  "x509_hash",
  "response_type":     "vp_token",
  "response_mode":     "direct_post.jwt",
  "response_uri":      "https://phone.aloaha.com/oidc.ashx?action=vp-response&id=vp-7c5f0e3d4b1a4f9c",
  "client_metadata": {
    "authorization_encrypted_response_alg": "ECDH-ES",
    "authorization_encrypted_response_enc": "A128GCM",
    "jwks": { "keys": [ /* ephemerer ECDH-ES-Schlüssel für die Response-Verschlüsselung */ ] }
  },
  "dcql_query": {
    "credentials": [
      {
        "id": "pid",
        "format": "dc+sd-jwt",
        "meta": { "vct_values": ["urn:eu.europa.ec.eudi:pid:1"] },
        "claims": [
          { "path": ["given_name"] },
          { "path": ["family_name"] },
          { "path": ["birth_date"] },
          { "path": ["email_address"] }
        ]
      }
    ]
  }
}

JWS-Header (exemplarisch)

{
  "alg": "ES256",
  "typ": "oauth-authz-req+jwt",
  "x5c": [
    "MIIB...Verifier-Leaf-Zertifikat, base64-kodierte DER",
    "MIIB...Intermediate (falls vorhanden)"
  ]
}

Hinweise

  • Der client_id-Claim ist mit dem vom Aufrufer bei vp‑start gewählten Prefix versehen (x509_hash: oder x509_san_dns:).
  • Bei x509_hash ist der Prefix-Wert base64url(SHA‑256(DER Leaf)), identisch mit dem x509_hash_value in verifier‑metadata.
  • Die Wallet validiert die Signatur gegen das Leaf in der x5c-Kette und gleicht das gewählte Prefix mit dem client_id-Claim ab.

POST /oidc.ashx?action=vp-response&id={id} #

Die Wallet sendet die Verifiable Presentation hier hoch. Der Verifier akzeptiert beide in OID4VP 1.0 §8.4 definierten Wire-Formen — wähle, was die Wallet liefert; der Handler erkennt die Form automatisch.

Form A — direct_post.jwt (verschlüsselt)

Content-Type: application/x-www-form-urlencoded

response=<JWE>

Das <JWE> ist mit ECDH‑ES-Schlüsselvereinbarung und A128GCM-Content-Encryption verschlüsselt, gegen den ephemeren JWK, den der Verifier im JAR-Feld client_metadata.jwks mitgegeben hat. Der Klartext im JWE ist der form‑kodierte Body aus Form B.

Form A — Variante

Content-Type: application/oauth-authz-resp+jwt

<JWE>

Manche Wallets liefern das JWE als rohen Body statt form-verpackt. Der Verifier akzeptiert beides.

Form B — direct_post (Klartext, form-kodiert)

Content-Type: application/x-www-form-urlencoded

vp_token=<SD-JWT-VC>&state=<optional>

Das vp_token ist die SD‑JWT VC mit selektiv offengelegten Claims und angehängtem KB‑JWT für das Holder-Binding. Format: <Issuer‑signiertes JWT>~<Disclosure1>~<Disclosure2>~...~<KB‑JWT>.

Erfolgs-Response

HTTP/1.1 200 OK
Content-Type: application/json

{
  "ok": true,
  "redirect": "/account.html",
  "sso_assertion": "eyJhbGciOiJSUzI1NiIs...",
  "sso_max_age": 3600,
  "vct": "urn:eu.europa.ec.eudi:pid:1",
  "issuer": "https://issuer.eudiw.dev/pid",
  "disclosures_verified": 4
}

Feldhinweise

  • redirect — wohin die aufrufende Seite den Browser als Nächstes leiten soll. Auf dem Standard-Mandanten immer /account.html; kann eine tiefere Return-URL sein, wenn der Wallet-Flow von dort gestartet wurde.
  • sso_assertion — RS256-signiertes Assertion-JWT, das der Browser-Tab im sessionStorage ablegt und am /oauth2/v1/authorize gegen einen OIDC-Code eintauscht (siehe SSO-Assertion).
  • sso_max_age — Assertion-Lebensdauer in Sekunden.
  • vct + issuer — Credential-Typ und Issuer der gerade verifizierten SD-JWT VC. Die PID-Claims selbst werden in den Benutzerdatensatz auf der Platte geschrieben; sie werden nicht im Response-Body wiederholt. Nach dem Assertion-Tausch sind die Claims über /oauth2/v1/userinfo abrufbar.
  • disclosures_verified — Anzahl selektiv offengelegter Claims, deren Hash gegen den SD-Claim-Digest im Issuer-signierten JWT passte.

Fehler-Response

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "ok": false,
  "error": "invalid_vp_token",
  "error_description": "kb-jwt signature did not verify against holder cnf"
}

Verifikationsstufen (in Reihenfolge)

  1. Body-Form-Erkennung (JWE‑form‑unwrap / JWE‑raw / plain).
  2. JWE-Decrypt (ECDH‑ES + A128GCM), falls anwendbar.
  3. SD‑JWT-VC-Envelope-Parse + Struktur-Validierung.
  4. Disclosure-Hash-Prüfung gegen die SD-Claim-Digests.
  5. KB‑JWT-Signaturprüfung gegen den Holder-Binding-Schlüssel in cnf.
  6. User-Identifier-Bindung (eine von: userHint, personal_administrative_number, email_address, pid_name_dob_hash, anonymous).
  7. SSO-Assertion-Mint (RS256, OIDC-kompatibel).

Jede Stufe emittiert ein strukturiertes Log-Event mit Session-ID (vp‑shape‑detected, vp‑verify‑result, vp‑user‑resolved, vp‑sso‑minted, etc.). Der Pfad einer fehlschlagenden Wallet lässt sich allein aus dem OIDC-Log nachvollziehen.

SSO sso_assertion · OIDC-Code-Flow-Übergabe #

Die Erfolgs-Response von vp‑response enthält sso_assertion — ein kurzlebiges RS256-signiertes JWT, das jede CodeB-föderierte Applikation gegen eine regäläre OIDC-Session eintauschen kann. So entsteht aus der Wallet-Präsentation eine normale Anmeldung.

Assertion-Payload (exemplarisch)

{
  "iss":  "https://www.aloaha.com",
  "sub":  "eu_3f8a1d7e",
  "role": "guest",
  "amr":  ["vc"],
  "acr":  "urn:codeb:acr:eudi-wallet",
  "iat":  1780995200,
  "exp":  1780998800
}

Konsumption auf der Relying-Party-Seite

  1. Führe einen Standard-OIDC-Authorization-Code-Flow mit PKCE gegen /oauth2/v1/authorize wie in oidc_api.html beschrieben. Füge acr_values=urn:codeb:acr:eudi-wallet hinzu, um eine EU-Wallet-basierte Anmeldung zu erzwingen.
  2. Der CodeB-IdP leitet den Nutzer durch logineu.html, die im Hintergrund vp‑start, vp‑request und vp‑response ansteuert.
  3. Bei Erfolg schließt der IdP den OIDC-Flow normal ab — deine Applikation erhält einen Authorization Code, tauscht ihn am /oauth2/v1/token-Endpunkt ein und bekommt id_token und access_token, als hätte sich der Nutzer per Passwort angemeldet.
  4. Das id_token trägt amr: ["vc"] und acr: "urn:codeb:acr:eudi-wallet", dazu die PID-Claims, die deine App über Standard-OIDC-Scopes (profile, email) angefordert hat.

Aus dem Wallet-Flow geminte User-IDs sind subjektstabil (z. B. pin_de_3f8a1d7e für eine PID mit Personalverwaltungsnummer, eu_3f8a1d7e für den Name-und-Geburtsdatum-Hash als Fallback). Beim ersten Sign-In ist die Rolle guest; Admins können die Rolle erhöhen.

POST /oauth2/v1/token · JWT‑Bearer-Grant (RFC 7523) #

Tausche eine SSO-Assertion (die vp‑response-Erfolgsantwort) gegen ein OIDC-access_token + id_token + refresh_token. Praktisch für Service-zu-Service-Flows und Integratoren (oder Test-Skripte), die nach erfolgreicher Wallet-Präsentation ein Bearer brauchen, ohne den Browser durch /authorize zu schleifen.

Request

POST /oauth2/v1/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<sso_assertion>        (aus der vp-response-Erfolgsantwort)
&client_id=<client_id>             (registrierter Client)
&client_secret=<secret>            (nur Confidential-Clients)
&scope=openid                       (optional, Standard openid)

Erfolgsantwort (200)

{
  "access_token":  "eyJhbGciOiJSUzI1NiIs...",
  "token_type":    "Bearer",
  "expires_in":    3600,
  "id_token":      "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "...",
  "scope":         "openid"
}

Fehler

  • 400 invalid_requestassertion oder client_id fehlt.
  • 401 invalid_grant — Assertion fehlerhaft, falsche aud, falscher typ (muss sso sein) oder absolute auth_time-Grenze (4 Stunden) überschritten.
  • 401 invalid_client — Confidential-Client hat client_secret nicht bestätigt.

Beispiel

curl https://www.aloaha.com/oauth2/v1/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  --data-urlencode "assertion=$SSO_ASSERTION" \
  -d "client_id=codeb-admin" \
  -d "scope=openid"
Type-Guard. Der typ-Claim der Assertion muss "sso" sein. access_token oder id_token im assertion-Parameter werden mit invalid_grant abgelehnt — Schutz gegen Token-Reuse (Confused-Deputy).

GET /signal.ashx?account=get #

Lese den Konto-Zustand des angemeldeten Nutzers. Unmittelbar vor account=password aufgerufen, damit der Aufrufer den Realm (für die HA1-Berechnung) und das has_password-Flag kennt.

Request

GET /signal.ashx?account=get
Authorization: Bearer <access_token>

Erfolgsantwort (200)

{
  "user":         "eu_3f8a1d7e",
  "role":         "guest",
  "disabled":     false,
  "profile":      { ... gespeicherte OIDC-Claims ... },
  "has_password": false,            // true wenn bereits HA1 gespeichert
  "realm":        "www.aloaha.com"  // für HA1 = MD5(user:realm:password)
}

Fehler

  • 401 invalid_token — Bearer fehlt, abgelaufen oder falsche Audience.
Warum Realm zurückgeben? Die Credentials-Datei nutzt SIP-Digest-HA1 = MD5(user:realm:password). Wenn account=get den Realm liefert, kann der Aufrufer HA1 client-seitig berechnen ohne separaten Config-Lookup; das Klartext-Passwort verlässt den Browser nie.

POST /signal.ashx?account=password · Erstvergabe + Wallet‑als‑Recovery #

Setze oder rotiere das Passwort des angemeldeten Nutzers. Drei Pfade, abhängig vom gespeicherten Zustand und dem Bearer-Faktor:

  • Erstvergabe (Nutzer hat noch kein HA1) — current_ha1 NICHT erforderlich. Wallet-erstellte Gäste aktivieren so Benutzername-+-Passwort-Anmeldung.
  • Wallet‑als‑Recovery (Nutzer hat HA1, Bearer trägt acr=urn:codeb:acr:eudi-wallet) — current_ha1 NICHT erforderlich. Die Wallet-Authentifizierung ist der Identitätsnachweis; klassischer "Passwort vergessen"-Pfad.
  • Reguläre Änderung (Nutzer hat HA1, Bearer ist passwort‑authentifiziert) — current_ha1 MUSS mitgesendet werden und wird constant-time verglichen.

Request

POST /signal.ashx?account=password
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "ha1":         "<32 Hex-Zeichen kleingeschrieben = MD5(user:realm:password)>",
  "current_ha1": "<optional; nur bei regulärer Änderung Pflicht>"
}

Erfolgsantwort (200)

{
  "ok":              true,
  "first_set":       true,    // true falls vorher kein Passwort vorhanden
  "wallet_recovery": true     // true falls Wallet-Auth current_ha1 ersetzt
}

Beide Flags können bei einer ganz neuen Wallet-gesteuerten Erstvergabe true sein — der Nutzer hatte noch kein Passwort (first_set) UND die Wallet-Auth war der Nachweis (wallet_recovery).

Fehler

  • 400 missing_ha1 / invalid_ha1 — Body fehlt oder fehlerhaft.
  • 400 missing_current_ha1current_ha1 wird für diese Änderung gebraucht, wurde aber nicht mitgesendet.
  • 400 invalid_current_ha1 — Wert hat nicht die richtige HA1-Form.
  • 400 new_equals_current — neues Passwort ist identisch zum aktuellen.
  • 401 current_password_incorrectcurrent_ha1 passt nicht zum gespeicherten Wert.

Audit-Spur

Drei verschiedene Ereignisnamen im Connection-Log, damit Operatoren die Pfade unterscheiden können:

  • account-password-first-set — Wallet-Gast vergab erstes Passwort.
  • account-password-wallet-reset — bestehender Nutzer hat über Wallet zurückgesetzt.
  • account-password-changed — bestehender Nutzer hat regulär rotiert.

FLOW End‑to‑end-Walkthrough: Wallet → Passwort setzen → manuell anmelden #

Die gesamte Integrationsstory. Sechs Request/Response-Hops, mit denen ein brandneuer EUDI-Wallet-Nutzer (a) sich per Wallet anmeldet, (b) im selben wallet-authentifizierten Session-Kontext Benutzername-+-Passwort-Anmeldung aktiviert und (c) per Anmeldung die Funktionalität verifiziert. Identische Form wie eu‑wallet‑mock.py --set-password — das ausgelieferte automatisierte Test-Skript für genau diesen Flow.

Vorbedingung: Integrator hat bereits vp‑start → vp‑request → vp‑response durchlaufen und hält:

  • sso_assertion aus dem Erfolgs-Body von POST /oidc.ashx?action=vp‑response (Single-Use, 30 Min TTL).
  • Den Benutzernamen aus demselben Body (z. B. eu_3f8a1d7e).

Schritt 1 — SSO-Assertion gegen OIDC-access_token tauschen

curl https://www.aloaha.com/oauth2/v1/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  --data-urlencode "assertion=$SSO_ASSERTION" \
  -d "client_id=codeb-admin" \
  -d "scope=openid"

# -> { access_token, id_token, refresh_token, expires_in, scope }
ACCESS_TOKEN=$(... aus Antwort extrahieren ...)

Schritt 2 — user + realm + has_password lesen

curl https://phone.aloaha.com/signal.ashx?account=get \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# -> { user: "eu_3f8a1d7e", role: "guest", has_password: false,
#      realm: "www.aloaha.com", ... }
USER="eu_3f8a1d7e"
REALM="www.aloaha.com"

Schritt 3 — HA1 = MD5(user:realm:password) berechnen

# Bash / OpenSSL
HA1=$(printf "%s:%s:%s" "$USER" "$REALM" "MeinNeuesPW" | md5sum | awk '{print $1}')

# Python
import hashlib
ha1 = hashlib.md5(f"{user}:{realm}:MeinNeuesPW".encode()).hexdigest()

Schritt 4 — Das neue HA1 an account=password senden

curl https://phone.aloaha.com/signal.ashx?account=password \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"ha1\":\"$HA1\"}"

# -> { ok: true, first_set: true, wallet_recovery: true }
# (beide Flags true bei der allerersten Wallet-gesteuerten Vergabe)

Schritt 5 — Verifizieren: mit neuem Passwort anmelden

curl https://phone.aloaha.com/oidc.ashx?action=login \
  -d "username=$USER" \
  -d "ha1=$HA1" \
  -d "return=/"

# -> { ok: true, user, role, sso_assertion, sso_max_age, redirect }
# Frische sso_assertion = Passwort funktioniert tatsächlich.

Schritt 6 — (Manuelle UI-Anmeldung) der Nutzer ruft /login.html auf und meldet sich an

Benutzername = eu_3f8a1d7e (oder das, was vp‑response zurückgab), Passwort = der Klartext aus Schritt 3. Der Browser hashed lokal zu HA1; der Server vergleicht mit dem Wert aus Schritt 4. Der Nutzer ist nun dauerhaft sowohl per Wallet als auch per Passwort anmeldebar.

Forgot-Password-Pfad

Exakt dieselben sechs Schritte funktionieren, wenn der Nutzer bereits ein Passwort hat. Schritt 4 liefert first_set: false, wallet_recovery: true — die Wallet-Auth ersetzt current_ha1. Schritt 5 bestätigt, dass das neue Passwort funktioniert. Im Audit-Log steht account-password-wallet-reset.

Sicherheits-Argument. Das Überspringen von current_ha1 geschieht nur, wenn der Bearer acr=urn:codeb:acr:eudi-wallet trägt — dieser Wert wird ausschließlich gesetzt, wenn der OID4VP-Verifier einen vollständigen SD‑JWT-VC- + KB‑JWT-Roundtrip mit kryptographisch verifizierter Holder-Bindung abgeschlossen hat. Eine erfolgreiche Wallet-Auth ist mindestens so stark wie ein Passwort; zusätzlich current_ha1 zu verlangen wäre ein UX-Bug, kein Sicherheits-Feature.

TRY eu-wallet-mock-both.py · lokale Mock-Wallet-Komplettrunde #

Der mitgelieferte Test-Harness durchläuft beide Client-Identifier-Prefixes hintereinander, end-to-end. Liefern beide Aufrufe HTTP 200 mit offengelegten PID-Claims und einer SSO-Assertion, funktioniert die Integration.

Ablauf

# 1. Session mit x509_hash starten (Wallet‑bevorzugt)
GET https://phone.aloaha.com/oidc.ashx?action=vp-start&client_id_prefix=x509_hash
        → { id, request_uri, deep_link, client_id, status }

# 2. Signierten Request abholen (wie es die Wallet tun würde)
GET {request_uri}
        → ES256-signiertes JWT, application/oauth-authz-req+jwt

# 3. JWS verifizieren, VP-Response bauen
#    (SD-JWT VC + KB-JWT gegen den ephemeren ECDH-ES-Schlüssel des Verifiers)

# 4. Response posten
POST https://phone.aloaha.com/oidc.ashx?action=vp-response&id={id}
     Content-Type: application/x-www-form-urlencoded
     Body: response=<JWE>
        → { ok:true, vct, iss, disclosures_verified, claims, sso_assertion }

# 5. Mit client_id_prefix=x509_san_dns wiederholen, um den HAIP‑1.0‑Final
#    Kompatibilitätspfad zu verifizieren. Selbes Skript, nur Query-Parameter umstellen.

Build-Abgleich

# Bestätige, dass du den erwarteten Build testest.
curl https://phone.aloaha.com/oidc.ashx?action=ping
# build = aktuelle Handler-Version. Wird bei jeder relevanten Änderung erhöht.

Log-Events für Operator-Greps

Jede in vp‑response aufgeführte Stufe emittiert ein strukturiertes Event mit Session-ID; dazu kommen einige Fehlerpfad-Events. Greppbares Vokabular (Ausschnitt):

vp-start · vp-request · vp-response-arrived
vp-shape-detected · vp-verify-result
vp-user-resolved · vp-sso-minted
vp-response-ok · vp-response-rejected
vp-abandoned · vp-user-binding-invalid
vp-request-miss · vp-request-key-fail · vp-request-sign-fail
vp-response-miss · vp-response-body-read-fail
vp-response-input-build-fail · vp-response-lib-missing
vp-response-input-build-fail · vp-response-lib-missing
vp-response-process-throw · vp-response-write-fail
Einschränkungen & defensiver Geltungsbereich. Der CodeB-Verifier ist Relying Party, weder eine notifizierte nationale Wallet noch ein qualifizierter Vertrauensdiensteanbieter. Die Protokoll-Substanz ist live und verifizierbar (Proof of Work); High-Assurance-Identitätsprüfung wartet auf iter‑2-Härtung (Issuer-Signaturkette gegen die EU-Liste vertrauenswürdiger Listen, OAuth-Status-List-Widerruf, Wallet-Attestation-Konformität).

Verwandte Dokumente. EU-Wallet-Proof of Work · /oidc.ashx (OIDC-IdP) API · Feature-Überblick · Live-Flow ausprobieren · API-Hub

Veröffentlicht 2026‑06‑09 · CodeB Sovereign Communications, gebaut von Aloaha Limited.