Relying-Party-Integration

Anmeldung mit der EU Digital Identity Wallet für Ihre Website oder App.

Leiten Sie Ihre Nutzer mit einem zusätzlichen OpenID-Connect-Parameter zu CodeB: acr_values=urn:codeb:acr:eudi-wallet. CodeB fordert von der EU Digital Identity Wallet des Nutzers eine Präsentation an, prüft sie und liefert Ihrer Anwendung ein signiertes id_token. Ihr Code berührt das Wallet-Protokoll nicht: Jede OpenID-Connect-Bibliothek funktioniert, und die Beispiele unten sind für Node.js, Python, PHP, Java, Go und ASP.NET Core getestet.

Zwei Regeln. 1. Ergänzen Sie die Autorisierungsanfrage um acr_values=urn:codeb:acr:eudi-wallet: Der Nutzer landet direkt bei der Wallet-Anmeldung. 2. Prüfen Sie vor der Freigabe, ob der acr-Claim im validierten id_token urn:codeb:acr:eudi-wallet oder urn:codeb:acr:eudi-wallet-linked ist. Jedes Beispiel macht beides.

So funktioniert es

  1. Ihre App leitet zu CodeB weiter. Der Browser ruft den CodeB-Autorisierungsendpunkt mit client_id, PKCE (S256), state, nonce und acr_values=urn:codeb:acr:eudi-wallet auf.
  2. CodeB öffnet die Wallet-Anmeldung. Auf dem Smartphone öffnet sich die Wallet direkt; am Desktop scannt der Nutzer einen QR-Code mit dem Smartphone, auf dem die Wallet liegt.
  3. Die Wallet präsentiert, CodeB prüft. CodeB prüft die Präsentation per OpenID for Verifiable Presentations: Aussteller-Signatur und Vertrauenskette, Holder-Binding und, sofern der Nachweis darauf verweist, den Widerrufsstatus.
  4. CodeB liefert einen Autorisierungscode. Der Browser kehrt mit einem Code und Ihrem state zu Ihrer Redirect-URI zurück.
  5. Ihr Server tauscht den Code. Ihr Backend tauscht den Code gegen Tokens, validiert das id_token und prüft den acr-Claim. Der Nutzer ist angemeldet.

Was Sie brauchen

Getestete Beispiele

Jedes Beispiel ist eine vollständige, lauffähige Anwendung mit drei Routen: /, der Login-Route und /callback. Die Konfiguration kommt aus Umgebungsvariablen (CODEB_ISSUER, CODEB_CLIENT_ID, CODEB_CLIENT_SECRET, APP_BASE_URL).

StackBibliothekRezeptDownload
Node.js 18+Express + openid-client 5Öffnenwallet-signin-node.js
Python 3Flask + AuthlibÖffnenwallet-signin-python.py
PHP 8jumbojett/openid-connect-phpÖffnenwallet-signin-php.php
Java 17+Spring Boot 3 / Spring Security 6ÖffnenWalletSignInApplication.java
Gogo-oidc v3 + x/oauth2Öffnenwallet-signin-go.go
ASP.NET Core 8Microsoft.AspNetCore.Authentication.OpenIdConnectÖffnenProgram.cs
Visual Basic .NETASP.NET Core 8 + Microsoft.AspNetCore.Authentication.OpenIdConnectÖffnenProgram.vb

So testen wir: Jedes Beispiel läuft in unserer Testumgebung gegen einen lokalen OpenID-Provider, der die CodeB-Endpunkte und Token-Inhalte nachbildet, zweimal: einmal mit Wallet-Anmeldung, die akzeptiert werden muss, und einmal mit Passwort-Anmeldung, die mit HTTP 403 abgelehnt werden muss. Außerdem prüft die Testumgebung, dass acr_values, PKCE S256, state und nonce gesendet werden und ein in einem anderen Browser gestarteter Callback abgelehnt wird.

Node.js 18+ Express + openid-client 5

Login-Route: /login. Download wallet-signin-node.js (speichern als wallet-signin-node.js)

