Skip to content

Add plugin-based API integration test rig for esignet-service (black-box, deployable as a CronJob with HTML reporting) #2120

Description

@nandhu-kumar

1. Problem Statement (Mandatory)

esignet-service embeds the ThunderID authorization engine (github.com/thunder-id/thunderid/pkg/thunderidengine) and supplies its own host providers / plugins (Actor, Authn, Role) plus executors (the engine's ConsentExecutor and custom ones such as MosipOtpExecutor) to deliver OIDC/OAuth sign-in. The engine is wired in esignet-service/cmd/esignet/main.go.

There is no automated way to validate that a deployed esignet-service instance — and the plugins running inside it — behaves correctly end to end:

  • Existing Go tests (6 files) are unit-level (internal/clientmgmt, internal/config, internal/log) and do not touch the engine-embedding surface or the plugins.
  • End-to-end coverage exists only as shell smoke scripts (scripts/oauth-smoke.sh, scripts/client-mgmt-smoke.sh) that must be run by hand against a locally started server, have no structured assertions, produce no report, and cannot be scheduled.
  • Nothing exercises a specific target environment (dev/QA/staging) on a schedule or produces a shareable pass/fail artifact for it.

As a result, plugin/flow regressions in a deployed environment (after a redeploy, a config change, or a pinned Thunder-fork bump) are only caught manually, if at all.

Purpose (Mandatory)

Build a black-box API integration test rig for esignet-service that:

  • Points at a real, running esignet-service via a configurable base URL (localhost for local runs, or a specific environment domain) — it behaves like an external OIDC/OAuth client / API test rig.
  • Calls the real esignet-service endpoints and lets the service exercise its own plugins and dependencies (Redis, Postgres, MOSIP IDA / SunbirdRC) — the rig injects nothing and fakes nothing.
  • Ships a small, self-contained OAuth/OIDC client (auth-code, PKCE, private_key_jwt client-assertion signing) built with the Go standard library and golang-jwt, replacing the ad-hoc bash/python signing in the smoke scripts. The ThunderID engine's own client/signing helpers live under its internal/ packages and are not importable from a separate module, so the rig does not depend on the engine (see Placement & dependency management).
  • Produces a custom HTML report capturing each request/response (with secrets redacted), status codes, latencies, assertions, and per-scenario pass/fail — plus a machine-readable JSON result for CI (JUnit XML is a planned follow-up, see Roadmap).
  • Is packaged as a container image and deployed as a Kubernetes CronJob so it runs on a schedule against a chosen environment and publishes the report.

Clarification on approach (Mandatory)

This is a black-box / API-rig design, not an in-process engine harness. Explicitly:

  • No fake HTTP servers, no miniredis, no DB test doubles, no in-process thunderidengine.New(...). The service under test is already deployed with its real dependencies.
  • The rig only needs network reachability to the target, valid test-client credentials, and a provisioned test identity in that environment.

Pre-Requisites (As Applicable)

  • A reachable, deployed esignet-service instance for the target environment; base URL configured via env var (e.g. TARGET_BASE_URL / ISSUER_URL). ISSUER_URL at the target must match the base URL the rig uses.
  • A test OAuth client provisioned in the target env: client_id, registered redirect_uri, and the private key / JWK used for client_assertion (private_key_jwt).
  • A test identity present in the target env for the provider it runs (AUTHN_PROVIDER = mock/catalog password user, or a MOSIP/Sunbird sandbox identity + OTP retrieval mechanism).
  • All plugin/provider inputs are supplied through configuration (env vars / mounted secrets), never hardcoded. In particular, for every provider the OIDC client_id and the subject individual id (the identity/UIN/VID the flow authenticates — e.g. the MOSIP individual id or the mock username) are passed via config, so the same rig binary runs against any environment and any provisioned identity by configuration alone. See Data Fields.
  • Deployment assets: container image build, a Kubernetes CronJob manifest (schedule + target-env selection via env/secret), and a location to publish/store the HTML report artifact.

Placement & dependency management (Mandatory)

  • The rig lives outside esignet-service as its own top-level Go module at esignet-api-test/, decoupled from the service so it can be built, versioned, and deployed as an independent CronJob image. It does not import esignet-service internals — it is a black-box HTTP client. (It is a sibling of the pre-existing top-level api-test/ directory, which is the legacy Java/biometric eSignet test rig and is unrelated.)
  • The rig has no dependency on the ThunderID engine. The engine's OAuth/OIDC client, client_assertion signing, and PKCE helpers all live under github.com/thunder-id/thunderid/internal/..., and Go forbids importing another module's internal/ packages from a separate module — so they cannot be reused by import. Instead the rig implements a small self-contained OAuth client with the standard library plus golang-jwt (the same primitives, ~one file each for PKCE and client-assertion signing).
  • Consequently there is no require/replace on thunderid and no engine pin to keep in sync across two go.mod files. The rig's go.mod has a single direct dependency (golang-jwt), which keeps builds fast and removes the pin-drift risk a shared engine dependency would introduce.
  • Container build: run go build in a multi-stage image (or in CI); with only public dependencies the module fetch needs no auth.

2. Goals (Mandatory)

  1. A standalone, config-driven Go API test rig placed outside esignet-service as its own top-level Go module at esignet-api-test/ that targets a base URL and runs entirely over real HTTP — no in-process engine, no fakes. See Placement & dependency management below.
  2. A self-contained OAuth/OIDC client for the auth-code + PKCE + client_assertion (private_key_jwt) flow, built with the standard library and golang-jwt, replacing the ad-hoc bash/python signing in the smoke scripts. (The ThunderID engine's own helpers are internal/-only and not importable, so they are re-implemented, not reused — see Placement & dependency management.)
  3. Scenario suites that call the real endpoints and validate plugin behavior end to end:
    • Well-known (both /.well-known/openid-configuration and the advertised jwks_uri / jwks.json), /oauth2/authorize, /flow/execute, GET /flow/meta, callback, /oauth2/token (auth-code + PKCE + client assertion), /oauth2/userinfo — and, when the discovery document advertises support, PAR (pushed_authorization_request_endpoint, RFC 9126; honouring require_pushed_authorization_requests) and DPoP-bound flows (dpop_signing_alg_values_supported, RFC 9449).
    • Step-wise flow execution with expected steps per scenario (no open-ended "loop until COMPLETE"). Each /flow/execute response is self-describing — flowStatus, the available data.actions[] (by ref), the required data.inputs[] (by identifier) — and the rig answers the advertised action/inputs rather than hardcoding action refs. But each scenario declares the steps it expects for the flow under test (e.g. the eSignet flow: login-method selection → credentials → consent → COMPLETE), and the run is asserted against that expectation: an unexpected extra step, a missing step, or an unexpected flowStatus fails the scenario instead of being silently driven through. A step may be pure action selection with no inputs — the deployed eSignet flow's first step offers acr_otp / acr_password / acr_bio / acr_kbi — and the scenario's expected step declares which offered action it selects (the auth factor under test); the rig never guesses among alternatives.
    • Per-step assertions on every /flow/execute response. Each step (authentication, OTP, consent) is asserted and recorded individually: HTTP 200, challengeToken present, no error and no data.fieldErrors, and well-formed actions[]/inputs[]; on COMPLETE the response must carry the assertion, which is then consumed by the callback. A step that progresses but returns an unexpected shape fails that step by name in the report, not just the scenario as a whole.
    • Flow metadata validation via the engine's GET /flow/meta endpoint (type=APP|OU, id, language, namespace): assert i18n (the languages list and key-value translations maps), design (theme and layout present), and application/client metadata (application.name, application.logoUrl; plus OU metadata where configured) — and that the data.meta component tree in /flow/execute responses references them consistently ({{t(...)}} i18n keys, {{meta(application.*)}} placeholders). This pins the contract the login UI consumes.
    • Consent handling: the flow may include a consent step (ConsentExecutor). When the target env has consent capture enabled, the consent step is one of the scenario's expected steps and is answered (auto-approve) and asserted like any other; the happy path does not assume credentials → COMPLETE directly.
    • Provider-specific positive flows delivered one per phase, driven by the target env's AUTHN_PROVIDER, with the OIDC client_id and the subject individual id supplied via config for every provider: mock/catalog password login first, then MOSIP OTP (send-OTP executor → KYC auth → exchange), then Sunbird KBI (see Roadmap).
    • Both a confidential client (private_key_jwt) and a public client (plain PKCE, no client assertion), mirroring oauth-smoke.sh.
    • Input-driven negatives (unknown client, wrong credential, reused code, mismatched PKCE, bogus code) — delivered after the positive flows are proven (see Roadmap), but fully independent of them at run time: every scenario provisions its own state end to end (own PKCE pair, own authorize/flow session), no scenario consumes another's output, so any subset — negatives without positives, one plugin without the rest — can run alone via profiles.
    • Client-management API (/client-mgmt/...) as covered today by client-mgmt-smoke.sh (see Roadmap).
    • Engine-surface sanity checks (implicit but first-class): the endpoints above are served by the embedded ThunderID engine itself — the esignet plugins only sit behind them. The discovery/JWKS scenario and several negatives (unknown client, reused code, mismatched PKCE, bogus code) involve no plugin at all, so they double as sanity checks of the engine's OIDC surface; a failure there after a pinned Thunder-fork bump is an expected, first-class regression signal pointing at the engine pin rather than the plugins. Engine internals remain out of scope (owned upstream, see Out of Scope).
  4. Token & claim validation (not just presence): fetch jwks_uri and cryptographically verify the id_token signature, iss, aud, and exp; assert state round-trips and (when sent) nonce matches; and assert /oauth2/userinfo returns the expected claims for the requested scope (from config), not merely a sub.
  5. Capture and report: record every request/response (headers with secrets redacted, body, status, latency) per step and generate a custom HTML report with per-scenario pass/fail and assertion detail; emit a machine-readable JSON result for CI (JUnit XML is a planned follow-up).
  6. Packaging & scheduling: build a container image and provide a CronJob manifest that runs the rig on a schedule against a specified environment; persist/publish the report artifact (see Operational Considerations).
  7. Profile-based runs and single-testcase selection. Scenario selection is two-dimensional: the provider mode (AUTHN_PROVIDER) gates which plugin flows apply, and a run profile (TEST_PROFILES, comma-combinable) selects the suite: smoke (preflight + discovery + one happy path), positive (all applicable happy paths), negative (input-driven negatives), plugin (provider-specific flows only), regression (everything applicable). Scenarios carry profile tags (a scenario may belong to several profiles) and are mutually independent, so any profile runs standalone — a scheduled smoke every hour and a nightly regression can share one image and differ only in config. Additionally, TEST_RUN (substring/regex over scenario names, like go test -run) runs a single scenario or subset, overriding profile selection; because every scenario embeds its own flow dependency as internal setup (via shared helpers, never shared state — e.g. the code-reuse negative performs its own full login first), a single testcase always executes with its complete flow chain and per-step assertions intact.
  8. A preflight environment-readiness check that fails fast and distinctly ("environment not ready") when the target is unreachable or the configured client/identity does not resolve — so scheduled runs don't misreport setup problems as plugin regressions.

3. Out of Scope (Mandatory)

  • In-process / hermetic engine tests, miniredis, DB test doubles, and fake MOSIP/Sunbird HTTP servers — explicitly not this approach.
  • Fault-injection negatives that require forcing a real dependency to fail (e.g. simulating a MOSIP outage); only input-driven negatives are in scope.
  • Provisioning/seeding the target environment itself (deploying esignet-service, creating clients/identities) — assumed to exist or handled by a separate bootstrap step.
  • Load / performance testing (performance-test/) and UI/browser tests (ui-test/).
  • Testing the ThunderID engine's own internals (flow runtime edge cases, declarative store adapter, themes/layouts — owned upstream). The engine's public OIDC/HTTP surface is still exercised implicitly, since the rig's endpoints are engine handlers (see Goals).
  • Retiring the existing shell smoke scripts / Postman collection (may be deprecated later as a follow-up).

4. Acceptance Criteria (Mandatory)

  • Given a reachable target esignet-service base URL with a provisioned test client and identity, When the rig runs the full authorization-code + PKCE flow over real HTTP, Then valid ID and access tokens are issued, /oauth2/userinfo returns the expected claims, and the scenario is reported as pass.
  • Given tokens are issued, When the rig validates them, Then the id_token signature verifies against the target's jwks_uri, its iss/aud/exp are correct, state round-trips, and the userinfo claims match those expected for the requested scope — not merely that a token/sub is present.
  • Given a target env with consent capture enabled, When the flow reaches a consent step, Then the rig auto-approves it and the happy path still completes and is reported pass.
  • Given the target env's configured AUTHN_PROVIDER, When the rig runs, Then only the applicable plugin scenarios execute (mosip → OTP flow, sunbird → KBI, catalog/mock → password) and validate the corresponding plugin behavior through the live endpoints.
  • Given a configured run profile (TEST_PROFILES), When the rig runs, Then only the scenarios tagged for that profile execute (smoke, positive, negative, plugin, regression — combinable), and any profile runs standalone because no scenario depends on another scenario's state.
  • Given a single-testcase filter (TEST_RUN), When the rig runs, Then only the matching scenario(s) execute, and each still runs its complete flow chain (its dependencies are internal setup steps, asserted and recorded like any other step).
  • Given a multi-step flow (authentication, OTP, consent), When the rig executes /flow/execute through the scenario's expected steps, Then every step's response is individually asserted (HTTP 200, challengeToken, no error/fieldErrors, well-formed actions/inputs) and the COMPLETE response carries the assertion; a malformed intermediate step fails that named step in the report, and a step sequence that differs from the scenario's expected steps fails the scenario.
  • Given the discovery document, When the rig configures itself, Then endpoints, PKCE method, client-auth methods, and feature gates (PAR, DPoP) are taken from the well-known document, and PAR/DPoP scenarios execute only when advertised (pushed_authorization_request_endpoint, dpop_signing_alg_values_supported), honouring require_pushed_authorization_requests.
  • Given the flow under test offers multiple login-method actions (e.g. acr_otp / acr_password / acr_bio / acr_kbi), When the rig executes its expected steps, Then it selects the action declared by the scenario (the auth factor under test) — never a guessed or first-listed alternative.
  • Given GET /flow/meta for the client under test, When the rig validates it, Then i18n (languages, key-value translations), design (theme, layout), and application/client metadata (application.name, application.logoUrl) are present and well-formed, and the data.meta component tree in /flow/execute references them consistently.
  • Given the target is unreachable or the configured client/identity does not resolve, When the preflight check runs, Then the run reports a distinct "environment not ready" outcome rather than a plugin-regression failure.
  • Given a completed run, When the rig finishes, Then it produces an HTML report containing, per scenario/step, the request and response (secrets redacted), HTTP status, latency, assertions, and an overall pass/fail summary, plus a machine-readable JSON result (JUnit XML is a planned follow-up).
  • Given the rig deployed as a CronJob against a chosen environment, When the schedule fires, Then it runs unattended and exits non-zero on failure. (Persisting/publishing the report artifact to durable storage is tracked under Operational Considerations / Roadmap, not the ephemeral emptyDir default.)

Business Rules

  • Given the rig authenticates as a client, When it builds client_assertion, Then it signs with the test client's registered key (client_id and key/kid from config) using its own golang-jwt-based signer, with aud = the target token endpoint.
  • Given tokens are issued, When validated, Then the OIDC iss equals the target env's configured issuer/base URL and the id_token signature verifies against the discovery jwks_uri.
  • Given an authorization request, When the callback/token round-trip completes, Then the returned state equals the value the rig sent (CSRF/round-trip check).
  • Given any report output, When it records requests/responses, Then secrets (client keys, tokens, OTPs, cookies) are redacted or masked — including any OTP extracted from a fetched mock-SMTP email body.

Exceptions & Error Handling

  • Given the target base URL is unreachable or returns 5xx, When the rig runs, Then it records a clear failure (endpoint, status, latency) and continues/reports rather than crashing.
  • Given invalid credentials or an invalid OTP, When the authn step runs, Then the service rejects it and the rig asserts the expected error and marks the negative scenario pass.
  • Given an unknown/unregistered client_id, When /oauth2/authorize is called, Then the request is rejected and asserted.
  • Given a mismatched PKCE verifier or a reused authorization code, When /oauth2/token is called, Then the token request is rejected and asserted.

Basic Flow (As Applicable)

  1. Rig loads config (target base URL, client_id, redirect_uri, client key/kid, provider mode, subject individual id + credential) from env / mounted secrets.
  2. Rig constructs its self-contained OAuth client (PKCE generation, client_assertion signing with the configured key).
  3. For each scenario it issues real HTTP calls: discovery → /oauth2/authorizestep-wise /flow/execute execution (resume, then answer each of the scenario's expected steps — credentials, OTP, consent — asserting every response, ending in flowStatus=COMPLETE) → /oauth2/auth/callback/oauth2/token/oauth2/userinfo.
  4. Each request/response is captured (redacted) with timing and assertion results.
  5. Rig aggregates results into an HTML report + JSON and exits non-zero on failure (for CI/CronJob visibility).

Scenarios (As Applicable)

  • Happy-path login for the target provider (catalog/mock password, MOSIP OTP, or Sunbird KBI) → tokens issued → userinfo claims verified.
  • Client-management CRUD against /client-mgmt/....
  • Input-driven negatives: bad credentials/OTP, unknown client, invalid PKCE, auth-code reuse.

Data Fields (As Applicable)

No new user-facing data fields — this is a testing/infrastructure change. All rig inputs are supplied via env vars / mounted secrets (never hardcoded), so switching environment, client, or identity is configuration-only. For every provider, the OIDC client_id and the subject individual id are passed through config.

Discovery-driven configuration (minimal config surface)

Everything the target publishes in /.well-known/openid-configuration is discovered at run time, not configured. The rig fetches the discovery document once per run and derives from it:

Derived at run time Discovery field Used for
Issuer issuer asserted as OIDC iss (expected value defaults to the target base URL)
Authorization / token / userinfo endpoints authorization_endpoint, token_endpoint, userinfo_endpoint all calls — no endpoint paths in config
Signing keys jwks_uri id_token signature verification; jwks.json scenario
PKCE method code_challenge_methods_supported pick S256; fail if absent
Client auth method token_endpoint_auth_methods_supported run private_key_jwt (confidential) and/or none (public) paths
Scopes to request scopes_supported validate the configured SCOPE is advertised before requesting it
Claims to expect claims_supported sanity-bound for EXPECTED_CLAIMS
PAR pushed_authorization_request_endpoint, require_pushed_authorization_requests enable/force the PAR scenario only when advertised
DPoP dpop_signing_alg_values_supported enable the DPoP scenario only when advertised
Grant/response types grant_types_supported, response_types_supported preflight sanity check for authorization_code

Extra configuration required (the full list — everything not in this table comes from well-known)

Only what cannot be discovered remains in config: the target address, the client's credentials, the test identities/data, run selection, and a few execution knobs.

Config input Env var Required Notes
Target base URL TARGET_BASE_URL yes esignet-service under test; may include a path prefix (e.g. https://<host>/v1/esignet) — all endpoint paths are appended to it; also the default expected iss (OIDC_ISSUER_URL only as an override)
OIDC client id CLIENT_ID yes primary test client provisioned in the target env (all providers)
Redirect URI REDIRECT_URI yes must be registered for the client
Client signing key + kid CLIENT_KEY_PATH, CLIENT_KID yes (confidential) PEM + kid for client_assertion; not needed for public-client-only runs
Requested scope SCOPE no (default openid profile email) validated against discovery scopes_supported
Provider mode AUTHN_PROVIDER yes mock | mosip | sunbird — gates plugin scenarios
Run profile(s) TEST_PROFILES no (default smoke) smoke | positive | negative | plugin | regression (comma-combinable; scenarios are multi-tagged)
Single-testcase filter TEST_RUN no substring/regex over scenario names (like go test -run); overrides profiles. Each scenario embeds its own flow dependency, so one testcase runs complete
Primary subject id TEST_USERNAME (mock) / INDIVIDUAL_ID (mosip/sunbird) yes (per provider) the identity the flow authenticates (mock username, or MOSIP/Sunbird UIN/VID)
Credential TEST_PASSWORD (mock) yes (mock) password for the mock/catalog flow
Test data set TEST_DATA_PATH no JSON/YAML file (mounted via Secret) with multiple clients and multiple identities for smoke/negative variants — see below
OTP source OTP_SOURCE no (default static) static | dynamican orthogonal input-resolution concern, deliberately NOT tied to any plugin/provider name: any flow whose steps request an otp input uses the configured source. static submits TEST_OTP; dynamic fetches the real OTP from the environment's mock-SMTP stream
Static OTP TEST_OTP no (default 111111) fixed OTP for OTP_SOURCE=static (sandbox identities are configured to accept it)
Dynamic OTP stream OTP_WS_URL yes if dynamic explicit WebSocket URL of the environment's mock-SMTP stream (commonly wss://smtp.<domain>/mocksmtp/websocket). Explicit config, not derived — the SMTP host can differ from the target host/domain, so it cannot be inferred from TARGET_BASE_URL
OTP recipient mailbox OTP_RECIPIENT_EMAIL yes if dynamic mailbox the mock-SMTP stream is keyed by
Expected claims EXPECTED_CLAIMS no claims (and optionally values) the userinfo/id_token response must contain for the requested scope; sanity-bounded by discovery claims_supported
HTTP timeout HTTP_TIMEOUT no (default 30s) per-request timeout
Report directory REPORT_DIR no (default out) where the HTML/JSON artifacts are written
Fail fast FAIL_FAST no (default false) stop after the first failed scenario

Multiple test-data inputs (smoke & negative variants)

Smoke and negative scenarios need more than one value per input, so alongside the single-value shorthands above, the optional test-data set (TEST_DATA_PATH) carries lists:

  • clients[] — multiple client entries with a role tag: the confidential client (key + kid), a public client (no assertion), and a deliberately unregistered client id for the unknown-client negative.
  • identities[] — multiple subject entries with an id-type tag — uin | vid | phone | username — plus per-identity credential/OTP expectation, and deliberately invalid entries for negatives.

Scenarios pick entries by role and id type: smoke uses the primary client + identity; plugin positives run each id type applicable to the provider (e.g. MOSIP OTP by UIN and by VID, phone where the env supports it); negatives use the unregistered client and invalid identities. The single-value env vars remain the shorthand for the primary entries, so simple runs need no data file.

Non-Functional Requirements (As Applicable)

  • Fully config-driven target: switching environments (localhost ↔ env domain) requires only configuration, no source changes.
  • Secrets supplied via env vars / Kubernetes secrets and redacted in all report output.
  • Runnable both locally (single command against localhost) and as a container/CronJob against an environment.
  • Self-contained OAuth client (no ThunderID engine dependency), so the rig builds and runs independently of the service's engine pin.
  • HTML report is self-contained (portable artifact) and the run emits a machine-readable JSON result for CI gating.

Operational Considerations (As Applicable)

  • Non-prod / sandbox targets. The rig performs real authentications and (for MOSIP) triggers real OTP sends and audit-log entries, so it is intended for dev/QA/sandbox environments.
  • Preflight environment-readiness check. Before running scenarios, verify the target is reachable and the configured client/identity resolve; report a setup failure distinctly ("environment not ready") so scheduled runs don't misattribute missing provisioning to a plugin regression.
  • Failure alerting. The CronJob exits non-zero (visible to Kubernetes) and emits a machine-readable result; wire an alert on failure (Slack/webhook/email, or a Prometheus pushgateway metric with pass-rate over time) so red runs are actually noticed.
  • Report persistence & publishing. The manifest's default emptyDir is ephemeral — the report dies with the pod. For real use, persist to a PVC or object store (S3/MinIO) or publish to a static location so the artifact survives. (Roadmap.)
  • Transient-failure handling. Against a live environment, distinguish transient network errors from assertion failures; use bounded in-rig retries for idempotent GETs rather than relying solely on Job backoffLimit restarts, to reduce false reds.
  • Secret hygiene. The client signing key is mounted from a Kubernetes Secret and should be rotated periodically; access to the mounted key and to report artifacts should be restricted (reports may echo redacted-but-sensitive material).
  • Test-identity residue. Each run creates sessions/consent records in the target env; confirm this is harmless for the sandbox or add a cleanup step.
  • CI & lint for the new module. As a separate go.mod, add a dedicated CI job (go build / go test / go vet) and a .golangci.yml mirroring the service's linters, so the rig is built on every change and doesn't rot. Pin the container image by tag/digest rather than :latest.

Definition of Done (Mandatory)

Delivery is strictly phased: the framework lands first (this issue); then Phase 2 delivers one plugin's positive flow together with the report so the whole rig is verified end to end; then the remaining plugins' positive flows follow one phase at a time; and only after all positive flows are proven do negatives and the remaining scope follow (see Roadmap).

Phase 1 — framework (this issue):

  • Code implemented (config-driven API test rig module with a self-contained OAuth client; no engine dependency)
  • Config loading & validation (target base URL, client, provider mode, identity — all via env/secrets)
  • Self-contained OAuth client: PKCE generation + client_assertion (private_key_jwt) signing with golang-jwt
  • Step-wise flow execution — the rig answers the actions/inputs each /flow/execute response advertises (no hardcoded action refs), with the expected steps declared per scenario and consent auto-approve support; no open-ended loop-until-COMPLETE
  • Request/response capture with secret redaction
  • Profile-based scenario selection (TEST_PROFILES: smoke/positive/negative/plugin/regression, combinable) with mutually independent scenarios
  • Discovery-driven configuration: endpoints, PKCE method, client-auth methods, and PAR/DPoP feature gates read from /.well-known/openid-configuration; config reduced to the non-discoverable minimum (target, client credentials, identity, profile)
  • Preflight environment-readiness check reports "environment not ready" distinctly from a plugin regression
  • Unit tests added (rig helpers: config, secret redaction, PKCE + client_assertion signing) and passing (go test ./...)
  • Container image built and Kubernetes CronJob manifest added
  • CI job + .golangci.yml for the new module
  • Documentation updated (how to run locally and how to deploy/schedule against an env)

Phase 2 — first plugin positive flow + report (verifies the framework end to end):

  • Mock/catalog full auth-code + PKCE + client_assertion happy path (authorize → step-wise flow execution → callback → token → userinfo) against a real running esignet-service, AUTHN_PROVIDER=mock
  • Confidential + public client happy paths both covered
  • Consent step auto-approved when the target env has consent capture enabled
  • Per-step assertions on every /flow/execute response (HTTP 200, challengeToken, no error/fieldErrors, well-formed actions/inputs; assertion present on COMPLETE), each step named in the report
  • Flow metadata validated via GET /flow/meta for the client under test: i18n (languages, translations), design (theme, layout), application/client metadata (application.name, application.logoUrl)
  • id_token validated against jwks_uri (signature + iss/aud/exp), state round-trip asserted, and userinfo claims checked against expected (config, EXPECTED_CLAIMS)
  • HTML report generation implemented and verified against this real flow (per-step request/response, redaction, latency, assertions); JSON result emitted; non-zero exit on failure

Phase 3 — MOSIP OTP positive flow: send-OTP executor → KYC auth → exchange, AUTHN_PROVIDER=mosip, OTP via static and dynamic (mock-SMTP) sources — the OTP source is plugin-agnostic config; run per id type from the test-data set (UIN and VID, phone where the env supports it); verified via the same report.

Phase 4 — Sunbird KBI positive flow: AUTHN_PROVIDER=sunbird, KBI answers from config; verified via the same report.

Phase 5+ — negatives & remaining scope (delivered after the positive flows; independently runnable via profiles): input-driven negatives, PAR and DPoP scenarios (gated on discovery advertising them), client-management CRUD, JUnit XML, report publishing, alerting — see Roadmap.

5. References (Mandatory)

Documentation (As Applicable)

  • esignet-service/README.md and .env.example — architecture, AUTHN_PROVIDER modes, configuration, and issuer/base-URL rules.

UI/UX Design Link (As Applicable)

Not applicable (HTML report layout to be defined during implementation).

Git Discussion Link (As Applicable)

  • Upstream engine (embedded by esignet-service, but its OAuth-client/signing/PKCE helpers are internal/-only and therefore not importable by this separate module): https://github.com/thunder-id/thunderidpkg/thunderidengine. The rig re-implements the equivalent client primitives with the standard library + golang-jwt.
  • Existing bash reference the rig supersedes: esignet-service/scripts/oauth-smoke.sh (same flow: authorize → flow/execute → callback → token → userinfo).

Related Issues / PRs (As Applicable)

  • Embedded esignet-service with new thunderidengine #2028 — Embedded esignet-service with new thunderidengine.
  • Existing smoke assets to be superseded/reused: scripts/oauth-smoke.sh, scripts/client-mgmt-smoke.sh, scripts/smoke-client-lib.sh, postman/embedder-positive-flow.json.

6. Roadmap / Phased Delivery

The delivery order is deliberately framework first, then one positive flow per plugin, then everything else — each phase must be green (verified via the report against a real environment) before the next starts:

  • Phase 1 — framework (this issue): the module, config (discovery-driven, minimal surface), self-contained OAuth client, step-wise flow execution with per-step assertions, profile-based scenario selection, capture/redaction, preflight check, container image + CronJob manifest, CI/lint.
  • Phase 2 — mock/catalog positive flow + report: the first end-to-end plugin scenario — with per-step /flow/execute assertions and flow metadata validation — plus the HTML/JSON report, verifying the whole rig (framework → real service → report artifact) works before further plugins are added.
  • Phase 3 — MOSIP OTP positive flow.
  • Phase 4 — Sunbird KBI positive flow.
  • Phase 5+ — negatives and remaining scope: input-driven negatives (bad credentials/OTP, unknown client, invalid PKCE, auth-code reuse), PAR + DPoP scenarios, client-management CRUD, JUnit XML, report publishing, alerting. Delivered after the positives, but runnable standalone via profiles.

Each plugin phase just adds a provider-scoped Scenario on the same framework, driven entirely by the config inputs above (notably the per-provider client_id and individual id):

  • MOSIP OTP flow (Phase 3) — send-OTP executor → KYC auth → exchange, gated on AUTHN_PROVIDER=mosip; individual id + OTP source from config. The MOSIP Authenticate plugin validates the OTP against real MOSIP IDA (credentials["otp"]), so the rig must obtain the actual OTP. The OTP source is a flow-input-resolution concern, deliberately not tied to any plugin name — whenever a flow step requests an otp input, the configured source supplies it, whichever provider owns the flow. Both sources are supported:
    • static — a fixed OTP (TEST_OTP); sandbox identities are configured to accept it.
    • dynamic — mirror the existing MOSIP Java rig (OTPListener + NotificationListener in mosip-functional-tests / the legacy api-test): a WebSocket client connects to the environment's mock-SMTP stream at the configured OTP_WS_URL (commonly wss://smtp.<domain>/mocksmtp/websocket, but the SMTP host can differ from the target host, so the URL is explicit config rather than derived), receives each captured email, regex-extracts the 6-digit code (\b(\d{6})\b), and keys it by recipient mailbox. The rig starts the listener before the send-OTP step, then retrieves the code (poll with timeout) and submits it in the auth step. Requires an env that runs mocksmtp (sandbox/QA); it cannot work against locked-down prod where OTP reaches real inboxes. Adds a WebSocket client dependency (e.g. github.com/coder/websocket).
  • Sunbird KBI flow (Phase 4) — knowledge-based identity, gated on AUTHN_PROVIDER=sunbird; individual id + KBI answers from config.
  • Input-driven negatives (Phase 5+) — bad credentials/OTP, unknown client, invalid PKCE, auth-code reuse — added only after all positive flows are proven, so a red negative can't be confused with an unproven framework. Driven from the test-data set's unregistered-client and invalid-identity entries (per id type: uin/vid/phone/username). Independent of the positive scenarios at run time (own sessions, no shared state) and selectable alone via TEST_PROFILES=negative.
  • PAR + DPoP scenarios (Phase 5+) — Pushed Authorization Requests (RFC 9126): push the authorization request to the advertised pushed_authorization_request_endpoint, then authorize via request_uri; honour require_pushed_authorization_requests when the env mandates it. DPoP (RFC 9449): send DPoP proof JWTs on token/userinfo and assert token binding for clients configured with dpopBoundAccessTokens. Both are gated on the discovery document advertising support, so the same rig runs against targets with or without these features.
  • Client-management CRUD (Phase 5+)/client-mgmt/... parity with client-mgmt-smoke.sh.
  • JUnit XML output (Phase 5+) — in addition to JSON, for CI systems that ingest JUnit.
  • Report publishing & persistence (Phase 5+) — attach/publish the HTML artifact from CI and the CronJob (PVC or object-store sidecar) instead of the ephemeral emptyDir default.
  • Failure alerting & trend metrics (Phase 5+) — notify on red runs (Slack/webhook/email) and/or push a pass-rate metric to Prometheus for SLO tracking over time.

Metadata

Metadata

Assignees

Type

Fields

No fields configured for Task.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions