Add “Login with idpass” to your portal

A verified-human login button — like “Login with Google”, but the person is KYC-verified on their own phone. You never store passwords or handle Aadhaar.

New here? Read this first (plain English).

Your users click a button on your site, approve on their phone with a fingerprint, and land back on your site — verified. You get their verified name; nothing else unless they consent. Not technical? Use the drop-in button below (2 lines), or send this page + your Client ID to whoever manages your website.

What happens when someone logs in

🖱️
1. ClickUser taps “Login with idpass” on your site
→
📱
2. ApproveScans the QR / app opens; confirms with fingerprint
→
✅
3. Verifiedidpass checks the identity + device
→
↩️
4. Back to youUser returns to your callback, signed in

Step 0 — get your keys

  1. Create a free account.
  2. Copy your Client ID (looks like idp_ab12cd34…) — and the secret if it’s a server-side app.
  3. Pick an integration path below.

Option A — Drop-in button easiest

No backend needed. Register a “App with no server” at signup. Two lines add this button:

🔐 Login with idpass  ← this is what your users see

your login page (HTML)
<script src="https://idpass.in/sdk/idpass.js"
        data-client-id="idp_YOUR_ID"
        data-redirect-uri="https://your-site.com/callback"
        data-button="#idpass-login"></script>
<div id="idpass-login"></div>
your callback page (e.g. /callback)
<script src="https://idpass.in/sdk/idpass.js"
        data-client-id="idp_YOUR_ID"
        data-redirect-uri="https://your-site.com/callback"></script>
<script>
  const res = await idpass.handleRedirect();      // null if no login in progress
  if (res) {
    console.log("Signed in as", res.claims.name); // verified name
    // res.claims = { sub, name, dob_verified, loa, ... }
    // now create a session for res.claims.sub on your side
  }
</script>

The SDK handles the secure exchange, verifies the token, and only works from the web address you registered. That’s the whole integration.

Option B — Server-side any language

Register a “Normal website” (you get a secret). Point any OpenID Connect library at our discovery URL — done.

Discovery URL: https://atithipass.com/portal/.well-known/openid-configuration

Node — openid-client
import { Issuer, generators } from "openid-client";
const iss = await Issuer.discover("https://idpass.in");
const client = new iss.Client({
  client_id: "idp_YOUR_ID", client_secret: "YOUR_SECRET",
  redirect_uris: ["https://your-site.com/callback"], response_types: ["code"],
});
// start login
const cv = generators.codeVerifier(); req.session.cv = cv;
res.redirect(client.authorizationUrl({
  scope: "openid profile",
  code_challenge: generators.codeChallenge(cv), code_challenge_method: "S256",
}));
// on /callback
const params = client.callbackParams(req);
const tokenSet = await client.callback("https://your-site.com/callback", params,
  { code_verifier: req.session.cv });
const user = tokenSet.claims();   // { sub, name, dob_verified, loa, ... }

Option C — Key pair extra-secure

For developers/fintechs who prefer no shared secret. Register an “Extra-secure (key pair)” app and paste your public key. Make one in 30 seconds:

run on your server
openssl genrsa -out my_private_key.pem 2048
openssl rsa -in my_private_key.pem -pubout -out my_public_key.pem
cat my_public_key.pem     # paste this output at signup

Keep my_private_key.pem secret on your server. Your OIDC library signs a short “client assertion” with it — no secret ever leaves your machine.

Option D — Raw HTTP no library · any stack

Wiring it by hand — PHP, Go, or any stack without an OIDC library? idpass is plain OpenID Connect (Authorization Code + PKCE). Four HTTP steps, no magic. This is the section to read if your callback gave you a code and state and you’re not sure what to do next.

Important: the token is signed, not encrypted.

There is no decryption step and no decryption key — if you went looking for one, that’s why you couldn’t find it. The id_token you get back is a JWS (a JWT with three dot-separated parts). The user’s details are the middle part, base64url-encoded. You verify its signature with our public key from https://atithipass.com/portal/oidc/jwks.json, then read the JSON. That decoded JSON is the user object.

Step 1 — send the user to authorize (build PKCE)