// Sign in with the EU Digital Identity Wallet - Node.js (Express + openid-client 5) // CodeB acts as the verifier: it asks the user's wallet for a presentation, // verifies it, and hands your app a signed OpenID Connect id_token. // // npm install express@5 express-session@1 openid-client@5 // SESSION_SECRET=<random> CODEB_CLIENT_ID=... CODEB_CLIENT_SECRET=... node wallet-signin-node.js // // Register https://<your-app>/callback as redirect URI for your client first. const express = require('express'); const session = require('express-session'); const { Issuer, generators } = require('openid-client'); const ISSUER = process.env.CODEB_ISSUER || 'https://phone.aloaha.com'; const CLIENT_ID = process.env.CODEB_CLIENT_ID || '<YOUR_CLIENT_ID>'; const CLIENT_SECRET = process.env.CODEB_CLIENT_SECRET || '<YOUR_CLIENT_SECRET>'; const APP_BASE_URL = process.env.APP_BASE_URL || 'http://localhost:3000'; const PORT = Number(process.env.PORT || 3000); const SESSION_SECRET = process.env.SESSION_SECRET; if (!SESSION_SECRET) throw new Error('Set SESSION_SECRET to a long random value'); // The acr values CodeB sets after an EU Digital Identity Wallet sign-in. const WALLET_ACRS = ['urn:codeb:acr:eudi-wallet', 'urn:codeb:acr:eudi-wallet-linked']; const app = express(); app.use(session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false })); let client; async function getClient() { if (!client) { const issuer = await Issuer.discover(ISSUER); client = new issuer.Client({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, redirect_uris: [APP_BASE_URL + '/callback'], response_types: ['code'], }); } return client; } app.get('/', (req, res) => { if (req.session.user) return res.send('Signed in as ' + req.session.user.sub + ' (acr=' + req.session.user.acr + ')'); res.send('<a href="/login">Sign in with your EU Digital Identity Wallet</a>'); }); app.get('/login', async (req, res, next) => { try { const c = await getClient(); const 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], // go straight to the wallet sign-in 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 getClient(); const o = req.session.oidc || {}; // Checks state, nonce, signature, issuer, audience and expiry of the id_token. const tokens = await c.callback(APP_BASE_URL + '/callback', c.callbackParams(req), { code_verifier: o.verifier, state: o.state, nonce: o.nonce }); const claims = tokens.claims(); if (!WALLET_ACRS.includes(claims.acr)) { return res.status(403).send('This page needs a sign-in with the EU Digital Identity Wallet (acr=' + claims.acr + ').'); } // New session id after sign-in (no session fixation). req.session.regenerate((err) => { if (err) return next(err); req.session.user = { sub: claims.sub, acr: claims.acr }; res.redirect('/'); }); } catch (e) { next(e); } }); app.listen(PORT, () => console.log('listening on ' + APP_BASE_URL));

Python 3 Flask + Authlib

Login-Route: /login. Download wallet-signin-python.py (speichern als wallet-signin-python.py)

# Sign in with the EU Digital Identity Wallet - Python (Flask + Authlib) # CodeB acts as the verifier: it asks the user's wallet for a presentation, # verifies it, and hands your app a signed OpenID Connect id_token. # # pip install flask authlib requests # SESSION_SECRET=<random> CODEB_CLIENT_ID=... CODEB_CLIENT_SECRET=... python wallet-signin-python.py # # Register https://<your-app>/callback as redirect URI for your client first. import os from flask import Flask, redirect, session, url_for from authlib.integrations.flask_client import OAuth ISSUER = os.environ.get("CODEB_ISSUER", "https://phone.aloaha.com") APP_BASE_URL = os.environ.get("APP_BASE_URL", "http://localhost:3000") WALLET_ACRS = ("urn:codeb:acr:eudi-wallet", "urn:codeb:acr:eudi-wallet-linked") app = Flask(__name__) app.secret_key = os.environ["SESSION_SECRET"] # long random value; signs the session cookie oauth = OAuth(app) oauth.register( "codeb", client_id=os.environ.get("CODEB_CLIENT_ID", "<YOUR_CLIENT_ID>"), client_secret=os.environ.get("CODEB_CLIENT_SECRET", "<YOUR_CLIENT_SECRET>"), server_metadata_url=ISSUER + "/.well-known/openid-configuration", client_kwargs={"scope": "openid profile email", "code_challenge_method": "S256"}, ) @app.route("/") def home(): u = session.get("user") if u: return "Signed in as %s (acr=%s)" % (u["sub"], u["acr"]) return '<a href="/login">Sign in with your EU Digital Identity Wallet</a>' @app.route("/login") def login(): # acr_values sends the user straight to the wallet sign-in. return oauth.codeb.authorize_redirect(APP_BASE_URL + "/callback", acr_values=WALLET_ACRS[0]) @app.route("/callback") def callback(): # Checks state, nonce, signature, issuer, audience and expiry of the id_token. token = oauth.codeb.authorize_access_token() claims = token["userinfo"] # the validated id_token claims if claims.get("acr") not in WALLET_ACRS: return "This page needs a sign-in with the EU Digital Identity Wallet (acr=%s)." % claims.get("acr"), 403 session.clear() session["user"] = {"sub": claims["sub"], "acr": claims["acr"]} return redirect(url_for("home")) if __name__ == "__main__": app.run(port=int(os.environ.get("PORT", "3000")))

