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 baseline below is the run captured on 2026-11-04 against the library at commit 9e8d5153, executed 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-final-test-plan | + PAR, sender-constrained access tokens (DPoP), strict alg list, redirect_uri exact match | FAPI 2.0 Security Profile Final (Baseline) |
fapi2-message-signing-final-test-plan | + JAR (signed authorization request), JARM (signed authorization response) | FAPI 2.0 Message Signing Final |
fapi-ciba-id1-test-plan | Client-Initiated Backchannel Authentication (poll mode), mTLS-bound tokens | FAPI-CIBA |
The fapi2-security-profile-final and fapi2-message-signing-final plans are the FAPI 2.0 Security Profile Final and Message Signing Final plans of the suite. Under profile.FAPI2Baseline and profile.FAPI2MessageSigning the OP accepts only its issuer, as a single-string aud, on client assertions, and it does not rotate refresh tokens (Final §5.3.2.1). The results are a regression run against the suite, not a certification.
Latest baseline
- PASSED20177.6%
- REVIEW3814.7%
- SKIPPED145.4%
- No verdict20.8%
- FAILED41.5%
- WARNING00.0%
Every FAILED and no-verdict module is a reviewed release exclusion — five causes, each recorded with a rationale and an expiry.
Snapshot status
The bars are the raw outcome of all nine plans, run against OFCS release-v5.3.4. The JSON artifact is a local gitignored file under conformance/baselines/. make conformance-release-verify accepted the run with zero blockers: every raw failure has a reviewed, expiry-bound entry in conformance/release-exclusions.json, every no-verdict module has an entry with evidence, and every REVIEW or SKIPPED module falls under a reviewed rule. That gate result is not an OIDC certification.
Why four modules FAILED
Every failing and no-verdict module traces back to one of five causes. Two are standing design decisions of the OP, one is a registration behaviour the suite rejects, and two are limits of driving the suite headlessly. Each one is listed with an owner and an expiry date in conformance/release-exclusions.json.
The dynamic certification profile also requires the id_token and token id_token response types and the implicit grant. The OP serves the authorization code flow only, as the OAuth 2.0 Security BCP recommends, so the dynamic plan's discovery check fails on the response-types and grant-types conditions. The signing-algorithm checks of the same module pass when the plan runs against an RS256 OP.
oidcc-userinfo-rs256 registers a client with userinfo_signed_response_alg=RS256 and no UserInfo encryption. Dynamic registration answers invalid_client_metadata, because the OP returns the signed UserInfo shape to a request carrying Accept: application/jwt rather than to a registration value, so the module never reaches its assertions. The algorithm is not the cause: the same registration is refused with ES256, and an OP built with op.WithSigningAlgorithm(op.AlgRS256) issues RS256 UserInfo.
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 not currently enabled; 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
oidcc-request-uri-signed-rs256 publishes its request object at a host served with the suite's self-signed certificate. The OP installs the conformance CA on the JWKS fetcher's transport but not on the request_uri fetcher, so /authorize answers invalid_request_uri before redirecting and the suite waits for a callback that never comes. The signing algorithm is not the cause: a run against an RS256 OP fails at the same fetch.
Per-plan breakdown
Each bar is one plan on its own scale: full width is that plan's own module count, spelled out as the fraction at the right of the row. The segments are therefore shares of that plan, never of the run — so a plan's size is read from its numbers, not from its bar length. 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 | 30 | 3 | 2 | 0 | 0 | 0 | 35 |
oidcc-config-certification-test-plan | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
oidcc-dynamic-certification-test-plan | 8 | 6 | 5 | 0 | 2 | 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-final-test-plan | 42 | 9 | 1 | 0 | 0 | 0 | 52 |
fapi2-message-signing-final-test-plan | 55 | 9 | 2 | 0 | 0 | 0 | 66 |
fapi-ciba-id1-test-plan | 32 | 0 | 2 | 0 | 0 | 0 | 34 |
| Total | 201 | 38 | 14 | 0 | 4 | 2 | 259 |
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
The Basic plans drive the code flow without PKCE, so the conformance OP declares op.WithProfile(profile.OIDCCore); the OAuth 2.1 default would refuse those requests.
| What it tests | Library option to enable | Doc page |
|---|---|---|
| Authorization Code flow (PKCE optional for confidential clients) | op.WithProfile(profile.OIDCCore); PKCE itself is always supported | /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-final-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 | the default; OP-issued ID Tokens are never signed with PS256, and op.New refuses op.WithSigningAlgorithm(op.AlgRS256) together with the FAPI profiles | /concepts/jose-basics |
Refusal of RS256 (FAPI), HS*, none | RS256 with a FAPI profile is a build-time error; HS* and none are absent from the closed alg type at internal/jose/alg.go | /security/design-judgments |
private_key_jwt | profile-enforced; the client assertion's aud must be the issuer, as a single string | /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 tokens are not rotated | profile-enforced under profile.FAPI2Baseline and profile.FAPI2MessageSigning (FAPI 2.0 Security Profile Final §5.3.2.1); every other profile rotates and detects reuse by default | /concepts/refresh-tokens |
fapi2-message-signing-final-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. The historical FAPI-CIBA-ID1 plan uses mTLS certificate-bound access tokens; the demo explicitly enables feature.MTLS to match its tls_client_certificate_bound_access_tokens metadata. The library's general profile.FAPICIBA preset also permits DPoP and defaults to it when neither binding is chosen, so that preset alone does not reproduce the ID1 plan.
| What it tests | Library option to enable | Doc page |
|---|---|---|
/bc-authorize endpoint + auth_req_id | op.WithCIBAGrant(op.CIBAGrant{HintResolver: ...}) | /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; CIBAGrant.PollInterval overrides advertised interval | /use-cases/ciba |
| Poll-abuse lockout cap | default 5 strikes; CIBAGrant.MaxPollViolations raises or lowers the cap | /use-cases/ciba |
| Sender-constrained access tokens | op.WithProfile(profile.FAPICIBA) permits DPoP or mTLS, defaulting to DPoP; this OFCS demo explicitly enables feature.MTLS for the historical ID1 plan | /concepts/sender-constraint, /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 person has to confirm something the harness cannot see, such as the error page the OP rendered or whether a login screen appeared again. The suite uploads a screenshot placeholder and waits in
WAITINGuntil someone marks it reviewed in the OFCS UI; the headless runner recordsREVIEWas-is. Not a failure. - SKIPPED — the test depends on something this OP does not advertise in discovery or per-client metadata, so the suite does not run it. For example, the
RS256negative tests skip because the client JWKS the suite registers for the FAPI plans holds only anES256key, so there is no RSA key to sign the negative case with. Not a failure. - WARNING — the test reached a terminal PASS but logged an advisory. The current snapshot has 0 warnings.
- FAILED — a module did not reach the result the suite expects. In this run, four failures are reviewed release exclusions: two from the dynamic plan (the authorization-code-only response types, and the refused
userinfo_signed_response_algregistration) and two from the deliberately subject-scoped back-channel logout metadata. The release verifier fails unless each one is listed and unexpired. - No verdict — the harness could not drive the module to a terminal result. One needs a signing-key rotation in the middle of the module; the other publishes a request object at a host whose certificate the OP's
request_urifetcher does not trust, so the OP refuses before redirecting and the suite waits for a callback that never comes. Both are reviewed release exclusions with evidence from a standalone run.
Why REVIEW modules are not auto-passed
The suite gates these modules on human judgment by design. A headless cmd/op-demo cannot honestly upload a screenshot of "this is what my user saw", and turning the gate off would misstate what was checked. The harness keeps REVIEW as it is; certification through the OpenID Foundation would have a person clear each one in the UI.
Modules currently FAILED — and why
Four modules across two 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-dynamic | The dynamic profile also requires the id_token and token id_token response types and the implicit grant, which the OP does not implement; the failing conditions are the response-types and grant-types checks. |
oidcc-userinfo-rs256 | oidcc-dynamic | Registration requests userinfo_signed_response_alg=RS256 with no UserInfo encryption; dynamic client registration answers invalid_client_metadata, so the module never reaches its assertions. The algorithm is not the cause. |
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 publishes an RS256-signed request object and waits for a successful authorization response. The OP cannot fetch the request_uri: the suite serves a self-signed certificate and the OP installs the conformance CA only on the JWKS fetcher, so /authorize answers invalid_request_uri before redirecting and the suite is left WAITING for a callback that never comes. The signing algorithm is not the cause. |
Modules currently in REVIEW
Thirty-eight modules, all of them cases where the suite has no callback to assert on or records a review item regardless of the outcome, so it asks a person to look at the rendered screen. Where the OP refuses or re-prompts, the refusal itself is pinned by the library's own tests.
| Plan | Modules | Why a person has to look |
|---|---|---|
oidcc-basic, oidcc-formpost-basic (3 each) | oidcc-ensure-registered-redirect-uri | An unregistered redirect_uri must not be redirected to, so the outcome is an error page. |
oidcc-max-age-1, oidcc-prompt-login | Only a person can confirm the login screen appeared again instead of the session being reused. | |
oidcc-dynamic (6) | oidcc-ensure-redirect-uri-in-authorization-request, oidcc-redirect-uri-query-added, oidcc-redirect-uri-query-mismatch | Same as the first row: refused without redirecting. |
oidcc-registration-logo-uri, oidcc-registration-policy-uri, oidcc-registration-tos-uri | The suite asks a person to confirm the consent screen presents the registered URIs. Whether they render depends on the interaction driver the embedder supplies. | |
oidcc-rp-initiated-logout (8) | oidcc-rp-initiated-logout-* | Each ends on a screen the OP renders itself — the logout confirmation, or the error page for a bad id_token_hint or unregistered post_logout_redirect_uri — because a refused logout must not redirect back to the RP. |
fapi2-security-profile-final, fapi2-message-signing-final (9 each) | the nine modules below | PAR request_uri refusals, request-object mismatches, the unsigned request without PAR and a state supplied only outside the request object are refused before redirecting, leaving an error page to confirm. The two long state / nonce modules are the exception: the OP accepts the long value and the flow succeeds, but the module records a review item either way. |
The nine FAPI 2.0 modules (the same names appear in both plans, 18 in total):
fapi2-…-ensure-different-nonce-inside-and-outside-request-objectfapi2-…-ensure-different-state-inside-and-outside-request-objectfapi2-…-ensure-authorization-request-with-long-noncefapi2-…-ensure-authorization-request-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
Modules currently SKIPPED — and why
Fourteen modules. The suite decides to skip from what discovery and per-client metadata advertise, so a skip here records a deliberate omission, not a test that failed to run.
| Module | Plan | Reason |
|---|---|---|
fapi2-security-profile-final-ensure-signed-client-assertion-with-RS256-fails | both fapi2-* plans | These modules assert that an RS256-signed client assertion or request object is rejected. The client JWKS the suite registers for the FAPI plans holds only an ES256 key, so there is no RSA key to sign the negative case with; the OP refuses RS256 issuing under the FAPI profiles as well. |
fapi2-security-profile-final-ensure-signed-request-object-with-RS256-fails | fapi2-message-signing-final | Same, for the request object. (The Message Signing plan reuses the security-profile module names.) |
fapi-ciba-id1-ensure-request-object-signature-algorithm-is-RS256-fails | fapi-ciba-id1 | Same, for the CIBA client's request object. |
fapi-ciba-id1-ensure-client-assertion-signature-algorithm-in-token-endpoint-request-is-RS256-fails | fapi-ciba-id1 | Same, for the client assertion at the token endpoint. |
oidcc-ensure-request-object-with-redirect-uri, oidcc-unsigned-request-object-supported-correctly-or-rejected-as-unsupported | oidcc-basic, oidcc-formpost-basic (both), oidcc-dynamic (first only) | Request-object modules of the non-FAPI plans. The suite skips them based on the request-object capabilities discovery advertises, and the OP never accepts alg=none. Signed request objects run in the FAPI plans, where JAR is mandatory. |
oidcc-request-uri-unsigned | oidcc-dynamic | Same family, for an unsigned object behind request_uri. |
oidcc-idtoken-unsigned | oidcc-dynamic | Asks for an unsigned ID Token. The OP signs every ID Token and does not advertise none. |
oidcc-registration-sector-bad, oidcc-registration-sector-uri | oidcc-dynamic | sector_identifier_uri modules. Pairwise subjects are opt-in (op.WithPairwiseSubject) and the demo runs the dynamic plan with public subjects; pairwise is covered by the library's own tests and by examples/34-pairwise-saas. |
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 heremake conformance-up generates certificates, starts the OFCS containers at https://localhost:8443, builds and starts cmd/op-demo at https://127.0.0.1:9443, and seeds the nine plans through the OFCS REST API. make conformance-baseline then drives every module and records 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 before merging security-relevant changes.
Which op.With... options each plan needs, the plan-to-flag table, and the release gate are in Reproducing the OFCS run. The baseline-diff cycle is under Capturing and diffing a baseline.
What FAPI 2.0 means in this codebase
op.WithProfile(profile.FAPI2Baseline) activates the configuration the two fapi2-* plans are built around. The full explanation is in FAPI 2.0; the pieces the plans assert are:
feature.PARandfeature.JAR, both auto-enabled byFAPI2Baseline:/oidc/parbecomes routable,request/request_uriare validated as signed JWTs.feature.JARM, additionally auto-enabled byFAPI2MessageSigning: authorization responses are signed JWTs.- Sender-constrained access tokens. The profile requires DPoP or mTLS. If the embedder enables
feature.MTLS(cnf.x5t#S256), that satisfies it and the DPoP default is not added. Otherwiseop.Newselectsfeature.DPoP(cnf.jkt), so a plainop.WithProfile(profile.FAPI2Baseline)still boots with sender-constrained tokens. With DPoP active, discovery advertisesdpop_signing_alg_values_supported: ES256, EdDSA, PS256. - Client assertions accept only the issuer as a single-string
aud, and refresh tokens are not rotated (profile.FAPI2Baselineandprofile.FAPI2MessageSigning). - A JOSE algorithm allow-list of
RS256 / PS256 / ES256 / EdDSAacross the codebase;HS*andnoneare structurally unreachable (seeinternal/jose/alg.go). token_endpoint_auth_methods_supportednarrowed to the FAPI production path (private_key_jwt), andredirect_uriexact-string match.- Per-client
RequestObjectSigningAlg/TokenEndpointAuthSigningAlgpin each FAPI client toPS256(orES256/EdDSA); discovery still advertises the codebase-wide list.
If you set options that conflict with the profile after WithProfile, op.New(...) returns a build-time error instead of letting a partial-FAPI configuration reach 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 | Current OFCS image pin (release-v5.3.4) + JKS truststore wiring |
conformance/release-exclusions.json | Reviewed, expiry-bound exceptions the release gate accepts |
scripts/conformance.sh | certs / ofcs-up / op-up / seed-plans / drive / batch / baseline / baseline-diff / release-verify |
tools/conformance/ | REST client, headless drive script, baseline and release-verify logic (Python) |
conformance/baselines/*.json | Captured snapshots (gitignored — environment-specific) |
Caveats worth naming
- Plan suite version. The run behind this page used OFCS
release-v5.3.4, which is also the current default pin indocker-compose.yml. - 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
test/scenarios, not OFCS.
The conformance harness sits next to an in-process Spec Scenario Suite under test/scenarios/. OFCS runs end-to-end against a live OP over HTTP; the scenario suite drives the same protocol invariants in-process. Both must pass before security-relevant changes merge.