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: the OpenID Connect Core 1.0-compatible shape accepts a confidential authorization-code client without PKCE. Use profile.Baseline when the deployment follows OAuth 2.1 / RFC 9700 and every authorization-code request must carry PKCE.

go
provider, err := op.New(
  // issuer, store, keyset, cookie keys, clients, and login flow …
  op.WithProfile(profile.Baseline),
)

profile.Baseline changes that one rule only. It does not require PAR, cap token lifetimes, restrict client-authentication methods, or require sender-constrained tokens. Those are FAPI requirements; use FAPI 2.0 Baseline when that is the target profile.

At successful construction the OP emits startup.profile to its audit logger. The event records the declared profiles, features, and grants as well as the resolved policy, including pkce_required. Use it to confirm the deployed posture before the first request.

Source: examples/00-security-profile runs an unprofiled OP and a profile.Baseline OP side by side, then sends both the same confidential-client request without a code_challenge.

Decide deliberately

Deployment intentConfiguration
Legacy OIDC clients must remain compatibleno profile declared; plan a PKCE migration
OAuth 2.1 postureop.WithProfile(profile.Baseline)
Financial-grade API profileop.WithProfile(profile.FAPI2Baseline) or the applicable FAPI profile

Public and native clients still require PKCE even without profile.Baseline; the visible difference is the confidential-client compatibility path.

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 · feature.JARM · …policy the profile is entitled to decide on its ownnot declared → the profile switches it onGrants — never suppliedgrant.AuthorizationCode · grant.CIBA · …only the embedder can supply the collaborators 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.WithGrants(grant.CIBA),
  op.WithProfile(profile.FAPICIBA),
  op.WithCIBA(op.WithCIBAHintResolver(resolver)),
)

The error names both the grant the profile requires and the option that activates it, so the fix is readable off the message. WithCIBA takes CIBA options such as op.WithCIBAHintResolver; it does not replace the explicit op.WithGrants(grant.CIBA) declaration. Every profile other than profile.FAPICIBA requires no grant of its own.