PHP 8 jumbojett/openid-connect-php

Login-Route: /login. Download wallet-signin-php.php (speichern als wallet-signin-php.php)

<?php // Sign in with the EU Digital Identity Wallet - PHP 8 (jumbojett/openid-connect-php) // CodeB acts as the verifier: it asks the user's wallet for a presentation, // verifies it, and hands your app a signed OpenID Connect id_token. // // composer require jumbojett/openid-connect-php:^1.0 // CODEB_CLIENT_ID=... CODEB_CLIENT_SECRET=... php -S localhost:3000 wallet-signin-php.php // // Register https://<your-app>/callback as redirect URI for your client first. require __DIR__ . '/vendor/autoload.php'; use Jumbojett\OpenIDConnectClient; $issuer = getenv('CODEB_ISSUER') ?: 'https://phone.aloaha.com'; $appBaseUrl = getenv('APP_BASE_URL') ?: 'http://localhost:3000'; $walletAcrs = ['urn:codeb:acr:eudi-wallet', 'urn:codeb:acr:eudi-wallet-linked']; session_start(); $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); if ($path === '/login' || $path === '/callback') { $oidc = new OpenIDConnectClient( $issuer, getenv('CODEB_CLIENT_ID') ?: '<YOUR_CLIENT_ID>', getenv('CODEB_CLIENT_SECRET') ?: '<YOUR_CLIENT_SECRET>' ); $oidc->setRedirectURL($appBaseUrl . '/callback'); $oidc->addScope(['openid', 'profile', 'email']); $oidc->setCodeChallengeMethod('S256'); $oidc->addAuthParam(['acr_values' => $walletAcrs[0]]); // straight to the wallet sign-in // First call redirects to CodeB; on /callback it exchanges the code and checks // state, nonce, signature, issuer, audience and expiry of the id_token. try { $oidc->authenticate(); } catch (\Jumbojett\OpenIDConnectClientException $e) { http_response_code(400); // wrong state, expired code, invalid token ... echo 'Sign-in failed: ' . htmlspecialchars($e->getMessage()); exit; } $acr = $oidc->getVerifiedClaims('acr'); if (!in_array($acr, $walletAcrs, true)) { http_response_code(403); echo 'This page needs a sign-in with the EU Digital Identity Wallet (acr=' . htmlspecialchars((string)$acr) . ').'; exit; } session_regenerate_id(true); $_SESSION['user'] = ['sub' => $oidc->getVerifiedClaims('sub'), 'acr' => $acr]; header('Location: /'); exit; } if (!empty($_SESSION['user'])) { echo 'Signed in as ' . htmlspecialchars($_SESSION['user']['sub']) . ' (acr=' . htmlspecialchars($_SESSION['user']['acr']) . ')'; } else { echo '<a href="/login">Sign in with your EU Digital Identity Wallet</a>'; }

Java 17+ Spring Boot 3 / Spring Security 6

Login-Route: /oauth2/authorization/codeb. Download WalletSignInApplication.java (speichern als WalletSignInApplication.java)

