Persist an OidcPolicy, validate it on the server, and verify inbound tokens.
The form emits an OidcPolicy on submit. Persist that object. Then verify inbound tokens against the policies you stored.
In the homepage builder, turn on Page + actions and Token verifier so Get Code installs page-actions and verify with the form.
The policy
Every provider form emits the same shape:
type OidcPolicy = {
issuer: string
claims: Record<string, string[]>
label?: string
}Matching rules:
issuermust equal the token'sissexactly.- Every listed claim must be present. Values match by exact string equality. Arrays match on intersection.
- Unlisted claims are not checked.
- A policy with no claim rules never matches.
- Validation requires an
audrule. issuermust behttpswith no query or fragment.
Save the policy
Render the installed policy-form.tsx on a page:
import { OidcPolicyForm } from "@/components/oidc/policy-form"
export default function Page() {
return <OidcPolicyForm />
}Each provider form calls onSubmit(policy) after client-side checks pass. The generated form already wires that callback. With Page + actions on, it calls savePolicy:
const onSubmit = async (policy: OidcPolicy) => {
const result = await savePolicy(policy)
setStatus(result?.errors ? "invalid" : "saved")
}Without that add-on, onSubmit logs the policy. Replace it with your save.
Validate on the server
Call validateOidcPolicy on the server before you persist. It returns { path: string; message: string }[]. If that array is non-empty, return { errors } and skip the write. Do not throw.
page-actions writes this stub to app/settings/oidc/actions.ts:
"use server"
import { validateOidcPolicy, type OidcPolicy } from "@/lib/oidc/policy"
export async function savePolicy(policy: OidcPolicy) {
const errors = validateOidcPolicy(policy)
if (errors.length > 0) {
return { errors }
}
// Replace with your storage.
console.log("save policy", policy)
}The generated form treats a non-empty result.errors as invalid, otherwise saved:
const result = await savePolicy(policy)
setStatus(result?.errors ? "invalid" : "saved")You can surface errors[].path and errors[].message in the UI. Replace the console.log with a write to your database. Keep the validateOidcPolicy check. The verifier refuses stored policies that fail it.
Verify tokens
verify writes lib/oidc/verify.ts. Call verifyOidcToken on the server only. It discovers the issuer's JWKS, checks signature and expiry, then matches claims against your policies.
import { verifyOidcToken } from "@/lib/oidc/verify"
import type { OidcPolicy } from "@/lib/oidc/policy"
async function loadPolicies() {
// Replace with your storage.
const policies: OidcPolicy[] = []
return policies
}
export async function POST(request: Request) {
const token = request.headers.get("authorization")?.replace(/^Bearer\s+/i, "")
const policies = await loadPolicies()
const result = await verifyOidcToken(token, policies)
if (!result.ok) {
return Response.json(
{ ok: false, code: result.code, message: result.message },
{ status: 401 }
)
}
return Response.json({ ok: true, claims: result.claims })
}Add the item with:
npx shadcn@latest add @oidc-ui/verifyOn success, result.policy is the matching OidcPolicy and result.claims is the verified payload.
On failure, result.code is one of:
| Code | Meaning |
|---|---|
missing-token | No token was provided |
malformed-token | Not a JWT with an iss claim |
unknown-issuer | No stored policy trusts that issuer |
invalid-policy | Policies for that issuer failed validateOidcPolicy |
discovery-failed | OIDC discovery or JWKS fetch failed |
invalid-token | Signature, expiry, or issuer check failed |
claims-mismatch | Token is valid but no policy accepts its claims |
For claims-mismatch, result.explanations lists the failed checks per policy.