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_verifiedassertion 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 credential is generated on the user's phone — a BabyJubjub wallet at install, optionally upgraded with a ZK proof of an Iranian biometric passport or INID when a partner requires citizenship.
- The identifier given to each partner is pairwise — same user, different partners, mathematically unlinkable identifiers.
- The login service (
sso-svc) does not store passport data, wallet addresses, or PII — and the JWT it issues carries none either.
The handshake, end-to-end
Partner site
- "Sign in with Jomhoor"
- Opens
/v1/authorize - PKCE
code_challenge
Jomhoor wallet
- Signs the nonce (BabyJubjub)
- App Attest / Play Integrity
- Consent screen
Partner gets a JWT withsub = pairwise subject
Partner does not learn
wallet address, passport, or PII
Concretely, six steps happen behind that one tap:
- Partner site →
/v1/authorize. The partner's web app redirects (or opens a Universal Link on mobile) tosso.jomhoor.org/v1/authorizecarrying itsclient_id,redirect_uri, and a PKCEcode_challenge. sso-svcissues a nonce and bounces the user into the Jomhoor wallet via a deep link.- 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.
- User sees the consent screen — partner name, partner logo, and what is being shared (nothing but a pairwise identifier).
- Wallet signs the nonce with the user's BabyJubjub key and POSTs to
/v1/authorize/verify.sso-svcreturns a one-timecode. - Partner exchanges the code at
/v1/tokens/exchange(with itsclient_secretand the PKCEcode_verifier) and receives a signed JWT.
What the JWT actually carries
JWT issued to the partner
signed bysso-svc, ES256
sub— pairwise subject (ps_…)client_id— which partner this is fortoken_type—accessorrefreshiat,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_verifiedflag
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.
- iOS: Apple App Attest. Verifies the app binary against Apple's signed bundle ID and refuses jailbroken devices.
- Android: Google Play Integrity. Verifies the APK signature, bootloader state, and that the device passes Play Protect.
- Failure mode: The wallet shows a "Device not supported" screen and refuses to onboard. This is the right answer; it is not a bug.
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:
- One account per device, not per email. The wallet's pairwise subject means a single user cannot silently create two accounts on your platform. With
zk_required = truethe uniqueness is stronger still: one passport, one account. - No user PII to store, leak, or be subpoenaed for. Your database holds
ps_…strings. Even if you are seized, the strings cannot be traced back to wallets or passports. - No cross-partner linkability. Even if two partner platforms both integrate Jomhoor, the same user gets two unrelated subjects — useful for privacy, and a hard cap on data-broker schemes.
- A clear choice of trust level. Set
zk_required = falsefor general communities that just want sybil resistance from device attestation; setzk_required = truefor referenda, citizen assemblies, deliberation platforms, or anything that legally requires verified Iranian nationality. - Fine-grained ZK escalation (Milestone 5). For partners that need more than "verified Iranian" — for example "verified Iranian, female, between 18 and 35, never voted in this referendum" — the wallet can produce a fresh ZK proof at sign-in time.
How to integrate
- Register your platform as an
sso_client. We bcrypt yourclient_secret, store your redirect URIs, your display name, your logo URL, and yourzk_requiredflag in a small public client registry. Reachable atGET /v1/clients/{id}— the consent screen renders from this. - Add a "Sign in with Jomhoor" button on your login page. It hits
/v1/authorizewith yourclient_id,redirect_uri, and a PKCEcode_challenge(base64url, no padding, S256). - On the callback, exchange the
codeat/v1/tokens/exchangewith yourclient_secretand the matchingcode_verifier. You receive an access token and a refresh token. - 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
- Not a profile service. There is no
/userinfoendpoint that returns a name, an email, or an avatar. We do not have those. - Not a single-sign-out service. Signing out of one partner does not sign you out of others. Each session is local to its partner.
- Not a custodial wallet service.
sso-svcnever sees your wallet key. If you reinstall the wallet, you re-scan your passport and recovery rebinds your prior per-partner pairwise subjects by proving the same nullifier — the passport itself is never revealed. - Not federated. Other identity providers cannot "Sign in to Jomhoor" on your behalf. The chain of trust terminates at your passport and your device's attestation.
What we have not solved yet
One honest gap remains:
- 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.