// Sign in with the EU Digital Identity Wallet - Java 17+ (Spring Boot 3, Spring Security 6) // CodeB acts as the verifier: it asks the user's wallet for a presentation, // verifies it, and hands your app a signed OpenID Connect id_token. // // Dependencies (Maven, Spring Boot 3.3 parent): // spring-boot-starter-web, spring-boot-starter-oauth2-client // Save as src/main/java/demo/WalletSignInApplication.java, then // CODEB_CLIENT_ID=... CODEB_CLIENT_SECRET=... mvn spring-boot:run // // Register https://<your-app>/callback as redirect URI for your client first. package demo; import java.util.List; import java.util.Map; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.ClientRegistrations; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @RestController public class WalletSignInApplication { static final List<String> WALLET_ACRS = List.of("urn:codeb:acr:eudi-wallet", "urn:codeb:acr:eudi-wallet-linked"); static String env(String k, String d) { String v = System.getenv(k); return v == null || v.isEmpty() ? d : v; } public static void main(String[] args) { System.setProperty("server.port", env("PORT", "3000")); SpringApplication.run(WalletSignInApplication.class, args); } @Bean ClientRegistrationRepository clients() { ClientRegistration codeb = ClientRegistrations .fromIssuerLocation(env("CODEB_ISSUER", "https://phone.aloaha.com")) .registrationId("codeb") .clientId(env("CODEB_CLIENT_ID", "<YOUR_CLIENT_ID>")) .clientSecret(env("CODEB_CLIENT_SECRET", "<YOUR_CLIENT_SECRET>")) .redirectUri("{baseUrl}/callback") .scope("openid", "profile", "email") .build(); return new InMemoryClientRegistrationRepository(codeb); } // After Spring has validated the id_token (signature, issuer, audience, nonce, expiry): require the wallet acr. @Bean OAuth2UserService<OidcUserRequest, OidcUser> walletOnly() { OidcUserService delegate = new OidcUserService(); return request -> { OidcUser user = delegate.loadUser(request); String acr = user.getIdToken().getClaimAsString("acr"); if (!WALLET_ACRS.contains(acr)) { throw new OAuth2AuthenticationException(new OAuth2Error("insufficient_user_authentication", "This page needs a sign-in with the EU Digital Identity Wallet (acr=" + acr + ").", null)); } return user; }; } @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository repo) throws Exception { DefaultOAuth2AuthorizationRequestResolver resolver = new DefaultOAuth2AuthorizationRequestResolver(repo, "/oauth2/authorization"); resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce() .andThen(b -> b.additionalParameters(p -> p.put("acr_values", WALLET_ACRS.get(0))))); // straight to the wallet sign-in http.authorizeHttpRequests(a -> a.requestMatchers("/", "/error").permitAll().anyRequest().authenticated()) .oauth2Login(o -> o .authorizationEndpoint(e -> e.authorizationRequestResolver(resolver)) .redirectionEndpoint(r -> r.baseUri("/callback")) .defaultSuccessUrl("/", true) .failureHandler((req, res, ex) -> res.sendError(403, ex.getMessage()))); return http.build(); } @GetMapping("/") String home(@AuthenticationPrincipal OidcUser user) { if (user == null) return "<a href=\"/oauth2/authorization/codeb\">Sign in with your EU Digital Identity Wallet</a>"; return "Signed in as " + user.getSubject() + " (acr=" + user.getIdToken().getClaimAsString("acr") + ")"; } }

Go go-oidc v3 + x/oauth2

Login-Route: /login. Download wallet-signin-go.go (speichern als wallet-signin-go.go)

