Explainer 08

Sign in with Jomhoor

The first time you open Taraaz, Civic Compass, or any other platform that integrates with Jomhoor, you do not type an email. You do not pick a password. You do not hand over a phone number. You scan a QR code with your Jomhoor wallet, approve a consent screen, and you are signed in.

"Sign in with Jomhoor" is an OAuth2 SSO system — like "Sign in with Google" in shape — but with three deliberate differences: the credential is a wallet that lives on your phone (optionally backed by a passport-based ZK identity), the partner platform never sees who you are across other partners, and there is no central account database to subpoena.

Two tiers of Jomhoor user

Not every Jomhoor partner needs to know you are an Iranian citizen. So the wallet has two states, and the SSO flow handles both:

Install the Jomhoor app

creates a BabyJubjub wallet + device attestation
  • Local key generated on-device
  • App Attest / Play Integrity verified
  • No passport yet, no on-chain identity yet

Attested wallet user

default after install
  • Can sign in to partners that do not require citizenship
  • Gets a pairwise ps_… subject per partner
  • Device-bound, sybil-resistant against bot farms
  • Cannot sign in to partners with zk_required = true

ZK-verified wallet user

after scanning a passport or INID
  • Can sign in to all partners, including zk_required = true
  • Same pairwise subject as before — the upgrade is invisible to existing partners
  • Unlocks on-chain voting and citizenship-gated features
  • Live zk_verified assertion served by /v1/tokens/validate

This matters because most partners do not actually need nationality. A neighbourhood-association forum, a tenants' union, a software-developers' guild, a fan community, an event ticket-holder chat — none of these need to know you are Iranian. They need to know that the same human keeps coming back, and that account creation is expensive enough that bot farms cannot drown them. An attested wallet alone gives them that. The ZK passport gate sits on top, available when the partner genuinely needs it.

From the partner's point of view, the toggle is a single boolean on their registered client: zk_required = true or false. The wallet enforces it; partners do not have to write any verification code.

Why a Jomhoor-specific SSO exists at all

Login is one of the largest surveillance surfaces on the modern internet. Every "Sign in with X" button silently links an account to a phone number, an email, a device, and a behavioural fingerprint that the identity provider then sells, leaks, or hands over on request. For an Iranian civic platform that is the wrong threat model on every axis: legal (which jurisdiction stores it), commercial (who profits from it), and political (who can compel its disclosure).

So Jomhoor builds login the same way it builds voting:

The handshake, end-to-end

USER

Partner site

  • "Sign in with Jomhoor"
  • Opens /v1/authorize
  • PKCE code_challenge

Jomhoor wallet

  • Signs the nonce (BabyJubjub)
  • App Attest / Play Integrity
  • Consent screen
SIGNED IN

Partner gets a JWT with
sub = pairwise subject

Partner does not learn
wallet address, passport, or PII

Concretely, six steps happen behind that one tap:

  1. Partner site → /v1/authorize. The partner's web app redirects (or opens a Universal Link on mobile) to sso.jomhoor.org/v1/authorize carrying its client_id, redirect_uri, and a PKCE code_challenge.
  2. sso-svc issues a nonce and bounces the user into the Jomhoor wallet via a deep link.
  3. Wallet attests the device. The wallet calls Apple App Attest (on iOS) or Google Play Integrity (on Android) to prove the request is coming from an unmodified Jomhoor app on a non-rooted device. This is mandatory in production.
  4. User sees the consent screen — partner name, partner logo, and what is being shared (nothing but a pairwise identifier).
  5. Wallet signs the nonce with the user's BabyJubjub key and POSTs to /v1/authorize/verify. sso-svc returns a one-time code.
  6. Partner exchanges the code at /v1/tokens/exchange (with its client_secret and the PKCE code_verifier) and receives a signed JWT.

What the JWT actually carries

JWT issued to the partner

signed by sso-svc, ES256
  • sub — pairwise subject (ps_…)
  • client_id — which partner this is for
  • token_typeaccess or refresh
  • iat, exp

Inside the token

  • Pairwise subject
  • Partner ID
  • Token type, validity window

Not inside the token

  • Wallet address
  • Public key
  • Passport / INID data
  • Email, phone, name
  • zk_verified flag

The pairwise subject is derived as HMAC-SHA256(server_secret, walletID:clientID). The same wallet across two different partners produces two unrelated ps_… strings; the partners cannot collude to merge user profiles even if they wanted to.

The zk_verified flag is deliberately not baked into the token. If a partner wants live trust state, it calls /v1/tokens/validate at the moment it matters — which returns a fresh {valid, subject, client_id, assertions} view. We chose this so that revocations and assertion changes take effect immediately rather than waiting for the token to expire.

Why app attestation is mandatory

A wallet on a rooted phone is a wallet that can be silently exfiltrated. A wallet running in a tampered build is a wallet whose attestation logic can be skipped. We refuse both. In production, sso-svc will not even start unless attestation is enabled.

This means Jomhoor cannot currently onboard users on Huawei devices without Google Mobile Services, custom ROMs, or jailbroken phones. We acknowledge this is a real exclusion. The trade-off is that the trust we extend to partners is grounded in something stronger than "the wallet says so."

For partners — what becoming a relying party gives you

Pick your gate. Most partners run with the default attested-wallet gate; civic platforms that need citizenship flip zk_required = true. Either way, Jomhoor SSO gives you:

How to integrate

  1. Register your platform as an sso_client. We bcrypt your client_secret, store your redirect URIs, your display name, your logo URL, and your zk_required flag in a small public client registry. Reachable at GET /v1/clients/{id} — the consent screen renders from this.
  2. Add a "Sign in with Jomhoor" button on your login page. It hits /v1/authorize with your client_id, redirect_uri, and a PKCE code_challenge (base64url, no padding, S256).
  3. On the callback, exchange the code at /v1/tokens/exchange with your client_secret and the matching code_verifier. You receive an access token and a refresh token.
  4. When you need to check live trust state, call /v1/tokens/validate.

The reference integrations are open source: Taraaz (Agora-based deliberation fork) and Civic Compass. The protocol is standard OAuth2 auth-code + PKCE; if you can integrate "Sign in with Google", you can integrate this.

What this is not

What we have not solved yet

One honest gap remains:

  1. Trust-root sovereignty. The ICAO root our passport proofs are checked against still lives in Rarimo's StateKeeper. M6 (covered in explainer 06) brings that under Jomhoor's admin keys. Until M6 lands, the deepest layer of trust below SSO is governed by someone else.

ZK escalation at sign-in and ZK-nullifier-based recovery (M5) shipped on 18 May 2026: partners with zk_required=true get a fresh INID query-proof gate before consent, and a user who reinstalls the wallet can rebind their prior pairwise subjects by proving the same nullifier — so per-partner identifiers survive a wipe without ever revealing the underlying passport.

Until trust-root sovereignty closes, "Sign in with Jomhoor" is already the most private way to log in to a civic platform on the open internet: no passwords, no emails, no phone numbers, no cross-site identifier, and no central database to seize. Everything beyond that is iteration.

End of series

This is the last file in the explainers series. If you have read 01 through 08, you now know substantially more about how Jomhoor actually works than most people who have written about it.

If something in here is wrong, unclear, or oversold, please open an issue or PR against the source repository. Honesty about limits is the precondition for trust about strengths.


← Back to docs · Previous: Sovereign stack and M6 →