OIDC Integration Guide

This guide explains how an external application authenticates citizens using
the RDC / DRC platform. The platform exposes a standard OpenID Connect
provider, so any compliant OIDC/OAuth 2.0 client library works.

This page is the integrator-facing subset: what you need to build a
relying party. The internal mechanics of the provider (interactions, MMA,
consent, cookies) are documented in the internal engineering docs and are not
published here.

At a glance

Item Value
Provider (OP) tri-ekyc — a standards-compliant OpenID Connect / OAuth 2.0 provider
Flow Authorization Code + PKCE (S256) — the only supported flow
Login UI drc-pass (citizen enters National ID → password → MFA → consent)
Token signing RS256 (verify with the published JWKS)
UserInfo endpoint Disabled — all claims are delivered in the ID token
Grant types authorization_code, refresh_token

1. Register your client

Client registration is static today — there is no public dynamic
registration endpoint. To onboard, request the platform team to add your client
to tri-ekyc/src/modules/oidc/configs/static-client.ts with:

Field Description
client_id Unique identifier for your app
client_secret Confidential secret (store server-side only)
redirect_uris Exact callback URLs — the redirect_uri you send must match one of these exactly
grant_types ['authorization_code', 'refresh_token']
scope Space-separated scopes you are allowed to request (see §3)

You will receive a client_id and client_secret per environment.

2. Discover the endpoints

Always read endpoints from the discovery document rather than hardcoding them:

GET {ISSUER_URL}/.well-known/openid-configuration

ISSUER_URL is {base}/api/oidc for the target environment:

Environment ISSUER_URL (example — confirm with platform team)
Local https://localhost:3443/api/oidc
Develop https://<develop-gateway>/api/oidc
Staging https://<staging-gateway>/api/oidc
UAT https://<uat-gateway>/api/oidc

Key endpoints (also resolvable from discovery):

Purpose Endpoint
Discovery GET /api/oidc/.well-known/openid-configuration
JWKS (verify tokens) GET /api/oidc/jwks
Authorization GET /api/oidc/auth
Token POST /api/oidc/token
End session (logout) GET|POST /api/oidc/session/end

3. Scopes & claims

Request openid plus whatever identity data you need. Add offline_access to
receive a refresh token.

Scope Claims returned (in the ID token)
openid sub, iss, aud, exp, iat
email email, isEmailVerified
phone phone, phoneCode, isPhoneVerified
identity id, firstName, lastName, dob, gender, address, province, city, passCreateTime, passActiveTime, version, …
offline_access Enables a refresh_token

Because UserInfo is disabled, do not call a /userinfo endpoint — read
claims straight from the verified ID token.

4. The flow

Your App                         tri-ekyc (OP)              drc-pass (Login UI)
   │                                  │                            │
   │── GET /api/oidc/auth ───────────>│                            │
   │   client_id, redirect_uri,       │                            │
   │   scope, state, nonce,           │── 302 ────────────────────>│
   │   code_challenge (S256)          │   citizen logs in          │
   │                                  │   (National ID → pwd →     │
   │                                  │    MFA → consent)          │
   │<── 302 {redirect_uri}?code&state ┤                            │
   │                                  │                            │
   │── POST /api/oidc/token ─────────>│                            │
   │   code, code_verifier,           │                            │
   │   client_id+secret, redirect_uri │                            │
   │<── { id_token, access_token,     │                            │
   │      refresh_token? } ───────────┤                            │
   │                                  │                            │
   │  verify id_token (RS256 via JWKS)│                            │
   │  check state + nonce             │                            │

Steps:

  1. Authorize — generate a PKCE code_verifier + code_challenge (S256),
    a random state and nonce; redirect the user to /api/oidc/auth.
  2. Citizen authenticates — tri-ekyc redirects to drc-pass; the citizen enters
    their National ID, password, MFA (OTP or face), and grants consent.
  3. Callback — the OP redirects back to your redirect_uri with code and
    state. Verify state matches.
  4. Token exchangePOST /api/oidc/token with the code, the PKCE
    code_verifier, your client credentials, and the same redirect_uri.
  5. Verify — validate the ID token signature (RS256 via JWKS), iss, aud,
    exp, and nonce. Read claims from it.

kyc_type (optional)

You may pass kyc_type on the authorize request to control required auth
methods:

kyc_type Required methods
1 MFA only (skip password)
omitted / other Password and MFA

5. Reference implementation (Node.js, openid-client v6)

A complete working example lives in demo-app/ (Koa + openid-client v6). The
core is:

import * as oidc from 'openid-client';

// Discover + configure the client
const config = await oidc.discovery(
  new URL(ISSUER_URL),   // e.g. https://localhost:3443/api/oidc
  CLIENT_ID,
  CLIENT_SECRET,
);

// --- /login ---
const code_verifier = oidc.randomPKCECodeVerifier();
const code_challenge = await oidc.calculatePKCECodeChallenge(code_verifier);
const nonce = oidc.randomNonce();
const state = oidc.randomState();
// persist { code_verifier, nonce, state } in the user's session

const authUrl = oidc.buildAuthorizationUrl(config, {
  redirect_uri: REDIRECT_URI,
  scope: 'openid email identity phone',  // add offline_access for refresh tokens
  code_challenge,
  code_challenge_method: 'S256',
  state,
  nonce,
});
// redirect the browser to authUrl

// --- /callback ---
const tokens = await oidc.authorizationCodeGrant(config, currentUrl, {
  pkceCodeVerifier: code_verifier,
  expectedNonce: nonce,
  expectedState: state,
  idTokenExpected: true,
});
const claims = tokens.claims();   // identity data from the ID token

Running locally against a self-signed cert? The demo sets
NODE_TLS_REJECT_UNAUTHORIZED=0. Never do this outside local dev.

To run the demo end-to-end:

# Provider + login UI
cd tri-ekyc  && npm run local      # OP on :3001 (SSL proxy :3443)
cd drc-pass  && yarn dev           # login UI on :9443

# Demo relying party
cd demo-app/examples/nodejs
npm install
# edit ../config.json (CLIENT_ID, CLIENT_SECRET, ISSUER_URL, REDIRECT_URI, SCOPES)
NODE_TLS_REJECT_UNAUTHORIZED=0 npm start   # visit http://localhost:3080

6. Refresh tokens

Request the offline_access scope to receive a refresh_token. Exchange it at
the token endpoint with grant_type=refresh_token to obtain new tokens without
re-prompting the citizen. Store refresh tokens encrypted, server-side only.

7. Logout

End the citizen’s session at the provider via /api/oidc/session/end
(RP-initiated logout). Pass id_token_hint and an optional
post_logout_redirect_uri (must be pre-registered).

8. Security checklist

  • Confidential client: client_secret lives only on your server, never in the browser.
  • Always use PKCE (S256) — required.
  • Validate state (CSRF) and nonce (replay) on the callback.
  • Verify the ID token signature against the JWKS, plus iss, aud, exp.
  • redirect_uri and post_logout_redirect_uri must be exact registered matches.
  • Use TLS everywhere; never disable cert validation outside local dev.
  • Read claims from the ID token — there is no UserInfo endpoint.

Reference integrations in this repo

Project What it shows
demo-app/ Minimal OIDC relying party (Koa + openid-client v6)
rdc-trident-simulate/ Real portal logging citizens in via tri-ekyc OIDC (multi-env)