Reproducing the OFCS run
This page is the runbook for running the OpenID Foundation Conformance Suite (OFCS) against the library yourself. OFCS conformance status records what the suite saw on the latest run; this page covers how to reproduce it and which library options each plan exercises.
The status page summarizes the run captured at 2026-11-04T19:51:48Z on core commit 9e8d5153 against OFCS release-v5.3.4. Its JSON snapshot is a local, gitignored artifact and does not record the image tag or digest; the pin you reproduce with is the conformance/docker-compose.yml default, release-v5.3.4.
Start at OFCS conformance status for the PASS / REVIEW / SKIPPED / WARNING counts. Read on for the op.With... options your own OP needs for the same plans to pass.
Personal project, not certified
The numbers on this site are reproducible snapshots — this page tells you how to reproduce them. They are not a substitute for paid OpenID Foundation certification. Run the suite locally to verify your own embedder configuration; do not cite local PASS counts as certification.
Prerequisites
- Docker with Compose v2 (the harness invokes
docker compose, notdocker-compose). - OpenSSL for cert generation (
scripts/conformance.sh certs). - Go 1.26 or later;
go.modpins thego1.27.3toolchain. curlandpython3for the seed-plans / drive scripts.
The OFCS image is pulled from the OpenID Foundation registry at the default tag release-v5.3.4 in conformance/docker-compose.yml. You do not need a separate OFCS checkout or mvn install step.
One-command quick start
From a clean clone of libraz/go-oidc-provider:
make conformance-upThat target chains four steps:
scripts/conformance.sh certs— self-signed RSA-2048 cert for the OP listener (coveringlocalhost+host.docker.internal), a pair of client certs the FAPI 2.0 mtls / mtls2 plan slots use, and a JKS truststore the OFCS container mounts so the suite trusts the OP without manual cert work.scripts/conformance.sh ofcs-up—docker compose up -dbrings up the OFCS containers and waits forhttps://localhost:8443to answer.scripts/conformance.sh op-up— builds and launchescmd/op-demoonhttps://127.0.0.1:9443, seeded with redirect URIs for every plan alias the suite will use.scripts/conformance.sh seed-plans— POSTs each plan template fromconformance/plans/to the OFCS REST API and prints the resulting plan IDs.
Tear everything down (including the mongo volume that holds OFCS state) with:
make conformance-downPlans, op-demo flags, and the wiring they activate
OFCS test plans differ in which library features they exercise. The table below maps each plan to the cmd/op-demo invocation that activates the matching wiring, and to the source file you can read end-to-end to see exactly what op.With... options that profile turns on.
| OFCS plan | op-demo invocation | Reference wiring |
|---|---|---|
oidcc-basic-certification-test-plan | make conformance-op-up (the basic profile, which declares profile.OIDCCore) | cmd/op-demo/main.go — commonOptions |
oidcc-config-certification-test-plan | make conformance-op-up (the basic profile, which declares profile.OIDCCore) | cmd/op-demo/main.go — commonOptions |
oidcc-dynamic-certification-test-plan | make conformance-op-up (DCR on by default in conformance.sh) | cmd/op-demo/main.go — op.WithDynamicRegistration branch |
oidcc-formpost-basic-certification-test-plan | make conformance-op-up (the basic profile, which declares profile.OIDCCore) | cmd/op-demo/main.go |
oidcc-rp-initiated-logout-certification-test-plan | make conformance-op-up (DCR on by default in conformance.sh) | cmd/op-demo/main.go |
oidcc-backchannel-rp-initiated-logout-certification-test-plan | make conformance-op-up (DCR on by default in conformance.sh) | cmd/op-demo/main.go |
fapi2-security-profile-final-test-plan | OP_PROFILE=fapi2-baseline make conformance-op-up | cmd/op-demo/profile_fapi2.go — fapi2BaselineOptions |
fapi2-message-signing-final-test-plan | OP_PROFILE=fapi2-message-signing make conformance-op-up | cmd/op-demo/profile_fapi2.go — fapi2MessageSigningOptions |
fapi-ciba-id1-test-plan | OP_PROFILE=fapi-ciba make conformance-op-up | cmd/op-demo/profile_fapi_ciba.go — fapiCIBAOptions |
scripts/conformance.sh op-up starts op-demo with -enable-dcr unless you set OP_ENABLE_DCR=0, and with the in-memory store unless you set OP_STORE=composite (MySQL for the durable substores, Redis for the volatile ones, via OP_MYSQL_DSN and OP_REDIS_DSN). The composite store captures a baseline against the storage shape a deployment runs; reset both engines between captures, because records left from a previous run change what the replay and reuse modules observe.
The two FAPI 2.0 rows are the suite's Final plans for the Security Profile and Message Signing. Their results describe the pinned suite's coverage of those specifications; a regression run is not a certification.
Every per-profile file in cmd/op-demo/ opens with a comment block enumerating the options that profile activates, the spec sections each option satisfies, and the OFCS modules the wiring is in service of. Read the file for the plan you intend to run before you copy patterns into your own OP.
What to wire into your own OP
The per-profile files in cmd/op-demo/ are the canonical wiring reference. The following sections summarise them so you can cross-check against your own embedder.
OIDC Core 1.0 (basic, config, formpost, RP-initiated logout)
The vanilla profile declares op.WithProfile(profile.OIDCCore): the OIDC Basic plan drives the code flow without PKCE, which the OAuth 2.1 default would reject. The base options live in commonOptions in cmd/op-demo/main.go and apply to every plan:
op.WithIssuer(...)— the issuer URL discovery and ID tokens advertise.op.WithStore(...)— the storage adapter (op/storeadapter/inmemfor the demo; production uses sql / redis adapters).op.WithKeyset(...)— the signing keyset (one key in the demo).op.WithSigningAlgorithm(...)— the one issuing algorithm,ES256unless the demo is started forRS256.op.WithCookieKeys(...)— the AEAD key for cookie encryption.op.WithMountPrefix(...)— the URL prefix the OP handler is mounted under.op.WithStaticClients(...)— the seed clients (public + confidential trio).op.WithLoginFlow(...)— the login orchestration; demo usesop.PrimaryPassword.op.WithFeature(feature.JAR)— opt-in JAR for non-FAPI profiles, so discovery advertises request-object signing algorithms (withoutnone) and the OFCS request-object modules either pass or skip cleanly.op.WithClaimsSupported(...)andop.WithACRValuesSupported(op.AAL1.ACRURI())— the claim list and the oneacrthe default policy satisfies at the demo's AAL1 login.op.WithAllowPrivateNetwork(op.OutboundJWKS)andop.WithAllowPrivateNetwork(op.OutboundJAR)— the suite hosts its relying-partyjwks_uriandrequest_uriendpoints on loopback. A production OP leaves them off unless its relying parties really live on a private network.
The conformance harness enables DCR by default. For the dynamic plans (oidcc-dynamic, *-rp-initiated-logout-*), op.WithDynamicRegistration(op.DynamicRegistration{...}) is added so /register is mounted and an Initial Access Token is minted at startup. Set OP_ENABLE_DCR=0 only when you intentionally want to measure a non-DCR discovery surface. The demo registers with Open: true because OFCS has no slot for an Initial Access Token in its plan templates, and it also prints an IAT to stdout. In production leave Open false, issue IATs through Provider.IssueInitialAccessToken, and keep them out of logs; see Dynamic client registration.
FAPI 2.0 Baseline
Set OP_PROFILE=fapi2-baseline when starting op-demo. The profile-specific additions, captured in fapi2BaselineOptions, are:
op.WithProfile(profile.FAPI2Baseline)— auto-enables PAR (RFC 9126), JAR (RFC 9101), and the FAPI alg lockdown.op.WithFeature(feature.DPoP)— sender-constrained access tokens. FAPI 2.0 requires DPoP or mTLS; the demo picks DPoP because the OFCS templates shipsender_constrain=dpopfor the fapi2-* plans.
The demo also seeds two op.PrivateKeyJWTClient records (demo-fapi, demo-fapi-2) holding the public halves of the OFCS test client JWKSes. The matching private halves live in conformance/plans/fapi2-*.json so OFCS can sign private_key_jwt assertions.
For deeper context on each option, see Concepts → FAPI, Concepts → DPoP, and Use case: FAPI 2.0 Baseline.
FAPI 2.0 Message Signing
Set OP_PROFILE=fapi2-message-signing. Everything Baseline does plus:
op.WithProfile(profile.FAPI2MessageSigning)— auto-enables JARM (signed authorization response) on top of PAR / JAR / alg lockdown.op.WithFeature(feature.DPoP)— same sender constraint as Baseline.op.WithDPoPNonceSource(...)— whenprofile.FAPI2MessageSigningorprofile.FAPICIBAis active with DPoP, the library requires a source for server-supplied DPoP nonces;op.Newrefuses the configuration without one (op/options_validate.go). This is a library configuration requirement, not a mandate in the FAPI 2.0 Message Signing Final specification: that document has no §5.3.4, and §5.6 covers ID-token signing. If mTLS is explicitly selected instead of DPoP, no DPoP nonce source is needed. The demo uses the in-memory rotator; a multi-instance OP must back the source with a shared store.
FAPI-CIBA
Set OP_PROFILE=fapi-ciba. CIBA differs from the fapi2-* plans in three significant ways:
- The OFCS
fapi-ciba-id1plan inherits FAPI 1.0's hardcoded cert-bound access-token requirement and exposes no sender-constraint variant. The demo therefore wiresop.WithFeature(feature.MTLS)instead offeature.DPoP; either satisfies the profile's "DPoP or mTLS" requirement. Because mTLS is chosen, no DPoP nonce source is needed. - CIBA clients never visit the authorization endpoint. Their seeds carry an empty
redirect_uriset and a narrowed grant list (CIBA URN +refresh_token), and the provider is built withop.WithGrants(AuthorizationCode, CIBA, RefreshToken)andop.WithCIBAGrant(op.CIBAGrant{PollInterval: time.Second, MaxPollViolations: 50, ...}), a one-second poll interval and a raised violation cap, so the plan's rapid polling does not trip the strike limit. - The OFCS plan drives the
auth_req_idpoll loop without a real authentication device. The demo wraps the in-memoryCIBARequestStorewithcibaAutoApproveStore, which schedules an out-of-bandApprove()after a configurable delay so the OP can simulate a device callback. Production embedders MUST triggerApprovefrom the user's actual authentication-device callback, never from inside the OP process.
The full wiring (including the auto-approve substore and the hint resolver) lives in cmd/op-demo/profile_fapi_ciba.go.
Driving the run
With the stack up and the plans seeded, you can drive modules in three ways.
Single browser-driven flow
Useful when iterating on one module from the OFCS UI:
scripts/conformance.sh drive 'https://host.docker.internal:9443/oidc/auth?...'Copy the Browser URL OFCS prints in the test detail panel.
Multiple modules at once
Pass a plan ID printed by seed-plans:
scripts/conformance.sh batch <plan-id> oidcc-server oidcc-userinfo-get ...The batch sub-command picks the right driver hint per module name (e.g. it sets OFCS_DRIVE_REJECT=1 for *user-rejects-authentication* modules so the demo cancels consent).
Every module in every plan
make conformance-baseline LABEL=local-checkThat runs every module in every seeded plan, restarting op-demo with the right profile between plans, and writes a deterministic JSON snapshot to conformance/baselines/<UTC-date>-local-check.json. Use this when verifying a refactor does not regress against a previously captured baseline.
Reading the result
OFCS reports four main terminal result values, and the harness also preserves advisory WARNING results.
- PASSED — the module ran and observed the spec-required behaviour.
- REVIEW — the module ran but a person must verify visual or out-of-band behaviour the harness cannot capture (consent UI strings, error page screenshots). Not a failure. The headless harness records
REVIEWas-is; certification through the OpenID Foundation would have a person clear each one in the OFCS UI. - SKIPPED — the module depends on something this OP does not advertise in discovery or per-client metadata. For example, a per-client
RS256negative test skips when the client metadata declaresPS256as its signing alg. Not a failure. - WARNING — the module reached a non-failed advisory result. The current status page tracks these separately from FAILED.
- FAILED — the module did not reach the suite's expected result. It blocks the release unless
make conformance-release-verifymatches it to a reviewed, unexpired exclusion (see Release gate); OFCS conformance status has the current breakdown. - No verdict — the module never reached a terminal state (
WAITINGorCONFIGUREDafter the runner's idle bound). Only anunreachable_verdictsentry with evidence can account for it.
OFCS conformance status lists which modules currently sit in REVIEW or SKIPPED, and why.
Release gate
baseline-diff tells you what changed; it is not the release sign-off. make conformance-release-verify is the strict gate:
make conformance-release-verify \
BASELINE_REFERENCE=conformance/baselines/<approved-reference>.json \
BASELINE_CANDIDATE=conformance/baselines/<candidate>.jsonThe candidate must contain exactly the reference's plan and module catalog, with no empty status or result. Every module must be FINISHED / PASSED, unless conformance/release-exclusions.json accounts for it. The manifest must be tracked and unchanged from HEAD, and each of its three sections covers one kind of outcome:
| Outcome | Manifest section | How an entry matches | What it must also carry |
|---|---|---|---|
| FAILED, including interrupted modules | exclusions | exact plan, module, status and result | the reason it fails by design |
| REVIEW or SKIPPED | accepted_outcomes | a class rule on the result and a module-name pattern (oidcc-rp-initiated-logout-*); intermittent marks a module the suite sometimes concludes as PASSED | the reason the suite cannot decide |
No verdict (WAITING, CONFIGURED) | unreachable_verdicts | exact plan, module and status | evidence of what was tried in a standalone run |
Every entry also names an owner and an exclusive ISO expiry date. A missing, duplicate, stale or expired entry is itself a blocker, so an exception cannot outlive the review that granted it. An OFCS catalog change, such as a renamed module after a pin bump, fails the same gate until a new approved reference is captured.
Capturing and diffing a baseline
The harness is built around the cycle "freeze the current state, then verify a change does not regress against it":
make conformance-up
make conformance-baseline LABEL=pre-change
# … make changes …
make conformance-baseline LABEL=post-change
make conformance-baseline-diff \
BASELINE_OLD=conformance/baselines/<utc>-pre-change.json \
BASELINE_NEW=conformance/baselines/<utc>-post-change.jsonbaseline-diff exits non-zero on any module that lost PASSED between the two snapshots. The diff classifies modules as:
- regressions — were
PASSED, now anything else (the gate). - fixes — were not
PASSED, nowPASSED. - non-pass churn — both states are non-
PASSEDbut differ (e.g. REVIEW → SKIPPED). - catalog drift — module appears in only one snapshot (typical when bumping the OFCS pin).
Snapshot files are gitignored — they record an environment-specific moment in time and would churn between machines and OFCS releases. If you want to keep a single canonical snapshot, copy it out from under conformance/baselines/ to a path you commit elsewhere.
The module list is queried from OFCS at runtime, so a catalog drift between OFCS releases is captured automatically. Set BASELINE_FILTER to a regex when you only want to baseline a subset:
BASELINE_FILTER='^oidcc-' make conformance-baseline LABEL=oidcc-onlyBumping the pinned OFCS release
conformance/docker-compose.yml carries the canonical pin as the default of IMAGE_TAG. To move forward for one run:
IMAGE_TAG=release-vX.Y.Z make conformance-upOFCS test logic drifts between releases: modules are renamed, merged or tightened. After any bump, recapture every plan's baseline and diff it against the previous one. A new SKIPPED or REVIEW row is usually a catalog rename, not a regression; check the renamed module's intent first. To make the new default permanent, edit the compose file and commit it.
Re-using the wiring in your own OP
The per-profile files in cmd/op-demo/ are deliberately small and well-commented so you can lift their structure directly into your own embedder:
- Concepts → FAPI — what
op.WithProfile(profile.FAPI2Baseline)actually changes. - Concepts → DPoP — how DPoP slots into the access-token issuance path.
- Concepts → mTLS — how
feature.MTLSextracts the client cert and producescnf.x5t#S256. - Use case: FAPI 2.0 Baseline — embedder walkthrough for a Baseline-target deployment.
A production embedder will replace the cmd/op-demo ephemera (in-memory store, ephemeral signing key, the auto-approve CIBA wrapper) with vault-backed keys, a durable storage adapter, and a real device-callback path — but the op.With... shape stays identical.
Caveats worth naming
- Plan suite version. The default pin is
release-v5.3.4. The retained snapshot is labelledv1.5.0and records neither the image tag nor the digest. - Headless drive. The drive script reverse-engineers the OFCS REST surface; OFCS does not document it.
- No real RP cert. The mTLS plan slots use generated self-signed certs at
conformance/certs/so the plan can be instantiated. No real CA chain is exercised. - Single OP instance. Cross-instance behaviour (e.g. token introspection across two OPs sharing a store) is exercised by the in-process Spec Scenario Suite under
test/scenarios/, not OFCS.