Use case — Custom consent UI
What is "consent" in OIDC?
After the user authenticates, OIDC Core 1.0 §3.1.2.4 requires the OP to obtain an authorization decision before releasing information. An interactive dialogue in which the user — not the RP — approves or denies the requested scopes (profile, email, anything else the RP listed) is one permitted way to obtain it; prior administrative consent or another established condition can also suffice. This page covers the library's interactive consent UI path between login and the redirect.
The bundled consent page works, but you almost certainly want it branded — your logo, your copy, your privacy / TOS links, your i18n. For small copy changes, locale bundle overlays are enough. For a branded server-rendered page, pass op.WithConsentUI. For full client-side rendering, use the JSON driver.
Specs referenced on this page
- OpenID Connect Core 1.0 — §3.1.2.4 (authorization decision and consent)
- RFC 6749 — OAuth 2.0 Authorization Framework, §3.1 (authorization endpoint)
- RFC 9700 — OAuth 2.0 Security Best Current Practice (CSRF mitigation, etc.)
Vocabulary refresher
- Consent screen — When the OP uses interactive dialogue, this is the page between login and the redirect-back-with-code where the user approves the scopes a particular RP is requesting. The OP decides what data to ask about; the user decides whether to release it.
- CSRF token — A per-session secret embedded in a form so a hostile third-party site cannot trick the browser into submitting "Approve" on the user's behalf. The OP issues and validates the token; SPA / template only echo it back in the POST body or the
X-CSRF-Tokenheader. - Content Security Policy (CSP) — A response header (
Content-Security-Policy: default-src 'none'; ...) that tells the browser which resources a page may load. The OP renders the bundled consent page under a strict default policy that blocks<script>, inline event handlers, and external assets, so a hostile RPclient_namecannot escalate into XSS.
Path 1 — Locale bundle overlay (minor copy / i18n)
If you only need to change the strings (translated copy, brand voice, partial translations), op.WithLocale overlays your keys on top of the seed en / ja bundles at key granularity. The bundled consent page picks up the overlay automatically.
brandJa, _ := op.LocaleBundleFromMap("ja", map[string]string{
"consent.title": "Acme へのアクセス許可",
"consent.scope.profile.description": "プロフィール情報の参照",
})
op.New(
/* required options */
op.WithLocale(brandJa),
)Bundle keys follow the surface they render — see Use case: i18n / locale negotiation for the catalogue and the resolution chain.
This path keeps the bundled HTML driver, the bundled CSP, and the bundled CSRF / cookie scheme. You only change strings.
Path 2 — Custom consent template (server-rendered)
When you want branded HTML but still want the OP to own state, CSRF, and consent persistence, pass op.WithConsentUI with a parsed *html/template.Template. The template receives interaction.ConsentTemplateData, including Client, Scopes, StateRef, CSRFToken, ApprovedScopesField, SubmitMethod, and SubmitAction.
tmpl := template.Must(template.ParseFiles("consent.html"))
op.New(
/* required options */
op.WithConsentUI(op.ConsentUI{Template: tmpl}),
)See examples/11-custom-consent-ui for the form field shape.
CSP for a custom template
op.ConsentUI has a ContentSecurityPolicy field. An empty value selects the library default (default-src 'none'; style-src 'none'; frame-ancestors 'none'; base-uri 'none'), which is intentionally too restrictive for a template that loads branding assets. A non-empty value is normalized by the public interaction.NormalizeCSP seam at op.New: you may add the style-src, img-src, font-src, or script-src policy your template needs, but form-action is forbidden, and frame-ancestors and base-uri must be exactly 'none' when present. Missing frame-ancestors and base-uri are appended automatically. Invalid policy returns interaction.ErrCSPNotPermitted through the configuration error.
Allowing scripts is a conscious trust decision: the page contains the CSRF token and continuation state. html/template escaping protects values such as the RP's display name, but it does not make arbitrary script execution harmless.
Path 3 — JSON driver (full markup control)
If you need to own the markup (custom layout, brand assets, framework-rendered consent), switch to the JSON driver. The OP returns the consent prompt as { type: "consent.scope", data: { scopes: [...] }, csrf_token, ... } JSON; your page or SPA renders it and posts the user's choice back.
import "github.com/libraz/go-oidc-provider/op/interaction"
op.New(
/* required options */
op.WithInteractionDriver(interaction.JSONDriver{}),
)The SPA echoes prompt.csrf_token into the X-CSRF-Token header on submission; the OP compares against the __Host-oidc_csrf cookie (double-submit pattern). See SPA / custom interaction for the worked end-to-end flow.
What you don't have to write either way
| Concern | Owned by the OP |
|---|---|
| CSRF token generation / validation | yes |
| Origin / Referer check on the POST | yes |
Cookie scope (__Host-, Secure, SameSite) | yes |
| Session lookup / consent persistence | yes |
| Rendering the redirect back to the RP after approval | yes |
Path 1 (locale bundle) keeps the bundled CSP (default-src 'none'; style-src 'none'; frame-ancestors 'none'; base-uri 'none') and the rendered HTML. For Path 2 (custom template), set ConsentUI.ContentSecurityPolicy rather than trying to add form-action for the RP redirect — form-action would block successful cross-origin completion. Path 3 (JSON driver) lets your own page set the CSP it needs.
The POST's Origin / Referer ceremony check is not derived from the client's redirect URI. It accepts the issuer origin and origins explicitly supplied with op.WithCORSOrigins; a redirect URI on another origin does not authorize that origin to submit interaction or end-session state.
Read next
- SPA / custom interaction — full SPA wiring on the JSON driver.
- i18n / locale negotiation — the locale bundle overlay path.
- CORS for SPA — when your front-end is on another origin.