Generate a random state and a PKCE code_verifier; the code_challenge is its SHA-256. Keep the verifier in the user’s session — you need it again in Step 3.

redirect the browser to
https://idpass.in/oidc/authorize
  ?client_id=idp_YOUR_ID
  &redirect_uri=https://your-site.com/callback   # must match a registered URI exactly
  &response_type=code
  &scope=openid%20profile
  &state=RANDOM_STATE                            # you generate; compared in step 2
  &nonce=RANDOM_NONCE                            # you generate; compared in step 4
  &code_challenge=BASE64URL( SHA256(code_verifier) )
  &code_challenge_method=S256                    # PKCE is mandatory
PHP — make the PKCE + state values
function b64url($b){ return rtrim(strtr(base64_encode($b),'+/','-_'),'='); }
$_SESSION['state']    = b64url(random_bytes(16));
$_SESSION['nonce']    = b64url(random_bytes(16));
$_SESSION['verifier'] = b64url(random_bytes(32));
$challenge = b64url(hash('sha256', $_SESSION['verifier'], true));  // raw=true, then b64url
$url = "https://idpass.in/oidc/authorize?".http_build_query([
  "client_id"=>"idp_YOUR_ID","redirect_uri"=>"https://your-site.com/callback",
  "response_type"=>"code","scope"=>"openid profile",
  "state"=>$_SESSION['state'],"nonce"=>$_SESSION['nonce'],
  "code_challenge"=>$challenge,"code_challenge_method"=>"S256"]);
header("Location: $url");

Step 2 — the user comes back to your callback

After they approve on their phone, idpass redirects to your redirect_uri with two query params. First check state equals what you stored (CSRF protection) — if not, stop.

GET https://your-site.com/callback?…
code  = code_ADBC53QWgSoqsJwzz2xQYno6Fox9AV9J   # one-time; swap it in step 3
state = w2UJuW6_6faWwfNQDOTV6g                   # must equal $_SESSION['state']

Step 3 — exchange the code for tokens

POST to the token endpoint with the code and the same code_verifier from Step 1. Send client_secret too if yours is a server-side (“Normal website”) app.

POST /oidc/token
curl -s https://idpass.in/oidc/token \
  -d grant_type=authorization_code \
  -d code=code_ADBC53QWgSoqsJwzz2xQYno6Fox9AV9J \
  -d redirect_uri=https://your-site.com/callback \
  -d client_id=idp_YOUR_ID \
  -d client_secret=YOUR_SECRET \        # omit only for a public (no-server) app
  -d code_verifier=THE_SAME_VERIFIER_FROM_STEP_1

The response is JSON: { "access_token": "…", "id_token": "eyJ…", "token_type": "Bearer", "expires_in": 3600 }.

Step 4 — read the user (decode & verify the id_token)

Verify the id_token signature against our JWKS (algorithm RS256), then its payload is your verified user. Also confirm nonce matches what you sent, and aud equals your client_id.

PHP — verify with our public keys
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
$jwks = json_decode(file_get_contents("https://idpass.in/oidc/jwks.json"), true);
$claims = JWT::decode($tok['id_token'], JWK::parseKeySet($jwks));   // verifies RS256 signature
// $claims is your user:
//   $claims->name, $claims->dob_verified, $claims->loa,
//   $claims->sub  (stable id, unique to YOUR app), $claims->unique_id
if ($claims->aud !== "idp_YOUR_ID")        die("wrong audience");
if ($claims->nonce !== $_SESSION['nonce']) die("nonce mismatch");
// -> log the user in as $claims->sub

Prefer an API call over decoding? Send the access_token (not the id_token) as a Bearer token to https://atithipass.com/portal/oidc/userinfo and it returns { sub, name, dob_verified, loa }.

Stuck? Common errors

You seeCause & fix
invalid_grantThe most common one. Either: (a) your redirect_uri in Step 3 doesn’t byte-for-byte match Step 1 and your registration; (b) the code_verifier doesn’t match the code_challenge you sent (regenerated it, or lost the session); or (c) the code was already used or expired — codes are single-use, exchange immediately.
invalid_clientServer-side app missing or wrong client_secret. (A public / no-server app must not send one.)
origin not allowedA public app called the token endpoint from a web origin you didn’t register. Add the origin to your app.
invalid_token at /userinfoYou sent the id_token. /userinfo takes the access_token only.

