On this page
ZamPass and the move to Pamodzi Pass
ZamPass is the government sign-in in use today. The e-Services platform signs citizens and staff in through it, and will keep doing so until Pamodzi Pass is ready and the switch-over is made. This guide explains ZamPass as it really behaves, how the platform protects itself from its weaker points, and what to do now so the move costs you a configuration change rather than a rewrite.
ZamPass today
ZamPass is built from:
- Ory Hydra (v2.3) as the OAuth 2.0 / OpenID Connect provider: authorise, token, userinfo, keys, logout;
- a customised Ory Kratos as the identity store and sign-in pages;
- a token hook that adds identity claims from the National Identity Register (NIR) to every token.
Endpoints
Hydra's public routes, under the ZamPass issuer:
| Purpose | Path |
|---|---|
| Discovery | /.well-known/openid-configuration |
| Keys | /.well-known/jwks.json |
| Authorisation | /oauth2/auth |
| Token | /oauth2/token |
| Userinfo | /userinfo |
| Revocation | /oauth2/revoke |
| Logout (RP-initiated) | /oauth2/sessions/logout |
Back-channel logout is supported for a client registered with a backchannel_logout_uri. Clients are registered by the ZamPass operators (SZI); there is no self-service registration. The issuer address of each environment comes with your client registration.
Claims
| Claim | Scope | What to know |
|---|---|---|
sub | always | The same value for every system (public subject); also the NIR's key for the person |
sid | always | The session id, used by back-channel logout |
amr | always | Kratos method names (password, totp, webauthn, lookup_secret, ...), not RFC 8176 values |
acr | - | Not sent |
firstName, lastName | profile | Proprietary names, not given_name / family_name |
dateOfBirth, gender | profile_extended | |
verified, trustLevel | with profile, profile_extended or document | verified is trustLevel > 1. Both are false / 0 when the NIR has no record or is unavailable. What each level means is not documented. |
nationalId, documentType, documentIssuedBy, documentNumber | document | nationalId is the NRC. Released whether or not it was verified. |
email, phone_number | email, phone | Listed by the provider but not produced |
Lifetimes in the reference configuration: authorisation code, access token, refresh token and ID token all 1 hour; consent is remembered indefinitely. Whether the NIR claims appear in the ID token, in /userinfo, or both has not been confirmed on the live issuer, so read both.
The weak points that matter to you
- An NRC from ZamPass is not proof on its own. The
documentscope releasesnationalIdeven whenverifiedisfalse. - No
acr, andamrin Kratos words. You cannot read "did this person use a second factor" from standard values. - One
subfor every system, so systems can link a person's activity across services. - Short refresh tokens (as long as the access token), so refresh adds little.
How the platform maps ZamPass
The platform treats ZamPass as one provider behind an identity port. Everything ZamPass-specific lives in one adapter (lib/identity/providers/zampass.js); the rest of the platform sees a canonical identity and never branches on the provider's name.
| Step | What the platform does |
|---|---|
| Start | Authorization code flow with state, nonce and PKCE S256. Endpoints come from discovery with the issuer pinned. Scopes: openid profile plus document (configurable with ZAMPASS_DOCUMENT_SCOPE); email and phone are no longer requested because ZamPass produces neither. |
| ID token | Fully validated: signature with the provider's keys and an asymmetric algorithm only, iss, aud, azp, exp, iat, nbf, nonce, with 60 seconds of clock skew. /userinfo is accepted only for the same sub. |
| NRC | Counted as verified only when verified === true (a JSON boolean), and, when ZAMPASS_NRC_MIN_TRUST_LEVEL is set, trustLevel reaches it. The account records when ZamPass vouched and a keyed hash of that NRC; if the NRC is later edited it is unverified again. |
| Assurance | Verified NRC: substantial. Unverified: ZAMPASS_UNVERIFIED_ASSURANCE (default low, never high). high needs a second factor on a verified identity. |
| MFA | Read from amr per ZAMPASS_MFA_TRUST (amr, always or never) and ZAMPASS_MFA_AMR. Default: RFC 8176 second factors plus totp, lookup_secret and webauthn. A passwordless webauthn / passkey alone does not count unless configured. Staff without an accepted second factor are asked for the platform's own TOTP. |
| Linking | An existing account is linked by NRC only when the NRC is verified (ZAMPASS_LINK_REQUIRES_VERIFIED=1). A staff account is never linked silently: the officer confirms, or an administrator approves a link request. |
| Profile | Names, date of birth and gender are refreshed at each sign-in only from a verified identity whose NRC is the account's own. A different verified NRC is a conflict: nothing is applied and the service desk gets a ticket. |
| Tokens | ZamPass access and refresh tokens are not kept. The platform issues its own session; nothing goes into localStorage or a URL. |
| Logout | RP-initiated logout through a POST form; back-channel logout at POST /api/auth/backchannel-logout (signature, iss, aud, iat age, events claim, no nonce, sub or sid). A logout fans out to federation nodes. |
| Outage | GET /api/status/identity probes the provider; the sign-in pages show "ZamPass is unavailable" instead of sending people to a dead page. Signed-in users are not affected. |
The platform's sign-in routes are /api/auth/oidc/login, /api/auth/oidc/callback and /api/auth/oidc/logout; the older /api/auth/zampass* names remain as aliases for one release.
Across the federation
The national hub is the only system that signs citizens in. A ministry node refuses to start or finish a citizen sign-in (403 FED_HUB_ONLY_CITIZEN_SIGNIN, pointing to the hub portal). The hub vouches for the citizen to a node with a short-lived signed assertion carrying a pairwise subject per node, the assurance level, and nrc_verified (the fact, never the number). A service can require the verified-identity capability: applying then needs a verified NRC and a substantial sign-in, and is refused with 403 IDENTITY_ASSURANCE_REQUIRED otherwise.
The move to Pamodzi Pass
Pamodzi Pass fixes ZamPass's weak points by design: acr and RFC 8176 amr, an ial claim, nrc always paired with nrc_verified, PKCE for every client, ES256 ID tokens, short-lived codes and tokens, refresh tokens only on request and rotated, pairwise subjects for third parties, and maker-checker on every system, grant and clearance.
The move is planned in stages. What exists today:
| Stage | State |
|---|---|
| Pamodzi Pass protocol core, accounts, MFA, registration, recovery, account pages, governance logic | Written; under active development |
| Pamodzi Pass administration pages and integration tests | In progress |
| "Continue with ZamPass" bridge in Pamodzi Pass | Next. Configured with PP_ZAMPASS_ISSUER, PP_ZAMPASS_CLIENT_ID, PP_ZAMPASS_CLIENT_SECRET, PP_ZAMPASS_REDIRECT_URI (and PP_ZAMPASS_HOSTS); off unless all four are set. Today the button answers "not available yet" and nothing is accepted from ZamPass. |
| e-Services platform switch-over: Pamodzi Pass as the only sign-in, local passwords kept only for sealed break-glass accounts | Next |
| Remote NRC verification against the national register, passkeys, device sign-in for terminals | Planned |
The bridge is meant to let a person sign in once with ZamPass and bring their account and its verified details across to Pamodzi Pass, so nobody has to register again.
On the platform side, the identity port can already hold a second OpenID Connect issuer next to ZamPass (an identity_clients row with provider oidc), with its acr values mapped to low, substantial and high through OIDC_ACR_ASSURANCE. Mapping Pamodzi Pass's nrc / nrc_verified / ial claims into the platform's verified-NRC rule is part of the switch-over work and is not built yet.
What integrators should do now
If your system calls the platform's APIs (Partner API, webhooks, document verification): nothing changes. Partner tokens are issued by the platform with client credentials, not by ZamPass or Pamodzi Pass.
If your system signs people in itself:
- Use standard OpenID Connect through a library. Code flow, PKCE
S256,state,nonce, discovery with theissuer pinned, full ID token validation. The same code then works against Pamodzi Pass.
- Put the provider's vocabulary in one place. Map
firstName/lastName,verified/trustLeveland theKratos
amrnames into your own neutral fields in one adapter. When Pamodzi Pass arrives, you add a second mapping (given_name/family_name,nrc_verified/ial,acr) instead of editing your business code. - Treat an NRC as verified only when the provider says so explicitly:
verified === truefrom ZamPass today,nrc_verified === truefrom Pamodzi Pass. Never link accounts or grant access on a self-declared or unverified NRC. - Do your own second factor for staff until you can rely on
acr: urn:pamodzi:aal2. ZamPass sends noacr. - Keep provider tokens on your server and give the browser only an
HttpOnlysession cookie. - Register a back-channel logout address so a sign-out anywhere ends your sessions.
- Ask for the minimum scopes. Request the NRC (
documenttoday,national_idwith Pamodzi Pass) only where thelaw and your purpose need it; Pamodzi Pass will require your DPA reference for it.
- Plan for the subject to change. Pamodzi Pass accounts have their own
sub. Link a person's Pamodzi Pass accountto your existing record by a verified NRC or an explicit confirmation, never by name or e-mail alone.
Testing without ZamPass
Never test against real people's ZamPass accounts. The platform's test doubles include a mock ZamPass (Hydra-shaped routes, NIR claims, back-channel logout, an outage switch) with invented personas: citizen-verified, citizen-unverified, citizen-no-nrc, officer-totp, officer-password, citizen-linked and citizen-nrc-taken. Start it with npm run stack:providers in zcm-backend. Test at least: a verified and an unverified NRC, an officer with and without a second factor, a changed signing key, and the provider being down.
Related: Sign in with Pamodzi Pass.