Ktieb tar-riċetti: kif bnejna ħanut online b’dħul, verifika tal-età u firma bil-wallet Ewropew
Il-ħanut demo tagħna huwa paġna web statika waħda – l-ebda software ta’ ħanut, l-ebda server tiegħu. Il-klijenti jirreġistraw u jidħlu bid-data tal-identità personali (PID) fil-wallet Ewropew tagħhom, jgħaddu minn verifika tal-età u jiffirmaw il-konferma tal-ordni f’CodeB Wallet. Dan l-artiklu juri kif kollox jaqbel flimkien, bil-kodiċi li nużaw, biex tkun tista’ tagħmel l-istess fil-ħanut tiegħek.

X’jagħmel il-ħanut demo
Ipprovah l-ewwel – jieħu żewġ minuti: iftaħ il-ħanut demo. Xejn ma jinbiegħ, xejn ma jitħallas u xejn ma jinħażen fuq server. Il-passi:
- Irreġistra / idħol bil-wallet Ewropew – mingħajr password, mingħajr e-mail ta’ konferma.
- Il-ħanut jirċievi l-isem biss mill-PID – mhux id-data tat-twelid, l-indirizz jew in-numru tal-karta.
- Oġġett 18+ jeħtieġ verifika tal-età: il-wallet iwieġeb biss “aktar minn 18: iva” (simulata fid-demo).
- Wara l-ordni l-klijent jiffirma l-konferma tal-ordni f’CodeB Wallet u jieħu lura l-PDF iffirmat fil-ħanut.
L-ingredjenti
Il-ħanut qatt ma jitkellem direttament ma’ wallet. Juża OpenID Connect sempliċi mal-gateway – il-protokoll wara l-buttuni “Idħol b’…” – u l-gateway jieħu ħsieb il-parti tal-wallet (OpenID4VP, verifika tal-emittent u tal-wallet). Għalhekk ħanut ma għandu bżonn l-ebda librerija tal-wallet.
| Parti | X’tagħmel | Fejn taħdem |
|---|---|---|
| Paġna tal-ħanut | HTML, CSS u madwar 400 linja JavaScript sempliċi – l-ebda framework | Kwalunkwe server web (hawn: myeudi.app) |
| CodeB EUDI Gateway | Fornitur OpenID Connect quddiem il-wallets Ewropej: jistaqsi lill-wallet, jivverifika t-tweġiba, joħroġ it-tokens | phone.codeb.io |
| CodeB Wallet | Iżomm il-PID, jaqsam biss dak li taqbel miegħu, jiffirma PDFs | App Android; għad-dħul ukoll il-wallet tal-web fil-browser |
| Talbiet għall-firma | Jgħaddi PDF lill-wallet u jirċievi l-PDF iffirmat | phone.codeb.io |
Pass 1 – ikseb client OpenID Connect
Il-ħanut huwa client pubbliku: paġna web ma tistax iżżomm sigriet, allura tuża PKCE minflok client secret. Għall-client neħtieġu mingħandek:
Client ID (eż. my-shop), ir-redirect URI(s) – il-paġna fejn jerġa’ lura l-klijent wara l-pass tal-wallet – u l-browser origins (eż. https://shop.example) li jistgħu jsejħu l-endpoints tat-token, userinfo u l-firma minn JavaScript (CORS).
Is-settings li mbagħad juża l-ħanut:
// Settings of the demo shop (CodeB EUDI Gateway on phone.codeb.io)
const CFG = {
clientId: 'my-shop', // your OpenID Connect client
issuer: 'https://phone.codeb.io',
authorize: 'https://phone.codeb.io/oauth2/v1/authorize',
token: 'https://phone.codeb.io/oidc.ashx?action=token',
userinfo: 'https://phone.codeb.io/oidc.ashx?action=userinfo',
acr: 'urn:codeb:acr:eudi-wallet' // "signed in with an EU wallet"
};Trid client għall-ħanut tiegħek jew għal sistema tat-test? Iktbilna – ara Għall-organizzazzjonijiet.
Pass 2 – “Idħol bil-wallet Ewropew tiegħi”
Klikk fuq il-buttuna jibni talba ta’ awtorizzazzjoni OpenID Connect normali b’PKCE u jibgħat il-browser lill-gateway. Hemmhekk il-klijent jiskennja kodiċi QR b’CodeB Wallet, jagħfas Iftaħ CodeB Wallet fuq il-mowbajl, jew juża l-wallet tal-web. acr_values jitlob dħul b’wallet Ewropew.
function b64url(bytes) {
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
const rnd = n => b64url(crypto.getRandomValues(new Uint8Array(n)));
async function loginWithWallet() {
const verifier = rnd(32), state = rnd(16), nonce = rnd(16);
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
sessionStorage.setItem('oidc', JSON.stringify({ verifier, state, nonce }));
const q = new URLSearchParams({
response_type: 'code', client_id: CFG.clientId,
redirect_uri: location.origin + location.pathname,
scope: 'openid profile', state, nonce,
code_challenge: b64url(new Uint8Array(hash)), code_challenge_method: 'S256',
acr_values: CFG.acr, ui_locales: 'en'
});
location.assign(CFG.authorize + '?' + q); // QR code, app button or web wallet: the gateway asks
}Pass 3 – lura fil-ħanut: ibdel il-kodiċi u vverifika l-id_token
Il-gateway jibgħat lill-klijent lura b’?code=…&state=…. Il-ħanut jibdel il-kodiċi mat-tokens u jivverifika l-id_token: l-emittent, id-destinatarju, in-nonce – u acr. Ivverifika acr int stess: acr_values hija xewqa, u l-ħanut jaċċetta biss dħul bil-wallet.
L-id_token jasal direttament mill-endpoint tat-token permezz ta’ TLS; għalhekk OpenID Connect iħalli lill-client joqgħod fuq TLS minflok il-firma tat-token (OpenID Connect Core, 3.1.3.7). Server li jgħaddi l-id_tokens lil ħaddieħor għandu jivverifika l-firma bil-ċwievet minn jwks_uri f’https://phone.codeb.io/.well-known/openid-configuration.
function claims(jwt) { // payload of the id_token (UTF-8 safe)
const b = atob(jwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(new TextDecoder().decode(Uint8Array.from(b, ch => ch.charCodeAt(0))));
}
async function finishLogin() {
const qs = new URLSearchParams(location.search);
const p = JSON.parse(sessionStorage.getItem('oidc') || 'null');
sessionStorage.removeItem('oidc');
if (qs.get('error')) throw new Error(qs.get('error') + ' ' + (qs.get('error_description') || ''));
if (!p || qs.get('state') !== p.state) throw new Error('state does not match');
// Form-encoded POST without other headers = a CORS "simple" request (no preflight).
const r = await fetch(CFG.token, {
method: 'POST', credentials: 'omit',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code', code: qs.get('code'), client_id: CFG.clientId,
redirect_uri: location.origin + location.pathname, code_verifier: p.verifier
})
});
if (!r.ok) throw new Error('token: HTTP ' + r.status + ' ' + await r.text());
const t = await r.json();
const c = claims(t.id_token);
const audOk = c.aud === CFG.clientId || (Array.isArray(c.aud) && c.aud.includes(CFG.clientId));
if (c.iss !== CFG.issuer || !audOk || c.nonce !== p.nonce) throw new Error('id_token');
if (c.acr !== CFG.acr) throw new Error('not signed in with an EU wallet');
return t; // access_token, id_token, expires_in
}Pass 4 – l-isem biss mill-PID
Il-ħanut għandu bżonn isem għall-kunsinna – xejn aktar. Userinfo jagħti l-isem u l-kunjom; eżattament dak li l-wallet wera lill-klijent qabel ma qasmu. Id-data tat-twelid, l-indirizz u n-numru tal-karta qatt ma jaslu għand il-ħanut. L-indirizz tal-kunsinna jiktbu l-klijent, bħal f’kull ħanut.
// No Authorization header: the token goes in the form body, so the browser sends no preflight.
const u = await fetch(CFG.userinfo, {
method: 'POST', credentials: 'omit',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'access_token=' + encodeURIComponent(t.access_token)
}).then(r => r.json());
console.log(u.given_name, u.family_name); // the shop asked for nothing elsePass 5 – il-verifika tal-età
Għal oġġett 18+ il-ħanut jistaqsi mistoqsija waħda: aktar minn 18? Il-wallet iwieġeb iva jew le – mingħajr data tat-twelid, mingħajr isem. Fil-ħanut demo dan il-pass huwa simulat, biex tara x’jara l-klijent u x’jirċievi l-ħanut. Verifika vera ta’ aktar minn 18 bil-wallet tiegħek: playground u verifika tal-età.
Pass 6 – iffirma l-konferma tal-ordni fil-wallet
Il-ħanut joħloq il-konferma tal-ordni bħala PDF fil-browser u jgħaddiha lill-gateway bħala talba għall-firma. It-tweġiba fiha link eudi-rqes://: fuq kompjuter il-ħanut juriha bħala kodiċi QR, fuq il-mowbajl bħala buttuna. CodeB Wallet iniżżel il-PDF, juri min qed jitlob, jiffirma – bis-servizz tal-firma ta’ CodeB jew bil-karta tal-firma tal-klijent – u jibgħat lura l-PDF iffirmat. Il-ħanut jistaqsi għall-istatus u joffri l-PDF iffirmat biex jitniżżel.
L-access token mid-dħul jawtorizza t-talba, allura klijenti li daħlu biss jistgħu joħolqu waħda. status_token iħalli lill-paġna tistaqsi għall-istatus mingħajr l-access token.
const SIGN = 'https://phone.codeb.io/sign-request.ashx';
async function requestSignature(pdf, accessToken) { // pdf: a Blob
const fd = new FormData(); // multipart: also a "simple" request
fd.append('access_token', accessToken);
fd.append('client_id', CFG.clientId);
fd.append('level', 'advanced'); // 'qualified' = signature card only
fd.append('label', 'Order confirmation 4711');
fd.append('return_uri', location.href); // "Back to the website" in the app
fd.append('file', pdf, 'order-4711.pdf');
const r = await fetch(SIGN + '?action=create', { method: 'POST', credentials: 'omit', body: fd });
if (!r.ok) throw new Error('sign-create: HTTP ' + r.status + ' ' + await r.text());
return r.json(); // { id, link: 'eudi-rqes://…', status_token, expires_in }
}
async function waitForSignedPdf(j) {
const q = 'id=' + encodeURIComponent(j.id) + '&t=' + encodeURIComponent(j.status_token);
for (;;) {
await new Promise(ok => setTimeout(ok, 3000));
const s = await fetch(SIGN + '?action=status&' + q, { credentials: 'omit', cache: 'no-store' }).then(r => r.json());
if (s.status === 'signed') return fetch(SIGN + '?action=signed&' + q, { credentials: 'omit' }).then(r => r.blob());
if (['declined', 'expired', 'withdrawn'].includes(s.status)) throw new Error(s.status);
}
}<!-- computer: QR code for the QR scanner in CodeB Wallet; phone: the same link as a button -->
<div id="qr"></div>
<a class="btn" id="open-app">Open CodeB Wallet</a>
<script src="qrcode.js"></script> <!-- any QR encoder; the demo uses a local copy of an MIT-licensed one -->
<script>
const q = qrcode(0, 'M'); q.addData(j.link); q.make();
document.getElementById('qr').innerHTML = q.createSvgTag({ cellSize: 4, margin: 4, scalable: true });
document.getElementById('open-app').href = j.link; // eudi-rqes://?client_id=…&request_uri=…
</script>Pass 7 – post għall-firma
Firma tidher l-aħjar f’kaxxa magħmula għaliha. Għalhekk il-konferma tal-ordni għandha qasam tal-firma vojt – PDF standard, li kull viewer tal-PDF juri. Talba għall-firma tista’ ssemmi l-qasam, u CodeB Wallet jiffirma ġo fih, bl-istampa li jagħżel il-klijent: ir-ritratt mid-dokument tal-identità, firma bl-idejn jew l-isem. Aktar: oqsma tal-firma.
% An empty signature field (ISO 32000-1, 12.7.4.5): a /Sig widget with a box, no value yet
6 0 obj
<< /Type /Annot /Subtype /Widget /FT /Sig /T (Customer_Signature)
/Rect [60 180 300 260] /P 3 0 R /F 4 >>
endobj
% the catalog lists the field ...
1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [6 0 R] >> >> endobj
% ... and the page lists the widget
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842]
/Contents 4 0 R /Resources << ... >> /Annots [6 0 R] >> endobjPass 8 – headers tas-sigurtà u CORS
Il-ħanut demo jaħdem b’Content Security Policy stretta: scripts mis-sit tiegħu biss, sejħiet tan-netwerk lilu nnifsu u lil phone.codeb.io biss.
Is-sejħiet tal-browser lil phone.codeb.io jibqgħu “simple requests” ta’ CORS: POSTs b’formola jew multipart u GETs, l-ebda header Authorization, l-ebda credentials. B’hekk m’hemmx preflight requests u lanqas cookies.
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self';
img-src 'self'; connect-src 'self' https://phone.codeb.io;
base-uri 'self'; form-action 'none'; frame-ancestors 'none'X’tgħallimna fit-triq
- Uri r-raġuni meta xi ħaġa ma taħdimx. Id-demo turi linja teknika bħal
token: HTTP 400 invalid_grant. L-ewwel test veru tagħna falla – il-linja wriet il-kawża f’sekondi. - Tibgħatx l-access token f’header Authorization mill-browser sakemm is-server ma jwieġebx għall-preflight requests: il-browser jimblokka s-sejħa u jgħid biss “failed to fetch”.
- Ivverifika acr fl-id_token. Inkella kull metodu ieħor ta’ dħul tal-gateway jiftaħ ukoll il-ħanut.
- Żomm l-istat f’sessionStorage, mhux f’localStorage. Fuq il-mowbajl il-klijent iħalli l-paġna għall-app u jerġa’ lura bil-link tar-ritorn – il-memorja tal-paġna tkun marret, sessionStorage le, u jispiċċa mat-tab.
- It-test tal-PDF jeħtieġ attenzjoni: il-fonts integrati tal-PDF jafu biss Latin-1 – l-ittri Maltin ħ, ġ, ż u ċ jeħtieġu mapping jew font inkorporat.
Mid-demo għall-produzzjoni
- Agħmel il-bdil tal-kodiċi fuq is-server tiegħek (client kunfidenzjali) u żomm it-tokens 'il barra mill-browser.
- Ivverifika l-firem tal-id_tokens bil-ċwievet tal-gateway (
jwks_uri). - Aħżen il-PDF iffirmat mal-ordni – hija l-prova tiegħek li l-klijent qabel.
- Uża
level=qualifiedfejn tkun meħtieġa firma elettronika kwalifikata (karta tal-firma), inkellaadvanced. - Itlob biss id-data li għandek bżonn – il-wallet juri kull element lill-klijent qabel ma jaqsmu.