No description
  • EJS 48.3%
  • JavaScript 32.7%
  • CSS 18.1%
  • Dockerfile 0.9%
Find a file
2026-05-05 22:33:52 +02:00
app feat: adapt README 2026-05-05 22:33:52 +02:00
.dockerignore feat: adapt README 2026-05-05 22:33:52 +02:00
.env.example feat: multi-domain 2026-05-05 22:03:43 +02:00
docker-compose.yml feat: multi-domain 2026-05-05 22:03:43 +02:00
README.md feat: adapt README 2026-05-05 22:33:52 +02:00
variants.yaml feat: multi-domain 2026-05-05 22:03:43 +02:00

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

  1. Add an entry to variants.yaml (slug, label, issuer, scopes)
  2. Register a new SP in IDA-SPR with client_id = https://{APP_DOMAIN}/sp/{slug}
  3. Add SECRET_{SLUG_UPPERCASE}=... to .env and the corresponding line in docker-compose.yml
  4. Restart the container — no image rebuild needed (variants.yaml is a mounted volume)

ID Austria Specifics

  • client_secret is mandatory — even without PKCE. Public clients are not supported.
  • client_id must be a URL — use https://{APP_DOMAIN}/sp/{slug} per variant
  • request_uri is not supported — results in an error
  • response_mode=form_post is not supported — results in an error
  • sub is transient — use the bPK claim for persistent user identification
  • No nonce in id_token — do not send a nonce in 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_DOMAIN to your domain (without https://)
  • Set SECRET_{SLUG_UPPERCASE} for each variant in variants.yaml
  • Generate SESSION_SECRET with openssl 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

  1. Create a test identity in the Test Identity Manager
  2. Open https://your-domain.example.com → click the variant button you want to test
  3. Authenticate with the test identity

Mobile / Simplified Continuation (App2App):

  1. Open the ID Austria app → Info → tap the version number 10× → enable Developer functions
  2. Set Backend environment to Referenz or QS → re-link the app
  3. 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:

  • BPKurn:pvpgvat:oidc.bpk
  • EID-CITIZEN-QAA-EIDAS-LEVELurn:pvpgvat:oidc.eid_citizen_qaa_eidas_level
  • MANDATE-TYPEurn: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
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