OFCS conformance status
go-oidc-provider is regressed against the OpenID Foundation Conformance Suite (OFCS). The harness lives in conformance/ in the source repo and scaffolds nine OFCS plans; the published baseline below is the v1.0.0 release snapshot run end-to-end against a cmd/op-demo instance.
Personal project, not certified
This is a personal project maintained by an individual developer. No OpenID Foundation membership fee is paid and no formal OIDC certification is held. The numbers on this page are reproducible snapshots from the plan set shown below. They are not a substitute for a paid OpenID Foundation certification and should not be cited as one.
What this snapshot exercises
| Plan | What it covers | Profile |
|---|---|---|
oidcc-basic-certification-test-plan | Authorization Code + PKCE, ID Token, UserInfo, refresh, discovery | OIDC Core 1.0 |
oidcc-config-certification-test-plan | The discovery document and the shape of the published JWKS | OIDC Core 1.0 — Discovery |
oidcc-dynamic-certification-test-plan | Dynamic client registration, then the code flow against the client it registered | OIDC Core 1.0 — Dynamic Registration |
oidcc-formpost-basic-certification-test-plan | The basic plan again with response_mode=form_post | OIDC Core 1.0 — form_post |
oidcc-rp-initiated-logout-certification-test-plan | /end_session, id_token_hint, post_logout_redirect_uri | RP-Initiated Logout 1.0 |
oidcc-backchannel-rp-initiated-logout-certification-test-plan | RP-initiated logout plus logout_token delivery to the registered back-channel URI | Back-Channel Logout 1.0 |
fapi2-security-profile-id2-test-plan | + PAR, sender-constrained access tokens (DPoP), strict alg list, redirect_uri exact match | FAPI 2.0 Baseline |
fapi2-message-signing-id1-test-plan | + JAR (signed authorization request), JARM (signed authorization response) | FAPI 2.0 Message Signing |
fapi-ciba-id1-test-plan | Client-Initiated Backchannel Authentication (poll mode), mTLS-bound tokens | FAPI-CIBA |
Latest baseline
- PASSED20977.1%
- REVIEW3914.4%
- SKIPPED155.5%
- No verdict20.7%
- FAILED62.2%
- WARNING00.0%
Every FAILED and no-verdict module is a reviewed release exclusion — three causes, none of them an unresolved defect.
Snapshot status
The bars are the raw outcome of all nine plans. The matching timestamped JSON is under conformance/baselines/. make conformance-release-verify accepted this release snapshot with zero blockers: every raw failure and no-verdict module has a reviewed, expiry-bound entry in conformance/release-exclusions.json. That gate result is not an OIDC certification.
Why six modules FAILED
Every failing and no-verdict module traces back to one of three causes. Two are standing design decisions that will not change in the 1.x line; the third is a limit of driving the suite headlessly. None of them is an unresolved defect, and each one is listed with an owner and an expiry date in conformance/release-exclusions.json.
Discovery advertises ES256 and nothing else, so the modules that assert the OIDC Core 1.0 §15.1 RS256 requirement fail, and a dynamic registration asking for an RS256-signed ID Token or UserInfo response is refused before it reaches its assertions. Supporting one algorithm removes algorithm negotiation — and the downgrade guard negotiation would need — and FAPI 2.0 forbids RS256 outright. Why the alg list is closed
The OP advertises backchannel_logout_session_supported: false and logs out by subject rather than by sid. That metadata is optional in the spec, but the certification module requires true unconditionally, and the rest of the plan aborts on the same assertion. Session-scoped sid logout is an opt-in profile planned for a later minor; subject-only logout is the fail-secure default. Back-channel logout
oidcc-server-rotate-keys asks a human to rotate the signing keys mid-module and press Start. The library takes its keyset at op.New, so a deployment rotates by constructing a new provider — which would drop the in-memory store the plan's registered client lives in. What the OP serves is unaffected: JWKS publishes every configured key. Key rotation
Per-plan breakdown
Bar length is the plan's module count on one shared scale, so a one-module plan cannot read as heavily as a seventy-one-module one. Outcomes with a count of zero are left out of the row.
The same snapshot as a table
| Plan | PASSED | REVIEW | SKIPPED | WARNING | FAILED | No verdict | Total |
|---|---|---|---|---|---|---|---|
oidcc-basic-certification-test-plan | 29 | 4 | 2 | 0 | 0 | 0 | 35 |
oidcc-config-certification-test-plan | 0 | 0 | 0 | 0 | 1 | 0 | 1 |
oidcc-dynamic-certification-test-plan | 7 | 6 | 5 | 0 | 3 | 2 | 23 |
oidcc-formpost-basic-certification-test-plan | 30 | 3 | 2 | 0 | 0 | 0 | 35 |
oidcc-rp-initiated-logout-certification-test-plan | 3 | 8 | 0 | 0 | 0 | 0 | 11 |
oidcc-backchannel-rp-initiated-logout-certification-test-plan | 0 | 0 | 0 | 0 | 2 | 0 | 2 |
fapi2-security-profile-id2-test-plan | 48 | 9 | 1 | 0 | 0 | 0 | 58 |
fapi2-message-signing-id1-test-plan | 60 | 9 | 2 | 0 | 0 | 0 | 71 |
fapi-ciba-id1-test-plan | 32 | 0 | 3 | 0 | 0 | 0 | 35 |
| Total | 209 | 39 | 15 | 0 | 6 | 2 | 271 |
What each test plan covers
Each OFCS test plan exercises a specific spec profile. The tables below map every plan to the library options that turn on the relevant code paths and to the doc pages where that surface is documented, so embedders can verify their own deployments expose the same configuration the suite asserts against.
oidcc-basic-certification-test-plan — OIDC Core 1.0
| What it tests | Library option to enable | Doc page |
|---|---|---|
| Authorization Code flow + PKCE | enabled by default | /concepts/authorization-code-pkce |
| ID Token issuance + claims | enabled by default | /concepts/tokens |
| UserInfo endpoint | enabled by default | /concepts/tokens |
Discovery (/.well-known/openid-configuration) | enabled by default | /concepts/discovery |
| JWKS publication | enabled by default | /operations/jwks |
| Refresh tokens + rotation | enabled by default; long-lived refresh requires the offline_access scope | /concepts/refresh-tokens |
Standard scopes (profile, email, address, phone) | op.WithScope(...) once per scope | /concepts/scopes-and-claims |
| Public / pairwise subjects | op.WithPairwiseSubject(salt) for pairwise; per-client SubjectType selects which one applies | /use-cases/pairwise-subject |
fapi2-security-profile-id2-test-plan — FAPI 2.0 Baseline
| What it tests | Library option to enable | Doc page |
|---|---|---|
| PAR (RFC 9126) | op.WithProfile(profile.FAPI2Baseline) implies feature.PAR | /concepts/fapi, /use-cases/fapi2-baseline |
| JAR (RFC 9101) | profile implies feature.JAR | /concepts/fapi |
S256 PKCE enforcement | profile-enforced | /concepts/authorization-code-pkce |
iss in authorization response (RFC 9207) | profile-enforced | /concepts/issuer |
ES256 for ID Token signing | profile-enforced; OP-issued ID Tokens are never signed with PS256 or RS256 | /concepts/jose-basics |
Refusal of RS256 (FAPI), HS*, none | closed alg type at internal/jose/alg.go | /security/design-judgments |
private_key_jwt | profile-enforced | /concepts/client-types |
| DPoP or mTLS sender constraint | op.WithFeature(feature.DPoP) or op.WithFeature(feature.MTLS) (at least one is mandatory under FAPI 2.0) | /concepts/sender-constraint, /concepts/dpop, /concepts/mtls |
redirect_uri exact match | profile-enforced | /concepts/redirect-uri |
| Refresh token rotation + reuse detection | enabled by default | /concepts/refresh-tokens |
fapi2-message-signing-id1-test-plan — FAPI 2.0 Message Signing
Message Signing layers signed authorization responses on top of Baseline. Everything the Baseline plan asserts also runs here — switch the profile constant and JARM activates automatically.
| What it tests | Library option to enable | Doc page |
|---|---|---|
| Everything from FAPI 2.0 Baseline (above) | op.WithProfile(profile.FAPI2MessageSigning) | (as above) |
| Signed authorization response (JARM) | profile implies feature.JARM | /concepts/fapi (JARM section) |
| Signed ID Token in token response | profile-enforced | /concepts/tokens |
Request object signing (PS256 / ES256) | profile-enforced | /concepts/fapi |
fapi-ciba-id1-test-plan — FAPI-CIBA (Client-Initiated Backchannel Authentication)
The CIBA plan exercises the OpenID Connect Client-Initiated Backchannel Authentication grant: an authentication request initiated by the client, completed asynchronously on the user's authentication device (push notification, IVR, etc.), and consumed back via a polling token request. The OP runs in poll mode, FAPI-CIBA inherits FAPI 1.0's hardcoded tls_client_certificate_bound_access_tokens requirement so mTLS sender constraint is mandatory.
| What it tests | Library option to enable | Doc page |
|---|---|---|
/bc-authorize endpoint + auth_req_id | op.WithCIBA(op.WithCIBAHintResolver(...)) | /use-cases/ciba |
Hint resolution (login_hint / id_token_hint / login_hint_token) | embedder-supplied HintResolver | /use-cases/ciba |
Polling discipline (authorization_pending / slow_down) | enabled by default; op.WithCIBAPollInterval(...) overrides advertised interval | /use-cases/ciba |
| Poll-abuse lockout cap | default 5 strikes; op.WithCIBAMaxPollViolations(n) raises or lowers the cap | /use-cases/ciba |
tls_client_certificate_bound_access_tokens (FAPI-CIBA mandate) | op.WithProfile(profile.FAPICIBA) implies feature.MTLS | /concepts/mtls |
Signed request object on /bc-authorize | op.WithFeature(feature.JAR) (auto under FAPI-CIBA) | /concepts/fapi |
Bound request_object iat and exp claims (FAPI-CIBA §5.2.2) | profile-enforced | /concepts/fapi |
How REVIEW, SKIPPED, WARNING, and FAILED categorize
- REVIEW — the test ran, but a human reviewer must verify visual or out-of-band behaviour the harness cannot capture honestly (consent UI strings, error page screenshots, certificate chain confirmation). Not a failure.
- SKIPPED — the test depends on a feature this OP does not advertise in discovery or per-client metadata. For example, the
RS256negative tests skip because the FAPI client metadata declaresPS256as its signing alg, puttingRS256out of scope for that probe. Not a failure. - WARNING — OFCS records this as a non-failed result value: the test reached a terminal PASS on its main assertions but logged an advisory the operator may want to address. The current snapshot has 0 warnings.
- FAILED — a module did not reach the suite's expected result. In the v1.0.0 snapshot, six failures are reviewed release exclusions: four arise from the permanent ES256-only signing policy and two from the deliberately subject-scoped back-channel logout metadata. The release verifier fails unless each exception is explicitly listed and unexpired.
- No verdict — the harness could not produce a terminal result. The two current modules require an in-process signing-key rotation or wait for an RS256 request object that the ES256-only OP correctly refuses. Both are reviewed release exclusions.
How to reproduce the conformance run yourself
- Stand up an OP with the relevant profile wired in —
op.WithProfile(profile.FAPI2Baseline)for the security profile,op.WithProfile(profile.FAPI2MessageSigning)for message signing,op.WithProfile(profile.FAPICIBA)plus the CIBA options for FAPI-CIBA, or noWithProfilefor the OIDC Core plan. - Register the plan against an OFCS deployment. The conformance suite is operated by the OpenID Foundation; the source repo's
conformance/directory contains plan templates and a pinned Docker image that brings up a local copy. - Drive the plan. The harness pokes
/authorize,/par,/token,/userinfo,/jwks, and the rest of the discovered endpoints through every required code path, then writes a JSON snapshot you can diff against the recorded baseline.
The detailed runbook (make targets, the JSON snapshot layout, the diff gate) is in Reproducing the baseline yourself below.
REVIEW vs FAILED — the distinction
OFCS mainly reports PASSED, FAILED, REVIEW, and SKIPPED; the harness also preserves WARNING when OFCS emits an advisory result. REVIEW does not mean a test failed. It means the test wants a human operator to confirm something the automation cannot — for example, "did the OP show a login screen here?" The test runs, takes screenshots, then sits in a WAITING state until someone in the OFCS UI clicks "reviewed". Our headless runner records REVIEW when the test reached that state without erroring.
Why we don't auto-pass REVIEW modules
The conformance suite gates these modules on human judgment by design. A cmd/op-demo running headless can't honestly upload a screenshot of "this is what my user saw"; turning the gate off would lie about what was actually checked. The harness records REVIEW as-is, on the understanding that paid certification would require sitting in front of the UI to clear them.
Modules currently FAILED — and why
Six modules across three plans. Each row is a reviewed, expiry-bound entry in conformance/release-exclusions.json; make conformance-release-verify fails the release if any of them is missing, unowned, or past its expiry.
| Module | Plan | Why it fails |
|---|---|---|
oidcc-discovery-endpoint-verification | oidcc-config | id_token_signing_alg_values_supported lists ES256 only; the module asserts the OIDC Core 1.0 §15.1 RS256 requirement. |
oidcc-discovery-endpoint-verification | oidcc-dynamic | Same assertion, same cause, on the dynamic plan. |
oidcc-idtoken-rs256 | oidcc-dynamic | Registration requests id_token_signed_response_alg=RS256; dynamic client registration answers invalid_client_metadata, so the module never reaches its assertions. |
oidcc-userinfo-rs256 | oidcc-dynamic | Same, for userinfo_signed_response_alg=RS256. |
oidcc-backchannel-logout-discovery-endpoint-verification | oidcc-backchannel-rp-initiated-logout | The OP advertises backchannel_logout_session_supported: false, which the spec permits and this module refuses. |
oidcc-backchannel-rp-initiated-logout | oidcc-backchannel-rp-initiated-logout | Cascades from the module above — the plan aborts on that assertion before this one can run. |
Modules with no verdict — and why
Two modules the harness could not drive to a terminal result. Both are recorded with the evidence from a standalone run against a live suite.
| Module | Plan | Why there is no verdict |
|---|---|---|
oidcc-server-rotate-keys | oidcc-dynamic | The module waits for an operator to rotate the signing keys and press Start. The keyset is fixed at op.New, and rebuilding the provider mid-plan would drop the in-memory store holding the plan's registered client. The module stays CONFIGURED until the runner's idle bound. |
oidcc-request-uri-signed-rs256 | oidcc-dynamic | The module pushes an RS256-signed request object and waits for a successful authorization response. The ES256-only OP refuses before redirecting — the correct behaviour — so no callback ever arrives and the suite records no result either way. |
Modules currently in REVIEW
oidcc-basic plan (4)
| Module | What it gates |
|---|---|
oidcc-ensure-registered-redirect-uri | Manual confirmation that the OP refused an unregistered redirect_uri |
oidcc-max-age-1 | Manual confirmation that max_age=1 re-prompted the user |
oidcc-prompt-login | Manual confirmation that prompt=login re-prompted |
oidcc-response-type-missing | Manual confirmation of the first-party error page for a request without response_type |
FAPI 2.0 plans (9 each, same set)
These all gate on a screenshot upload of the OP's error page or a manual "is the user actually re-prompted" judgment. They run cleanly headless but stay REVIEW until human sign-off (the same nine names appear on both fapi2-security-profile-id2 and fapi2-message-signing-id1, totalling 18 across the two plans):
fapi2-…-ensure-different-nonce-inside-and-outside-request-objectfapi2-…-ensure-different-state-inside-and-outside-request-objectfapi2-…-ensure-request-object-with-long-noncefapi2-…-ensure-request-object-with-long-statefapi2-…-ensure-unsigned-authorization-request-without-using-par-failsfapi2-…-par-attempt-reuse-request_urifapi2-…-par-attempt-to-use-expired-request_urifapi2-…-par-attempt-to-use-request_uri-for-different-clientfapi2-…-state-only-outside-request-object-not-used
The OP returns the right HTTP error in every case (the negative tests pass their internal assertions); OFCS just wants a human to inspect the rendered error UI.
Modules currently in WARNING
None in the current snapshot. The previous fapi-ciba-id1-refresh-token advisory is now a plain PASSED module.
Modules currently SKIPPED — and why
| Module | Reason |
|---|---|
fapi2-…-ensure-signed-client-assertion-with-RS256-fails (×2) | The FAPI client used in the plan registers token_endpoint_auth_signing_alg=PS256, so OFCS skips the per-client RS256 negative test on both fapi2 plans. |
fapi2-message-signing-…-ensure-signed-request-object-with-RS256-fails | Same — the FAPI client's request_object_signing_alg=PS256 makes the RS256 negative test inapplicable. |
fapi-ciba-id1-ensure-request-object-signature-algorithm-is-RS256-fails | The FAPI-CIBA client registers request_object_signing_alg=PS256. |
fapi-ciba-id1-ensure-client-assertion-signature-algorithm-in-backchannel-authorization-request-is-RS256-fails | Same — token_endpoint_auth_signing_alg=PS256 on the CIBA client. |
fapi-ciba-id1-ensure-client-assertion-signature-algorithm-in-token-endpoint-request-is-RS256-fails | Same. |
oidcc-ensure-request-object-with-redirect-uri | The oidcc-basic plan does not enable JAR; the OP omits request_object_signing_alg_values_supported from discovery and OFCS skips. |
oidcc-unsigned-request-object-supported-correctly-or-rejected-as-unsupported | Same — JAR off, no request parameter, OFCS skips. |
"SKIPPED" is intentional, not "didn't run"
OFCS's skip decision is a function of what discovery and per-client metadata advertise. The FAPI clients in the plan declare PS256 as their token-endpoint-auth and request-object signing alg, so OFCS's "RS256 should fail" probes are not applicable and the suite marks them skipped rather than running them and recording a pass.
Reproducing the baseline yourself
git clone https://github.com/libraz/go-oidc-provider.git
cd go-oidc-provider
make conformance-up
make conformance-baseline LABEL=local-check
ls conformance/baselines/ # JSON snapshot lands hereThe harness:
- Generates self-signed RSA-2048 certs (
scripts/conformance.sh certs). - Brings up the OFCS Docker stack at
https://localhost:8443. - Builds and runs
cmd/op-demoathttps://127.0.0.1:9443. - Seeds the OFCS plans via the REST API. The harness scaffolds nine plans; the latest status table above records all nine.
- Records pass/fail per module to a deterministic JSON file.
make conformance-baseline-diff exits non-zero on any module that lost PASSED between two snapshots — that is the regression gate the project uses pre-merge for security-relevant changes.
What FAPI 2.0 means in this codebase
op.WithProfile(profile.FAPI2Baseline) activates the configuration the two fapi2-* plans are built around:
feature.PAR(auto-enabled byFAPI2Baseline) —/parbecomes routable;request_uriaccepted at/authorizefeature.JAR(auto-enabled byFAPI2Baseline) —request/request_urivalidated as signed JWTsfeature.JARM(additionally auto-enabled byFAPI2MessageSigning) — authorization responses signed as JWTs- Sender-constrained access tokens — the profile imposes a DPoP-or-mTLS requirement. If the embedder explicitly enables
feature.MTLS(cnf.x5t#S256), that satisfies the requirement and suppresses the DPoP default. Otherwiseop.Newselectsfeature.DPoP(cnf.jkt) as the canonical default, so a plainop.WithProfile(profile.FAPI2Baseline)still boots with sender-constrained access tokens. Discovery advertisesdpop_signing_alg_values_supported: ES256, EdDSA, PS256when DPoP is active. - JOSE alg allow-list locked to
RS256 / PS256 / ES256 / EdDSAcodebase-wide;HS*andnoneare structurally unreachable (seeinternal/jose/alg.go) token_endpoint_auth_methods_supportedintersected with the FAPI production path (private_key_jwt)redirect_uriexact-string match enforced- per-client
RequestObjectSigningAlg/TokenEndpointAuthSigningAlgnarrowing pins each FAPI client toPS256(orES256/EdDSA); the discovery doc still advertises the codebase-wide list
If you set conflicting options after WithProfile, op.New(...) returns a build-time error rather than letting a partial-FAPI configuration escape into production.
Where the harness lives
| Path | What it is |
|---|---|
conformance/README.md | Operator runbook |
conformance/plans/*.json | Plan templates (server / client / resource blocks) |
conformance/docker-compose.yml | OFCS image pin (release-v5.2.1) + JKS truststore wiring |
scripts/conformance.sh | certs / ofcs-up / op-up / seed-plans / drive / batch |
tools/conformance/ofcs.py | REST client + headless drive script |
conformance/baselines/*.json | Captured snapshots (gitignored — environment-specific) |
Caveats worth naming
- Plan suite version. OFCS is pinned to
release-v5.2.1. Tests added or renamed in newer OFCS releases are not covered until the pin is bumped. - Headless drive. The drive script reverse-engineers the OFCS REST surface; OFCS does not document it. Behaviour is confirmed against v5.2.1 only.
- 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
test/scenarios, not OFCS.
The conformance harness sits next to an in-process Spec Scenario Suite under test/scenarios/. The two suites cover different layers — OFCS runs end-to-end against a live OP via HTTP, the scenario suite drives the same protocol invariants in-process — and both must be passing before security-relevant changes merge.