An official developer portal of the Government of the Republic of Zambia · Test environmentVerify a certificate
Pamodzi Developer HubBuild, integrate and learn the e-Services framework
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:

PurposePath
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

ClaimScopeWhat to know
subalwaysThe same value for every system (public subject); also the NIR's key for the person
sidalwaysThe session id, used by back-channel logout
amralwaysKratos method names (password, totp, webauthn, lookup_secret, ...), not RFC 8176 values
acr-Not sent
firstName, lastNameprofileProprietary names, not given_name / family_name
dateOfBirth, genderprofile_extended
verified, trustLevelwith profile, profile_extended or documentverified 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, documentNumberdocumentnationalId is the NRC. Released whether or not it was verified.
email, phone_numberemail, phoneListed 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 document scope releases nationalId even when

    verified is false.

  • No acr, and amr in Kratos words. You cannot read "did this person use a second factor" from standard values.
  • One sub for 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.

StepWhat the platform does
StartAuthorization 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 tokenFully 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.
NRCCounted 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.
AssuranceVerified NRC: substantial. Unverified: ZAMPASS_UNVERIFIED_ASSURANCE (default low, never high). high needs a second factor on a verified identity.
MFARead 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.
LinkingAn 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.
ProfileNames, 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.
TokensZamPass access and refresh tokens are not kept. The platform issues its own session; nothing goes into localStorage or a URL.
LogoutRP-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.
OutageGET /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:

StageState
Pamodzi Pass protocol core, accounts, MFA, registration, recovery, account pages, governance logicWritten; under active development
Pamodzi Pass administration pages and integration testsIn progress
"Continue with ZamPass" bridge in Pamodzi PassNext. 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 accountsNext
Remote NRC verification against the national register, passkeys, device sign-in for terminalsPlanned

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:

  1. Use standard OpenID Connect through a library. Code flow, PKCE S256, state, nonce, discovery with the

    issuer pinned, full ID token validation. The same code then works against Pamodzi Pass.

  2. Put the provider's vocabulary in one place. Map firstName / lastName, verified / trustLevel and the

    Kratos amr names 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.

  3. Treat an NRC as verified only when the provider says so explicitly: verified === true from ZamPass today,

    nrc_verified === true from Pamodzi Pass. Never link accounts or grant access on a self-declared or unverified NRC.

  4. Do your own second factor for staff until you can rely on acr: urn:pamodzi:aal2. ZamPass sends no acr.
  5. Keep provider tokens on your server and give the browser only an HttpOnly session cookie.
  6. Register a back-channel logout address so a sign-out anywhere ends your sessions.
  7. Ask for the minimum scopes. Request the NRC (document today, national_id with Pamodzi Pass) only where the

    law and your purpose need it; Pamodzi Pass will require your DPA reference for it.

  8. Plan for the subject to change. Pamodzi Pass accounts have their own sub. Link a person's Pamodzi Pass account

    to 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.