- EJS 48.3%
- JavaScript 32.7%
- CSS 18.1%
- Dockerfile 0.9%
| app | ||
| .dockerignore | ||
| .env.example | ||
| docker-compose.yml | ||
| README.md | ||
| variants.yaml | ||
ID Austria OIDC Test Client
A Node.js OIDC test client for ID Austria, supporting multiple SP registrations (variants) simultaneously on the same domain.
Implements the Authorization Code Flow, displays all returned claims (including Austria-specific claims like bPK, eIDAS level, PVP version, Vollmacht/Vertretungs-attributes, and extended Personenmerkmale), and handles iOS Safari's ITP cookie restrictions via a state-encoded session strategy.
Features
| Feature | Details |
|---|---|
| Flow | Authorization Code (response_type=code) |
| Auth method | client_secret_post — required by ID Austria |
client_id format |
URL — derived as https://{APP_DOMAIN}/sp/{slug} per variant |
| Variants | Arbitrary number of SP registrations via variants.yaml — each with its own client_id, scopes, and IDA-SPR configuration |
| Environments | ref (idp.ref.id-austria.gv.at) and qs (idp.qs.id-austria.gv.at) — selectable per variant |
| Scopes | Configurable per variant in variants.yaml |
| Logout | RP-Initiated Logout via end_session_endpoint |
| Session strategy | State-encoded with variant slug (cookie-independent, iOS Safari ITP-compatible) |
| Claims display | Basisdaten, technische Attribute, Vollmachten/Vertretungen (3 subsections), Weitere Merkmale (21 attributes), OIDC metadata, raw dumps |
| Debug endpoint | /debug/discovery?env={slug} — OIDC Discovery metadata as JSON |
Multi-Variant Architecture
Each ID Austria SP registration requires a unique client_id URL. This client uses the path /sp/{slug} to make multiple registrations on the same domain unique:
https://idaustria-test.example.com/sp/ref ← REF environment
https://idaustria-test.example.com/sp/nat ← QS, natural person mandate profile
https://idaustria-test.example.com/sp/jur ← QS, legal entity mandate profile
All variants share the same redirect_uri (/callback) and post_logout_redirect_uri (/). The active variant is embedded in the HMAC-protected state parameter, so the callback always uses the correct OIDC config for token exchange and logout.
Variant Configuration
Variant metadata lives in variants.yaml (committed to the repository, no secrets):
variants:
- slug: ref
label: Referenzumgebung
issuer: ref # ref | qs
scopes: openid profile
- slug: nat
label: Natürliche Vertretung (QS)
issuer: qs
scopes: openid profile
- slug: jur
label: Juristische Vertretung (QS)
issuer: qs
scopes: openid profile
Each variant requires a SECRET_{SLUG_UPPERCASE} entry in .env (gitignored). Variants without a matching secret are skipped silently at startup.
Adding a New Variant
- Add an entry to
variants.yaml(slug, label, issuer, scopes) - Register a new SP in IDA-SPR with
client_id = https://{APP_DOMAIN}/sp/{slug} - Add
SECRET_{SLUG_UPPERCASE}=...to.envand the corresponding line indocker-compose.yml - Restart the container — no image rebuild needed (
variants.yamlis a mounted volume)
ID Austria Specifics
client_secretis mandatory — even without PKCE. Public clients are not supported.client_idmust be a URL — usehttps://{APP_DOMAIN}/sp/{slug}per variantrequest_uriis not supported — results in an errorresponse_mode=form_postis not supported — results in an errorsubis transient — use thebPKclaim for persistent user identification- No
noncein id_token — do not send anoncein the authorization request - Discovery URL (reference env):
https://idp.ref.id-austria.gv.at/.well-known/openid-configuration
iOS Safari / App2App (Simplified Continuation)
Standard session-over-cookie fails on iOS Safari due to ITP: the callback request arrives without the session cookie, breaking the OIDC state check. This client works around this by encoding the session ID and variant slug (HMAC-secured with a nonce) inside the state parameter. The callback reconstructs the session from state directly, without relying on a cookie.
Quick Start
1. Configure variants.yaml
Edit variants.yaml to define the SP variants you want to test. Each variant maps to one IDA-SPR registration.
2. Register in IDA-SPR
For each variant, create a new service provider registration at IDA-SPR:
| Field | Value |
|---|---|
Application identifier (client_id) |
https://your-domain.example.com/sp/{slug} |
redirect_uri |
https://your-domain.example.com/callback |
post_logout_redirect_uri |
https://your-domain.example.com/ |
| Test identities | enable |
| Attribute / mandate profile | configure per variant as needed |
Save the generated client_secret immediately — it is shown only once.
3. Configure .env
cp .env.example .env
Edit .env:
- Set
APP_DOMAINto your domain (withouthttps://) - Set
SECRET_{SLUG_UPPERCASE}for each variant invariants.yaml - Generate
SESSION_SECRETwithopenssl rand -hex 32
Also add the corresponding SECRET_* environment variable lines to docker-compose.yml.
4. Run
With Traefik (the default docker-compose.yml includes Traefik labels for automatic TLS via Let's Encrypt):
docker compose build # required once after adding js-yaml dependency
docker compose up -d
docker compose logs -f
Standalone (without Traefik, for local development):
docker build -t idaustria-client ./app
docker run --rm -p 3000:3000 \
-v "$(pwd)/variants.yaml:/app/variants.yaml:ro" \
-e APP_DOMAIN=localhost:3000 \
-e SECRET_REF=your_secret \
-e SESSION_SECRET=$(openssl rand -hex 32) \
-e TRUST_PROXY=false \
idaustria-client
5. Test
- Create a test identity in the Test Identity Manager
- Open
https://your-domain.example.com→ click the variant button you want to test - Authenticate with the test identity
Mobile / Simplified Continuation (App2App):
- Open the ID Austria app → Info → tap the version number 10× → enable Developer functions
- Set Backend environment to Referenz or QS → re-link the app
- Open the test client on your mobile device → tap the matching variant button → the app launches automatically
Endpoints
| URL | Description |
|---|---|
/ |
Landing page / claims display after login |
/login?env={slug} |
Initiate login for the given variant |
/callback |
OIDC callback — register this URL in IDA-SPR |
/logout |
RP-Initiated Logout (uses the variant from session) |
/debug/discovery?env={slug} |
OIDC Discovery metadata for a variant (JSON) |
/health |
Health check |
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
APP_DOMAIN |
Yes | — | Domain, e.g. idaustria-test.example.com (no https://) |
SESSION_SECRET |
Yes | random | Session signing secret — openssl rand -hex 32 |
SECRET_{SLUG} |
Yes* | — | Client secret from IDA-SPR for variant {slug} (uppercase). At least one required. |
ISSUER_REF |
No | https://idp.ref.id-austria.gv.at/… |
OIDC Discovery endpoint for issuer: ref variants |
ISSUER_QS |
No | https://idp.qs.id-austria.gv.at/… |
OIDC Discovery endpoint for issuer: qs variants |
TRUST_PROXY |
No | true |
Trust X-Forwarded-* headers from a reverse proxy |
PORT |
No | 3000 |
HTTP listen port |
Claim Naming: pvpgvat vs. OID
ID Austria delivers Austria-specific attributes under two different naming schemes:
| Scheme | Example | Where documented |
|---|---|---|
urn:pvpgvat:oidc.* |
urn:pvpgvat:oidc.bpk |
MOA-ID Handbook (see below) |
urn:oid:1.2.40.0.10.2.1.1.* |
urn:oid:1.2.40.0.10.2.1.1.149 |
id-austria.gv.at/de/developer/registrieren/personenmerkmale |
The pvpgvat names are what ID Austria actually delivers in tokens. The OID names appear in the official developer documentation but are used as fallbacks in this client for forward-compatibility.
pvpgvat Naming Rule
The urn:pvpgvat:oidc.* suffix is derived mechanically from the PVP-2.2 attribute name:
pvpgvat claim = "urn:pvpgvat:oidc." + PVP_ATTR_NAME.toLowerCase().replace(/-/g, '_')
Examples:
BPK→urn:pvpgvat:oidc.bpkEID-CITIZEN-QAA-EIDAS-LEVEL→urn:pvpgvat:oidc.eid_citizen_qaa_eidas_levelMANDATE-TYPE→urn:pvpgvat:oidc.mandate_type
Source: MOA-ID Handbook v4.x — Protocol chapter (This is the only publicly accessible document with the full PVP-2.2 attribute name → OID mapping table.)
Complete pvpgvat ↔ OID ↔ PVP-2.2 Mapping
All OIDs are under the 1.2.40.0.10.2.1.1 arc; the table shows only the suffix.
Core / Basisdaten
| pvpgvat claim | OID suffix | PVP-2.2 name | Notes |
|---|---|---|---|
urn:pvpgvat:oidc.bpk |
.149 |
BPK |
Bereichsspezifisches Personenkennzeichen — use for persistent user identity |
urn:pvpgvat:oidc.pvp_version |
— | PVP-VERSION |
PVP protocol version |
(standard OIDC) given_name |
.261.20 |
PRINCIPAL-NAME |
|
(standard OIDC) family_name |
— | — | |
(standard OIDC) birthdate |
.55 |
BIRTHDATE |
Technische Attribute
| pvpgvat claim | OID suffix | PVP-2.2 name | Notes |
|---|---|---|---|
urn:pvpgvat:oidc.eid_citizen_qaa_eidas_level |
.261.108 |
EID-CITIZEN-QAA-EIDAS-LEVEL |
eIDAS assurance level URI (★ always delivered) |
urn:pvpgvat:oidc.eid_identity_status_level |
.261.109 |
EID-IDENTITY-STATUS-LEVEL |
e.g. testidentity (★ always delivered) |
urn:pvpgvat:oidc.eid_issuing_nation |
.261.32 |
EID-ISSUING-NATION |
ISO 3166-1 alpha-2 country code |
urn:pvpgvat:oidc.eid_ida_level |
.261.107 |
EID-IDA-LEVEL |
ID Austria assurance level |
urn:pvpgvat:oidc.eid_sector_for_identifier |
.261.34 |
EID-SECTOR-FOR-IDENTIFIER |
bPK sector URI |
urn:pvpgvat:oidc.eid_ccs_url |
.261.64 |
EID-CCS-URL |
Citizen card environment URL |
urn:pvpgvat:oidc.eid_signer_certificate |
.261.66 |
EID-SIGNER-CERTIFICATE |
Base64 DER signer certificate |
urn:pvpgvat:oidc.eid_online_identity_link |
.261.39 |
EID-ONLINE-IDENTITY-LINK |
Online Personenbindung (JWT) |
(★) = delivered in every successful authentication response regardless of requested scopes.
Vollmachten / Vertretungen — Mandate attributes
| pvpgvat claim | OID suffix | PVP-2.2 name | Description |
|---|---|---|---|
urn:pvpgvat:oidc.mandate_type |
.261.68 |
MANDATE-TYPE |
Mandate type description |
urn:pvpgvat:oidc.mandate_type_oid |
.261.106 |
MANDATE-TYPE-OID |
OID of the mandate type |
urn:pvpgvat:oidc.mandate_reference_value |
.261.90 |
MANDATE-REFERENCE-VALUE |
Mandate reference identifier |
urn:pvpgvat:oidc.mandate_prof_rep_oid |
.261.86 |
MANDATE-PROF-REP-OID |
Professional representative OID |
urn:pvpgvat:oidc.mandate_prof_rep_description |
.261.88 |
MANDATE-PROF-REP-DESCRIPTION |
Professional representative description |
Vollmachten — Mandator: natural person (vertretene natürliche Person)
| pvpgvat claim | OID suffix | PVP-2.2 name | Description |
|---|---|---|---|
urn:pvpgvat:oidc.mandator_natural_person_given_name |
.261.78 |
MANDATOR-NATURAL-PERSON-GIVEN-NAME |
Given name |
urn:pvpgvat:oidc.mandator_natural_person_family_name |
.261.80 |
MANDATOR-NATURAL-PERSON-FAMILY-NAME |
Family name |
urn:pvpgvat:oidc.mandator_natural_person_birthdate |
.261.82 |
MANDATOR-NATURAL-PERSON-BIRTHDATE |
Date of birth |
urn:pvpgvat:oidc.mandator_natural_person_bpk |
.261.98 |
MANDATOR-NATURAL-PERSON-BPK |
bPK of the represented person |
urn:pvpgvat:oidc.mandator_natural_person_bpk_list |
.261.73 |
MANDATOR-NATURAL-PERSON-BPK-LIST |
bPK list |
urn:pvpgvat:oidc.mandator_natural_person_enc_bpk_list |
.261.72 |
MANDATOR-NATURAL-PERSON-ENC-BPK-LIST |
Encrypted foreign bPK list |
Vollmachten — Mandator: legal entity (vertretene juristische Person)
| pvpgvat claim | OID suffix | PVP-2.2 name | Description |
|---|---|---|---|
urn:pvpgvat:oidc.mandator_legal_person_full_name |
.261.84 |
MANDATOR-LEGAL-PERSON-FULL-NAME |
Full company name |
urn:pvpgvat:oidc.mandator_legal_person_source_pin |
.261.100 |
MANDATOR-LEGAL-PERSON-SOURCE-PIN |
Stammzahl (source identifier) |
urn:pvpgvat:oidc.mandator_legal_person_source_pin_type |
.261.76 |
MANDATOR-LEGAL-PERSON-SOURCE-PIN-TYPE |
Stammzahl type (v1) |
urn:pvpgvat:oidc.mandator_legal_person_source_pin_type_v2 |
.261.77 (inferred) |
MANDATOR-LEGAL-PERSON-SOURCE-PIN-TYPE-V2 |
Stammzahl type (v2) — delivered by live IdP; not in MOA-ID 4.x handbook; OID inferred from sequence gap |
Weitere Merkmale / Extended Personenmerkmale
These use urn:eidgvat:attributes.* and org.iso.18013.5.1:* namespaces (not pvpgvat):
| Claim | Description |
|---|---|
urn:eidgvat:attributes.gender |
Gender |
urn:eidgvat:attributes.nationality |
Nationality |
urn:eidgvat:attributes.maritalStatus |
Marital status |
org.iso.18013.5.1:resident_postal_code |
Postal code |
urn:eidgvat:attributes.mainAddress |
Primary residence |
urn:eidgvat:attributes.mainAddressCommunalData |
Municipality data |
urn:eidgvat:attributes.mainAddressRegistrationDate |
Registration date |
urn:eidgvat:attributes.furtherResidences |
Additional residences |
org.iso.18013.5.1:age_over_14 / _16 / _18 / _21 |
Age attestations |
org.iso.18013.5.1:portrait |
Photo |
org.iso.18013.5.1:portrait_capture_date |
Photo date |
org.iso.18013.5.1:signature_usual_mark |
Signature |
urn:eidgvat:attributes.vehicleRegistrations |
Vehicle registration data |
urn:eidgvat:attributes.identificationDocumentData |
Current ID document |
urn:eidgvat:attributes.idCardData |
ID card data |
urn:eidgvat:attributes.passportData |
Passport data |
urn:eidgvat:attributes.studentId |
Student ID |
urn:eidgvat:attributes.gda |
Healthcare provider |
ID Austria Error Codes
These appear as error_description in the callback when a flow fails:
| Code | Cause |
|---|---|
UserCancel |
User cancelled the flow |
UserNotLoggedIn |
ID Austria app not linked to an account |
UserLoggedOut |
Logged out for security reasons |
UserLockedOut |
Too many failed attempts |
SystemInMaintenance |
Scheduled maintenance |
SystemNotReachable |
No internet connection |
Stack
- Runtime: Node.js 24 (Alpine)
- Framework: Express 5
- OIDC: openid-client v6
- Config: js-yaml v4
- Sessions: express-session (in-memory store — restart clears sessions)
References
- ID Austria Developer Portal
- OIDC Integration Guide
- OIDC for Mobile Apps / App2App
- Reference Environment
- IDA-SPR (Service Provider Registration)
- Test Identity Manager
- Person attributes / claims reference
- Vollmachten / mandate attributes
- MOA-ID Handbook v4.x — Protocol chapter — authoritative source for pvpgvat ↔ OID ↔ PVP-2.2 attribute name mapping