Add EU Digital Identity Wallet sign-in to your website or app.
Send your users to CodeB with one extra OpenID Connect parameter, acr_values=urn:codeb:acr:eudi-wallet. CodeB asks the user's EU Digital Identity Wallet for a presentation, verifies it, and returns a signed id_token to your application. Your code never handles the wallet protocol: any OpenID Connect library works, and the samples below are tested for Node.js, Python, PHP, Java, Go and ASP.NET Core (C# and Visual Basic).
New to it? Start with the CodeB EUDI Gateway overview and the compact developer quickstart.
Two rules. 1. Add acr_values=urn:codeb:acr:eudi-wallet to the authorization request: it sends the user straight to the wallet sign-in. 2. Before you grant access, check that the acr claim of the validated id_token is urn:codeb:acr:eudi-wallet or urn:codeb:acr:eudi-wallet-linked. Every sample does both.
How it works
- Your app redirects to CodeB. The browser goes to the CodeB authorization endpoint with your client_id, PKCE (S256), state, nonce and acr_values=urn:codeb:acr:eudi-wallet.
- CodeB opens the wallet sign-in. On a phone the wallet opens directly; on a desktop the user scans a QR code with the phone that holds the wallet.
- The wallet presents, CodeB verifies. CodeB checks the presentation over OpenID for Verifiable Presentations: issuer signature and trust chain, holder binding and, when the credential references one, its revocation status.
- CodeB returns an authorization code. The browser comes back to your redirect URI with a code and your state.
- Your server exchanges the code. Your back end swaps the code for tokens, validates the id_token and checks the acr claim. The user is signed in.
What you need
- A client registered on the CodeB tenant: we register your redirect URI(s) and send you a
client_idand a client secret. Contact us to get one. - The discovery document:
https://phone.aloaha.com/.well-known/openid-configuration. All samples read the endpoints from it. - A server-side component that keeps the client secret. Single-page apps can use a public client with PKCE (see the OIDC sign-in cookbook), but a back end is the safer choice.
- For the button and the QR code on your page: CodeB Wallet buttons, QR code frames and NFC reader sticker (labels in English, German and Maltese, downloadable kit).
Tested samples
Each sample is a complete, runnable application with three routes: /, the login route and /callback. Configuration comes from environment variables (CODEB_ISSUER, CODEB_CLIENT_ID, CODEB_CLIENT_SECRET, APP_BASE_URL).
| Stack | Library | Recipe | Download |
|---|---|---|---|
| Node.js 18+ | Express + openid-client 5 | Open | wallet-signin-node.js |
| Python 3 | Flask + Authlib | Open | wallet-signin-python.py |
| PHP 8 | jumbojett/openid-connect-php | Open | wallet-signin-php.php |
| Java 17+ | Spring Boot 3 / Spring Security 6 | Open | WalletSignInApplication.java |
| Go | go-oidc v3 + x/oauth2 | Open | wallet-signin-go.go |
| ASP.NET Core 8 | Microsoft.AspNetCore.Authentication.OpenIdConnect | Open | Program.cs |
| Visual Basic .NET | ASP.NET Core 8 + Microsoft.AspNetCore.Authentication.OpenIdConnect | Open | Program.vb |
How we test them: every sample runs in our test environment against a local OpenID provider that mirrors the CodeB endpoints and token contents, twice: once with a wallet sign-in, which must be accepted, and once with a password sign-in, which must be refused with HTTP 403. The test also checks that acr_values, PKCE S256, state and nonce are sent, and that a callback started in another browser is refused.
Node.js 18+ Express + openid-client 5
Login route: /login. Download wallet-signin-node.js (saves as 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 (saves as 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 (saves as 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 (saves as 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 (saves as 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 (saves as 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 (saves as Program.vb) The same flow as the C# sample, for Visual Basic teams: a console project switched to the 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 ModuleWhat your app receives
The id_token is signed with RS256; the keys are published at /.well-known/jwks.json. After a wallet sign-in it carries:
| acr | urn:codeb:acr:eudi-wallet: the user signed in with a wallet presentation that CodeB verified against the issuers this tenant trusts. urn:codeb:acr:eudi-wallet-linked: the same, with a wallet the user linked to an existing CodeB account, finished on the same device. |
| amr | ["vc"]: the user authenticated with a verifiable credential. |
| sub | Identifier of the CodeB user. A wallet linked to a CodeB account always signs in as that account; otherwise CodeB derives the user from the attributes the wallet discloses. |
| iss, aud, exp, iat, auth_time, nonce | Standard OpenID Connect claims. Your library validates them. |
| role, groups, preferred_username | With the profile (or groups) scope. |
Profile data comes from the userinfo endpoint (call it with the access token):
| name, given_name, family_name, birthdate | From the user's CodeB account. Empty profile fields are filled from the wallet's PID when the user signs in. Claims CodeB has no value for are left out. |
| email, email_verified | Only when the account has an address. Rely on it only when email_verified is true. |
| vc and other wallet attributes | Only the attributes enabled for your client (per-client allowlist). By default no wallet attribute is passed on. |
Which PID attribute becomes which claim (the same names for every PID that follows ARF 3.0): Claims your app receives.
The same flow as raw HTTP
For stacks without an OpenID Connect library. Replace the placeholders; code_challenge is the base64url SHA-256 of your 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)Platforms without code
WordPress, Nextcloud and identity brokers such as Keycloak or Microsoft Entra ID connect to CodeB as a normal OpenID Connect provider:
- WordPress single sign-on
- Nextcloud
- Keycloak, Auth0, Okta, Microsoft Entra ID, Cognito
- OIDC integration how-to
If the plugin or broker lets you add extra authorization parameters, add acr_values=urn:codeb:acr:eudi-wallet and map the acr claim. Otherwise the user chooses "EU Wallet" on the CodeB sign-in page.
Embedded widget
signin-widget.js runs on pages served by a CodeB tenant, for example the widget demo. On your own domain, use the OpenID Connect flow on this page: it needs no cross-origin calls, and your server receives a signed token rather than a browser callback.
Testing
Use the verifier test page to try a wallet presentation against CodeB, and the EU Wallet sign-in page to see what your users will see. The full verifier protocol is documented in the EU Wallet verifier API.
Frequently asked questions
Do I have to implement OpenID for Verifiable Presentations (OpenID4VP) myself?
No. CodeB is the verifier: it talks to the wallet, checks the presentation and gives your application a standard OpenID Connect id_token. Your code only needs an OpenID Connect client.
How do I make sure the user really signed in with the EU Digital Identity Wallet?
Send acr_values=urn:codeb:acr:eudi-wallet in the authorization request and, after validating the id_token, accept the sign-in only when the acr claim is urn:codeb:acr:eudi-wallet or urn:codeb:acr:eudi-wallet-linked.
Which programming languages are supported?
Any language with an OpenID Connect library. This page has tested samples for Node.js, Python, PHP, Java (Spring Boot), Go, ASP.NET Core (C#) and Visual Basic .NET, plus the raw HTTP requests.
Which wallets work?
Wallets that present a PID over OpenID for Verifiable Presentations in SD-JWT VC or ISO mdoc format. The verifier checks the issuer's trust chain, holder binding and, when the credential references one, its revocation status.
What does my application receive?
A signed id_token with sub, acr and amr (plus role, groups and preferred_username with the profile scope). Name and e-mail come from the userinfo endpoint.
Can I use the embeddable JavaScript widget on my own domain?
No. The widget runs on pages served by a CodeB tenant. For your own website or app, use the OpenID Connect flow described here.
Does this flow return age_over_18?
No. The sign-in asks the wallet only for the attributes needed to sign the user in; age_over_18 is not one of them.
What happens if the user has no wallet?
The user cannot finish the wallet sign-in. Keep your usual sign-in as an alternative, or tell the user that a wallet is needed.
Related
This page is not endorsed by the European Commission.