Turn your tenant's membership card into a Verifiable Credential in the European Digital Identity Wallet.
Members mint their own card from a self-serve page, your tenant signs it, and it lives inside the member's own wallet on their own device — presentable at the door, at renewal, at partner discounts, or for wallet-based sign-in. Single-use offer, tenant-signed, season-length validity, per-tenant VCT allow-list. No third-party dependency. Everything ships out of the box.
- Per-tenant OpenID4VCI 1.0 Final issuer at
/vci.ashxwith its own signing key atApp_Data/<tenant>/vci/(auto-generated on first use). - Default membership VCT
urn:aloaha:vc:tenant-membershipthat projects the signed-in member'stenant+rolefrom the OIDC user record. - Member-facing page at /member-card.html — sign in, click the button, scan the QR with your wallet.
- Standard OID4VCI wire: single-use pre-authorized code offer,
credential_offer_uri+openid-credential-offer://deep-link, HAIP-conformant DPoP + attest_jwt_client_auth at the token endpoint. - SD-JWT VC output format — selective disclosure means the door bouncer can verify "yes, valid member" without seeing the member's date of birth.
1 Endpoints in play
Three endpoints do all the work. All are per-tenant and enforce the tenant host on the URL.
GET/vci.ashx?available-attributes
Bearer <member access_token>. Returns which VCTs the signed-in member is ready to issue (per ComputeReady). Use to render a "not ready" list on the UI when the member's OIDC profile is missing mandatory claims.
POST/vci.ashx?self-offer
Bearer <member access_token>. Body: { vct?, days? }. Server enforces subject = bearer.sub, projects OIDC claims via BuildClaimsForVct, checks the per-tenant allow-list, and returns { offer_id, offer_uri, deep_link, vct, subject, expires_utc }. The deep_link is openid-credential-offer://?credential_offer_uri=…. Rate-limited to one offer per 5 seconds per subject.
GET/vci.ashx?offer=<id>
Anonymous. The wallet fetches this after scanning the QR — standard OpenID4VCI credential-offer JSON with the pre-authorized code. Everything downstream (?token, ?credential, ?notification) is stock OID4VCI.
2 Enabling self-serve for your tenant
Nothing to enable if you are happy with the bare urn:aloaha:vc:tenant-membership credential (issued by default). To add other credential types — PID, verified email, resident address, or a tenant-specific VCT — drop this file into App_Data/<tenant>/vci/self-offer-allowlist.json:
{
"vcts": [
"urn:aloaha:vc:tenant-membership",
"urn:eu.europa.ec.eudi:pid:1",
"urn:eu.europa.ec.eudi:email:1"
]
}
Only VCTs listed here will accept the ?self-offer path from members. The admin-only ?create-offer is unaffected — admins can still mint any VCT the platform catalogue supports. The file is read on every self-offer call, so no restart is required.
urn:aloaha:vc:tenant-membership. Malformed file (bad JSON, unknown VCT) fails closed with a 403 or 400 rather than silently opening the door.3 Populating members' cards
Because ?self-offer pulls credential claims from the member's OIDC user record, whatever your tenant populates on the OIDC user profile ends up on the credential. Typical wiring:
| Credential claim | OIDC source claim | Notes |
|---|---|---|
membership_number | preferred_username or sub | Stable per member. |
given_name / family_name | given_name / family_name | Derived from name if the pair is absent. |
member_since | created_at | Custom claim on your OIDC provider. |
valid_until | membership_valid_until | Custom claim, ISO 8601 date. |
category | role or membership_category | Adult / Youth / Senior / Family etc. |
The default urn:aloaha:vc:tenant-membership emits tenant + role. To emit richer fields you either populate them on the OIDC user record (they flow through automatically once BuildClaimsForVct knows the field) or you register a new VCT with a bespoke claim map — contact your platform integrator for the code path.
4 Season-length validity
Credentials are issued with an expires_utc derived from days on the offer (default is the platform's OfferDefaultDays; max is OfferMaxDays). For a season-length card, pass days explicitly on the client — e.g. 305 for 1 August to 31 May — and the wallet will show the correct expiry on the credential tile.
fetch('/vci.ashx?self-offer', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + accessToken,
'Content-Type': 'application/json' },
body: JSON.stringify({ days: 305 })
});
5 Revocation
Two mechanisms, pick whichever fits your membership lifecycle:
- Non-renewal by expiry — simplest for yearly membership. Issue with
daysmatching your season; the credential dies on its own. - OAuth Status List — long-lived credentials reference the tenant's status list; when a member is expelled or unpaid, flip the bit. Already shipped platform-wide (HAIP GAP 4).
6 Verification at the door
Two patterns, pick whichever suits the setting. Share-QR is the right pattern for physical-presence checks (the clubhouse door, a partner discount, a stall at an event). OID4VP is the right pattern when the wallet-holder is remote and you need cryptographic binding to a fresh nonce.
Pattern A — Share-QR verifier (physical-presence checks)
The wallet-holder opens the credential in their wallet and taps the Verify (QR) tab. The wallet renders a QR containing a URL like:
https://<tenant>/vc-verify.html#c=<sd-jwt-vc>
Anyone with a phone camera scans that QR. Their browser opens /vc-verify.html, which does the following entirely in the verifier's browser — no sign-in, no wallet on the verifier side, no server round-trip for the credential itself:
- Parse the SD-JWT VC from the URL fragment (the credential rides after the
#, which the browser never sends to the server). - Extract the
issclaim and fetch the issuer's public keys from<iss>/.well-known/jwt-vc-issuer(SD-JWT VC standard) or the CodeB convenience endpoint/vci.ashx?jwks. - Pick the JWK matching the JWT header's
kid. - Verify the ES256 signature via WebCrypto.
- Check
exp(expiry) andnbf(not-before). - SHA-256 each selective disclosure and confirm the hash appears in the credential payload's
_sdarray (proves the disclosed value was in the signed set). - If the credential references an OAuth Status List (
status.status_list.uri+idx), fetch the status list JWT, verify its own signature, DEFLATE-decompress the bitmap, read the bit atidx. Fails closed on any missing/expired/bad-signature check.
Renders a green VALID / red INVALID card with the disclosed attributes and a per-check breakdown. Compatible with any SD-JWT VC issued by any CodeB tenant (JWKS lookup follows the credential's own issuer, not the verifier's host).
Pattern B — OpenID4VP verifier (remote / high-security)
Standards-track OID4VP against your tenant's verifier endpoint. See the verifier integration cookbook for the full recipe; a "check my member card" page can be built in an afternoon using the existing DCQL query for urn:aloaha:vc:tenant-membership. The verifier hosts a QR containing an OID4VP request; the wallet-holder scans that QR (opposite direction from Pattern A), the wallet builds a nonce-bound presentation, the verifier confirms.
Standards conformance
The Share-QR verifier implements SD-JWT VC (IETF draft-ietf-oauth-sd-jwt-vc), OAuth Status Lists (IETF draft-ietf-oauth-status-list), and JWA ES256 (RFC 7518). Signature verification runs on WebCrypto (W3C), bitmap decompression on DecompressionStream (WHATWG Compression Standard). No third-party JavaScript libraries. Zero external network calls other than the issuer JWKS fetch and the optional status-list fetch, both of which follow the credential's own iss.
7 Security posture
- Subject binding — the member cannot mint a credential for anyone else. The bearer's
subis authoritative; the client cannot override it. - VCT allow-listing — only VCTs the tenant has opted in are self-serviceable.
- Claim projection — the client cannot inject arbitrary claims; every credential claim is server-derived from the OIDC user record via
BuildClaimsForVct. - Single-use — each offer is single-use with a short pre-authorized code TTL. A leaked deep-link is worthless after redemption or expiry.
- Rate-limit — one offer per 5 seconds per subject; the tenant is never floodable.
- Per-tenant isolation — offers live in
App_Data/<tenant>/vci/offers/; the credential is signed with the tenant's own issuer key; no cross-tenant leakage.
8 Standards conformance
OpenID for Verifiable Credential Issuance (OID4VCI) 1.0 Final, OpenID for Verifiable Presentations (OID4VP), SD-JWT VC (IETF), HAIP profile of OID4VC. Compatible with the ARF 3.0 reference wallets and any conforming European Digital Identity Wallet.