// Sign in with the EU Digital Identity Wallet - Go (go-oidc v3 + x/oauth2) // CodeB acts as the verifier: it asks the user's wallet for a presentation, // verifies it, and hands your app a signed OpenID Connect id_token. // // go get github.com/coreos/go-oidc/v3/oidc golang.org/x/oauth2 // CODEB_CLIENT_ID=... CODEB_CLIENT_SECRET=... go run wallet-signin-go.go // // Register https://<your-app>/callback as redirect URI for your client first. // Sessions are kept in memory to keep the sample short; use your own session store. package main import ( "context" "crypto/rand" "encoding/base64" "fmt" "html" "log" "net/http" "os" "sync" "time" "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" ) var walletAcrs = map[string]bool{"urn:codeb:acr:eudi-wallet": true, "urn:codeb:acr:eudi-wallet-linked": true} type pending struct { verifier, nonce string created time.Time } var ( mu sync.Mutex inflight = map[string]pending{} // state -> PKCE verifier + nonce (10 minutes) users = map[string]string{} // session id -> "sub (acr)" ) func env(k, d string) string { if v := os.Getenv(k); v != "" { return v } return d } func random() string { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { panic(err) } return base64.RawURLEncoding.EncodeToString(b) } func main() { ctx := context.Background() appBase := env("APP_BASE_URL", "http://localhost:3000") provider, err := oidc.NewProvider(ctx, env("CODEB_ISSUER", "https://phone.aloaha.com")) if err != nil { log.Fatal(err) } clientID := env("CODEB_CLIENT_ID", "<YOUR_CLIENT_ID>") conf := oauth2.Config{ ClientID: clientID, ClientSecret: env("CODEB_CLIENT_SECRET", "<YOUR_CLIENT_SECRET>"), Endpoint: provider.Endpoint(), RedirectURL: appBase + "/callback", Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, } verifier := provider.Verifier(&oidc.Config{ClientID: clientID}) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if c, err := r.Cookie("sid"); err == nil { mu.Lock() u, ok := users[c.Value] mu.Unlock() if ok { fmt.Fprint(w, "Signed in as "+html.EscapeString(u)) return } } fmt.Fprint(w, `<a href="/login">Sign in with your EU Digital Identity Wallet</a>`) }) http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { state, nonce, pkce := random(), random(), oauth2.GenerateVerifier() mu.Lock() for k, v := range inflight { // drop abandoned sign-ins if time.Since(v.created) > 10*time.Minute { delete(inflight, k) } } inflight[state] = pending{pkce, nonce, time.Now()} mu.Unlock() // Bind the state to this browser: the callback only accepts the state from its own cookie. http.SetCookie(w, &http.Cookie{Name: "oidc_state", Value: state, Path: "/callback", MaxAge: 600, HttpOnly: true, SameSite: http.SameSiteLaxMode}) http.Redirect(w, r, conf.AuthCodeURL(state, oidc.Nonce(nonce), oauth2.S256ChallengeOption(pkce), oauth2.SetAuthURLParam("acr_values", "urn:codeb:acr:eudi-wallet"), // straight to the wallet sign-in ), http.StatusFound) }) http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { state := r.URL.Query().Get("state") c, err := r.Cookie("oidc_state") if err != nil || c.Value != state { http.Error(w, "state does not belong to this browser", http.StatusBadRequest) return } http.SetCookie(w, &http.Cookie{Name: "oidc_state", Value: "", Path: "/callback", MaxAge: -1}) mu.Lock() p, ok := inflight[state] delete(inflight, state) mu.Unlock() if !ok || time.Since(p.created) > 10*time.Minute { http.Error(w, "unknown state", http.StatusBadRequest) return } tok, err := conf.Exchange(r.Context(), r.URL.Query().Get("code"), oauth2.VerifierOption(p.verifier)) if err != nil { http.Error(w, "token exchange failed", http.StatusBadGateway) return } raw, _ := tok.Extra("id_token").(string) idt, err := verifier.Verify(r.Context(), raw) // signature, issuer, audience, expiry if err != nil || idt.Nonce != p.nonce { http.Error(w, "invalid id_token", http.StatusUnauthorized) return } var claims struct { Acr string `json:"acr"` } if err := idt.Claims(&claims); err != nil || !walletAcrs[claims.Acr] { http.Error(w, "This page needs a sign-in with the EU Digital Identity Wallet (acr="+claims.Acr+").", http.StatusForbidden) return } sid := random() mu.Lock() users[sid] = idt.Subject + " (acr=" + claims.Acr + ")" mu.Unlock() http.SetCookie(w, &http.Cookie{Name: "sid", Value: sid, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode}) http.Redirect(w, r, "/", http.StatusFound) }) log.Fatal(http.ListenAndServe(":"+env("PORT", "3000"), nil)) }

ASP.NET Core 8 Microsoft.AspNetCore.Authentication.OpenIdConnect

Login-Route: /login. Download Program.cs (speichern als Program.cs)

