Skip to content

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.

go
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-profile runs a profile.OIDCCore OP and a profile.OAuth21 OP side by side, then sends both the same confidential-client request without a code_challenge.

Decide deliberately ​

Deployment intentConfiguration
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 compatibleop.WithProfile(profile.OIDCCore); plan a PKCE migration
Financial-grade API profileop.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 ​

ProfilePKCEFeatures switched onSender-constrained tokensGrant requiredOther limits
profile.OIDCCoreOptional for confidential clientsnonenot requirednonenone; cannot be combined with another profile
profile.OAuth21 (default)Requirednonenot requirednonenone
profile.FAPI2BaselineRequiredPAR, JARDPoP or mTLS (DPoP if neither is configured)noneAccess token lifetime ≤ 10 minutes; PAR expires_in below 600 seconds
profile.FAPI2MessageSigningRequiredPAR, JAR, JARMDPoP or mTLSnoneThe FAPI2Baseline limits
profile.FAPICIBANot applicable (no /auth redirect)JARDPoP or mTLSgrant.CIBAThe 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.

A profile supplies features, never grants
Declaring a profile switches on the feature flags it needs. It never adds a grant: a grant the profile requires and the deployment did not declare makes op.New fail instead of quietly enabling it.op.WithProfile(…)Features: suppliedfeature.PAR · feature.JAR · …policy the profile can decide alonenot declared → the profile enables itGrants: never suppliedgrant.AuthorizationCode · grant.CIBA · …only you can supply what they neednot declared → op.New fails
The asymmetry is deliberate. A feature is a rule the profile can enforce on its own; a grant needs an authenticator, a store, sometimes a notification channel — things only you can provide, so silently enabling one would produce an OP that answers requests it cannot complete.

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.

go
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.