What you receive

ClaimMeaning
subStable user id — unique to your app (two portals can’t link the same person).
nameGovernment-verified full name.
dob_verifiedtrue — date of birth is verified (the DOB itself stays private by default).
loaLevel of assurance (2 = government-verified).
device_boundtrue — the login is tied to the user’s enrolled phone.
device_attestedtrue only if the phone’s key passed hardware attestation (TEE/StrongBox). If you require a hardware-bound credential, gate on this.
attest_levelThe device key grade: strongbox / tee (hardware) · none (software key).
amrAuthentication methods. Contains hwk when the key is hardware-attested, else swk (software key); plus mfa, user.
unique_idA stable per-app hash of the person — same human returning to your app always yields the same unique_id (use it to detect duplicate accounts), yet it can’t be linked across different apps.
sub vs unique_idBoth are stable and app-scoped. sub is the account/session subject; unique_id is the anti-duplicate signal for the underlying person.

Optional fields (with user consent)

Beyond the base identity, you can request additional verified fields by adding scopes. At sign-in the user sees a per-field toggle and chooses what to share — so a field arrives only if the user consents. If they decline, the claim is simply absent (design your flow to handle that).

Scope to requestClaim you receiveMeaning
idpass:addressaddressGovernment-verified address (single string).
idpass:aadhaar_last4aadhaar_last4The masked Aadhaar (XXXXXXXX1234) — last 4 digits only.
request them in the authorize URL
scope=openid%20profile%20idpass:address%20idpass:aadhaar_last4

The full Aadhaar number is never shared — only the masked last-4. The scope in the token response tells you which optional fields the user actually granted. Users can also present single facts (e.g. “over 18”) without revealing anything else.

Advanced & regulated

Need higher assurance? idpass supports bank-grade options (sender-constrained tokens, pushed requests, a regulated profile) and agent-assisted / call-centre login where the request is pushed to the user’s phone. Turn these on per app, or ask us — the discovery document lists everything a library needs automatically.

Troubleshooting

The error idpass returns tells you precisely what to fix. The two you’re most likely to hit when wiring it by hand:

Getting invalid_grant at the token step? 9 times out of 10 it’s PKCE.

You must send the same code_verifier at /oidc/token that you used to build the code_challenge for /oidc/authorize. Store it in the user’s session between the two steps and don’t regenerate it. If the verifier is fresh (or empty), the challenge can’t match and the exchange is rejected.

ErrorWhat it means & how to fix it
invalid_client
HTTP 401
Your app failed to authenticate at the token step. Server-side app: send the correct client_secret (as a form field, or HTTP Basic client_id:client_secret). No-server / SPA app: do not send a secret at all — identity is proven by PKCE. Lost your secret? Register a fresh app or ask us to reissue it.
invalid_grant
HTTP 400
The authorization code or PKCE didn’t validate. Check, in order: (1) the code_verifier matches the code_challenge you sent (see the box above — the usual cause); (2) you’re exchanging the code once and promptly — codes are single-use and expire quickly; (3) the redirect_uri at the token step is byte-for-byte identical to the one in the authorize request and your registration.
invalid_request · code_challenge (PKCE) required You didn’t send PKCE. Always include code_challenge + code_challenge_method=S256 on the authorize request — it’s mandatory. Also make sure scope is openid profile, not empty or undefined.
invalid_token at /userinfo You sent the id_token. The userinfo endpoint takes the access_token from the same token response — pass that as Authorization: Bearer instead.
Scanning the QR does nothing The person needs the idpass app installed and enrolled first (one-time DigiLocker verification). An un-enrolled phone can’t approve a login.

Still stuck after checking the above? Send us your client_id and the time of a failed attempt — we can see the exact reason server-side and tell you which of these it is.

Create your account & get a Client ID   Try the live demo   ⬇ Postman collection

Prefer Postman? Import the collection above, set your client_id/secret, and run the requests.