// Sign in with the EU Digital Identity Wallet - ASP.NET Core 8 (Program.cs) // CodeB acts as the verifier: it asks the user's wallet for a presentation, // verifies it, and hands your app a signed OpenID Connect id_token. // // dotnet new web -n WalletSignIn && cd WalletSignIn // dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect --version 8.0.* // (replace Program.cs with this file) // CODEB_CLIENT_ID=... CODEB_CLIENT_SECRET=... dotnet run // // Register https://<your-app>/callback as redirect URI for your client first. using System.Security.Claims; 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" }; string Env(string k, string d) => Environment.GetEnvironmentVariable(k) is { Length: > 0 } v ? v : d; var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication(o => { o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie() .AddOpenIdConnect(o => { o.Authority = Env("CODEB_ISSUER", "https://phone.aloaha.com"); o.ClientId = Env("CODEB_CLIENT_ID", "<YOUR_CLIENT_ID>"); o.ClientSecret = Env("CODEB_CLIENT_SECRET", "<YOUR_CLIENT_SECRET>"); o.CallbackPath = "/callback"; o.ResponseType = "code"; // authorization code flow, PKCE is on by default o.Scope.Clear(); o.Scope.Add("openid"); o.Scope.Add("profile"); o.Scope.Add("email"); o.MapInboundClaims = false; // keep the original claim names (sub, acr) o.ClaimActions.Remove("acr"); // the handler drops acr from the sign-in cookie by default; keep it o.Events = new OpenIdConnectEvents { // straight to the wallet sign-in OnRedirectToIdentityProvider = ctx => { ctx.ProtocolMessage.AcrValues = walletAcrs[0]; return Task.CompletedTask; }, // the id_token is already validated (signature, issuer, audience, nonce, expiry); now require the wallet acr OnTokenValidated = ctx => { var acr = ctx.Principal?.FindFirst("acr")?.Value; if (acr is null || Array.IndexOf(walletAcrs, acr) < 0) { ctx.HandleResponse(); ctx.Response.StatusCode = 403; return ctx.Response.WriteAsync("This page needs a sign-in with the EU Digital Identity Wallet (acr=" + acr + ")."); } return Task.CompletedTask; }, }; }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapGet("/", (ClaimsPrincipal user) => user.Identity?.IsAuthenticated == true ? Results.Text("Signed in as " + user.FindFirst("sub")?.Value + " (acr=" + user.FindFirst("acr")?.Value + ")") : Results.Content("<a href=\"/login\">Sign in with your EU Digital Identity Wallet</a>", "text/html")); app.MapGet("/login", () => Results.Challenge(new AuthenticationProperties { RedirectUri = "/" }, new[] { OpenIdConnectDefaults.AuthenticationScheme })); app.Run();

Visual Basic .NET ASP.NET Core 8 + Microsoft.AspNetCore.Authentication.OpenIdConnect

Login-Route: /login. Download Program.vb (speichern als Program.vb) Derselbe Ablauf wie im C#-Beispiel, für Visual-Basic-Teams: ein Konsolenprojekt, umgestellt auf das Web-SDK.

' Sign in with the EU Digital Identity Wallet - ASP.NET Core 8 in Visual Basic (Program.vb) ' CodeB acts as the verifier: it asks the user's wallet for a presentation, ' verifies it, and hands your app a signed OpenID Connect id_token. ' ' dotnet new console -lang VB -n WalletSignIn -f net8.0 && cd WalletSignIn ' (in WalletSignIn.vbproj set Sdk="Microsoft.NET.Sdk.Web") ' dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect --version 8.0.* ' (replace Program.vb with this file) ' CODEB_CLIENT_ID=... CODEB_CLIENT_SECRET=... dotnet run ' ' Register https://<your-app>/callback as redirect URI for your client first. Imports System.Security.Claims Imports Microsoft.AspNetCore.Authentication Imports Microsoft.AspNetCore.Authentication.Cookies Imports Microsoft.AspNetCore.Authentication.OpenIdConnect Imports Microsoft.AspNetCore.Builder Imports Microsoft.AspNetCore.Http Imports Microsoft.Extensions.DependencyInjection Module Program ReadOnly WalletAcrs As String() = {"urn:codeb:acr:eudi-wallet", "urn:codeb:acr:eudi-wallet-linked"} Function Env(key As String, fallback As String) As String Dim v = Environment.GetEnvironmentVariable(key) Return If(String.IsNullOrEmpty(v), fallback, v) End Function Sub Main(args As String()) Dim builder = WebApplication.CreateBuilder(args) builder.Services.AddAuthentication( Sub(o) o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme End Sub). AddCookie(). AddOpenIdConnect( Sub(o) o.Authority = Env("CODEB_ISSUER", "https://phone.aloaha.com") o.ClientId = Env("CODEB_CLIENT_ID", "<YOUR_CLIENT_ID>") o.ClientSecret = Env("CODEB_CLIENT_SECRET", "<YOUR_CLIENT_SECRET>") o.CallbackPath = "/callback" o.ResponseType = "code" ' authorization code flow, PKCE is on by default o.Scope.Clear() o.Scope.Add("openid") : o.Scope.Add("profile") : o.Scope.Add("email") o.MapInboundClaims = False ' keep the original claim names (sub, acr) o.ClaimActions.Remove("acr") ' the handler drops acr from the sign-in cookie by default; keep it o.Events = New OpenIdConnectEvents With { .OnRedirectToIdentityProvider = Function(ctx) ' straight to the wallet sign-in ctx.ProtocolMessage.AcrValues = WalletAcrs(0) Return Task.CompletedTask End Function, .OnTokenValidated = Function(ctx) ' the id_token is already validated (signature, issuer, audience, nonce, expiry); now require the wallet acr Dim acr = ctx.Principal?.FindFirst("acr")?.Value If acr Is Nothing OrElse Array.IndexOf(WalletAcrs, acr) < 0 Then ctx.HandleResponse() ctx.Response.StatusCode = 403 Return ctx.Response.WriteAsync("This page needs a sign-in with the EU Digital Identity Wallet (acr=" & acr & ").") End If Return Task.CompletedTask End Function } End Sub) builder.Services.AddAuthorization() Dim app = builder.Build() app.UseAuthentication() app.UseAuthorization() app.MapGet("/", Function(user As ClaimsPrincipal) As IResult If user.Identity IsNot Nothing AndAlso user.Identity.IsAuthenticated Then Return Results.Text("Signed in as " & user.FindFirst("sub")?.Value & " (acr=" & user.FindFirst("acr")?.Value & ")") End If Return Results.Content("<a href=""/login"">Sign in with your EU Digital Identity Wallet</a>", "text/html") End Function) app.MapGet("/login", Function() Results.Challenge(New AuthenticationProperties With {.RedirectUri = "/"}, New List(Of String) From {OpenIdConnectDefaults.AuthenticationScheme})) app.Run() End Sub End Module

