Use case — Declaring a security profile
op.WithProfile makes the OP's intended security posture explicit. Leaving it unset is also a choice: an OP built without it runs profile.OAuth21, so every authorization-code request must carry PKCE, confidential clients included. A deployment that must admit relying parties unable to send PKCE declares profile.OIDCCore, which keeps the OpenID Connect Core 1.0 shape.
provider, err := op.New(
// issuer, store, keyset, cookie keys, clients, and login flow …
op.WithProfile(profile.OIDCCore), // opt out of the PKCE-on-every-request default
)profile.OAuth21 changes that one rule only. It does not require PAR, cap token lifetimes, restrict client-authentication methods, or require sender-constrained tokens. Those controls belong to the applicable FAPI profiles; use FAPI 2.0 Baseline when that is the target profile. Startup logs and audit events report it as oauth21, distinct from the fapi2-baseline string of the FAPI 2.0 profile; the Go identifier is profile.OAuth21.
profile.OIDCCore adds no requirement of its own: PKCE stays optional for confidential clients. It is for legacy relying parties and the OpenID Connect Basic certification plan. It cannot be combined with any other profile, and op.New fails if you try.
At successful construction the OP emits startup.profile to its audit logger. The event records the declared profiles, features, and grants, and the resolved policy: signing_alg, pkce_required, par_required, sender_constrained, client_auth_methods, the token lifetimes and more. Use it to confirm the deployed posture before the first request. Each profile appears under its identifier: oauth21, oidc-core, fapi2-baseline, fapi2-message-signing or fapi-ciba; match on these strings in dashboards and alerts.
Source:
examples/00-security-profileruns aprofile.OIDCCoreOP and aprofile.OAuth21OP side by side, then sends both the same confidential-client request without acode_challenge.
Decide deliberately
| Deployment intent | Configuration |
|---|---|
| OAuth 2.1 posture (the default) | no profile declared, or op.WithProfile(profile.OAuth21) to say so |
| Legacy OIDC clients that cannot send PKCE must remain compatible | op.WithProfile(profile.OIDCCore); plan a PKCE migration |
| Financial-grade API profile | op.WithProfile(profile.FAPI2Baseline) or the applicable FAPI profile |
Public and native clients require PKCE under every profile; the visible difference is the confidential-client path.
What each profile requires
| Profile | PKCE | Features switched on | Sender-constrained tokens | Grant required | Other limits |
|---|---|---|---|---|---|
profile.OIDCCore | Optional for confidential clients | none | not required | none | none; cannot be combined with another profile |
profile.OAuth21 (default) | Required | none | not required | none | none |
profile.FAPI2Baseline | Required | PAR, JAR | DPoP or mTLS (DPoP if neither is configured) | none | Access token lifetime ≤ 10 minutes; PAR expires_in below 600 seconds |
profile.FAPI2MessageSigning | Required | PAR, JAR, JARM | DPoP or mTLS | none | The FAPI2Baseline limits |
profile.FAPICIBA | Not applicable (no /auth redirect) | JAR | DPoP or mTLS | grant.CIBA | The FAPI2Baseline limits |
Source: RequiredFeatures, RequiredAnyOf, RequiredGrants, RequiresPKCE, MaxAccessTokenTTL and PARLifetimeCeiling in op/profile.
Features are supplied, grants are not
A profile constrains its two neighbouring declaration axes differently, and the asymmetry is deliberate.
Either way, the resolved policy is recorded in the startup.profile audit event, so the configuration the OP actually booted with is readable from the audit trail rather than inferred from the option list.
Missing features are switched on for you. A feature flag such as PAR or JAR is policy the profile is entitled to decide, so declaring profile.FAPI2Baseline enables what it needs without a second option.
A missing grant fails op.New instead. Activating a grant drags in collaborators only the embedder can supply, so the library will not mount an endpoint the deployment never asked to serve. profile.FAPICIBA is the case that bites: the profile's entire subject matter is the /bc-authorize ceremony, but that endpoint is mounted from the grant set. Without the check, a deployment could declare the profile, have JAR and DPoP switched on for it, and still answer 404 to every backchannel-authentication request.
provider, err := op.New(
// …
op.WithProfile(profile.FAPICIBA),
op.WithCIBAGrant(op.CIBAGrant{HintResolver: resolver}),
)op.WithCIBAGrant is the option that activates the grant: it adds grant.CIBA to the grant set and takes the CIBA settings, such as CIBAGrant.HintResolver. The store must also provide a CIBA request substore. Declaring profile.FAPICIBA without it fails op.New, and the error names both the grant the profile requires and the option that activates it, so the fix can be read off the message. Listing grant.CIBA in op.WithGrants alone does not help, because it leaves the hint resolver unwired and moves the failure to the next check. Every profile other than profile.FAPICIBA requires no grant of its own.