Was Ihre App erhält

Das id_token ist mit RS256 signiert; die Schlüssel liegen unter /.well-known/jwks.json. Nach einer Wallet-Anmeldung enthält es:

acrurn:codeb:acr:eudi-wallet: Der Nutzer hat sich mit einer Wallet-Präsentation angemeldet, die CodeB gegen die Aussteller geprüft hat, denen dieser Mandant vertraut. urn:codeb:acr:eudi-wallet-linked: dasselbe mit einer Wallet, die der Nutzer mit einem bestehenden CodeB-Konto verknüpft hat, abgeschlossen auf demselben Gerät.
amr["vc"]: Der Nutzer hat sich mit einem verifizierbaren Nachweis angemeldet.
subKennung des CodeB-Nutzers. Eine mit einem CodeB-Konto verknüpfte Wallet meldet immer dieses Konto an; sonst ermittelt CodeB den Nutzer aus den Attributen, die die Wallet offenlegt.
iss, aud, exp, iat, auth_time, nonceStandard-OpenID-Connect-Claims. Ihre Bibliothek validiert sie.
role, groups, preferred_usernameMit dem Scope profile (oder groups).

Profildaten liefert der Userinfo-Endpunkt (Aufruf mit dem Access-Token):

name, given_name, family_name, birthdateAus dem CodeB-Konto des Nutzers. Leere Profilfelder werden bei der Anmeldung aus der PID der Wallet befüllt. Claims ohne Wert lässt CodeB weg.
email, email_verifiedNur wenn das Konto eine Adresse hat. Verlassen Sie sich nur darauf, wenn email_verified true ist.
vc und weitere Wallet-AttributeNur die Attribute, die für Ihren Client freigeschaltet sind (Allowlist pro Client). Standardmäßig wird kein Wallet-Attribut weitergegeben.

Welches PID-Attribut zu welchem Claim wird (dieselben Namen für jede PID nach ARF 3.0): Claims, die Ihre App erhält.

Derselbe Ablauf als reine HTTP-Anfragen

Für Stacks ohne OpenID-Connect-Bibliothek. Platzhalter ersetzen; code_challenge ist der base64url-kodierte SHA-256 Ihres code_verifier.

# 1. Send the browser to the authorization endpoint https://phone.aloaha.com/oauth2/v1/authorize?response_type=code &client_id=<YOUR_CLIENT_ID> &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback &scope=openid%20profile%20email &state=<RANDOM>&nonce=<RANDOM> &code_challenge=<BASE64URL_SHA256_OF_VERIFIER>&code_challenge_method=S256 &acr_values=urn%3Acodeb%3Aacr%3Aeudi-wallet # 2. On https://app.example.com/callback?code=...&state=... exchange the code curl -s https://phone.aloaha.com/oauth2/v1/token \ -u '<YOUR_CLIENT_ID>:<YOUR_CLIENT_SECRET>' \ -d grant_type=authorization_code \ -d code='<CODE>' \ -d redirect_uri=https://app.example.com/callback \ -d code_verifier='<VERIFIER>' # 3. Validate the id_token (RS256, keys at /.well-known/jwks.json): # iss, aud, exp, nonce - then require acr = urn:codeb:acr:eudi-wallet(-linked)

Plattformen ohne eigenen Code

WordPress, Nextcloud und Identity-Broker wie Keycloak oder Microsoft Entra ID binden CodeB als normalen OpenID-Connect-Provider an:

Wenn das Plugin oder der Broker zusätzliche Autorisierungsparameter erlaubt, ergänzen Sie acr_values=urn:codeb:acr:eudi-wallet und übernehmen den acr-Claim. Andernfalls wählt der Nutzer auf der CodeB-Anmeldeseite "EU Wallet".

Eingebettetes Widget

signin-widget.js läuft auf Seiten, die ein CodeB-Mandant ausliefert, zum Beispiel in der Widget-Demo. Auf Ihrer eigenen Domain nutzen Sie den OpenID-Connect-Ablauf dieser Seite: Er braucht keine Cross-Origin-Aufrufe, und Ihr Server erhält ein signiertes Token statt eines Browser-Callbacks.

Testen

Auf der Verifier-Testseite probieren Sie eine Wallet-Präsentation gegen CodeB aus, auf der EU-Wallet-Anmeldeseite sehen Sie, was Ihre Nutzer sehen. Das vollständige Verifier-Protokoll steht in der EU-Wallet-Verifier-API.

Häufige Fragen

Muss ich OpenID for Verifiable Presentations (OpenID4VP) selbst implementieren?

Nein. CodeB ist der Verifier: CodeB spricht mit der Wallet, prüft die Präsentation und gibt Ihrer Anwendung ein Standard-OpenID-Connect-id_token. Ihr Code braucht nur einen OpenID-Connect-Client.

Wie stelle ich sicher, dass sich der Nutzer wirklich mit der EU Digital Identity Wallet angemeldet hat?

Senden Sie acr_values=urn:codeb:acr:eudi-wallet in der Autorisierungsanfrage und akzeptieren Sie die Anmeldung nach der Validierung des id_token nur, wenn der acr-Claim urn:codeb:acr:eudi-wallet oder urn:codeb:acr:eudi-wallet-linked ist.

Welche Programmiersprachen werden unterstützt?

Jede Sprache mit einer OpenID-Connect-Bibliothek. Diese Seite enthält getestete Beispiele für Node.js, Python, PHP, Java (Spring Boot), Go, ASP.NET Core (C#) und Visual Basic .NET sowie die reinen HTTP-Anfragen.

Welche Wallets funktionieren?

Wallets, die eine PID per OpenID for Verifiable Presentations im Format SD-JWT VC oder ISO mdoc präsentieren. Der Verifier prüft die Vertrauenskette des Ausstellers, das Holder-Binding und, sofern der Nachweis darauf verweist, den Widerrufsstatus.

Was erhält meine Anwendung?

Ein signiertes id_token mit sub, acr und amr (mit dem Scope profile zusätzlich role, groups und preferred_username). Name und E-Mail-Adresse liefert der Userinfo-Endpunkt.

Kann ich das einbettbare JavaScript-Widget auf meiner eigenen Domain nutzen?

Nein. Das Widget läuft auf Seiten, die ein CodeB-Mandant ausliefert. Für Ihre eigene Website oder App nutzen Sie den hier beschriebenen OpenID-Connect-Ablauf.

Liefert dieser Ablauf age_over_18?

Nein. Die Anmeldung fordert von der Wallet nur die Attribute an, die für die Anmeldung nötig sind; age_over_18 gehört nicht dazu.

Was passiert, wenn der Nutzer keine Wallet hat?

Dann kann er die Wallet-Anmeldung nicht abschließen. Bieten Sie Ihre gewohnte Anmeldung als Alternative an oder weisen Sie darauf hin, dass eine Wallet nötig ist.

Diese Seite ist nicht von der Europäischen Kommission autorisiert oder unterstützt.