Use case — Grant management
When a grant is long-lived — a recurring payment mandate, a standing data-access consent — the client needs more than "authorize once and hope". It needs to name a grant, add to it later, read back what it currently holds, and tear it down. The OAuth 2.0 Grant Management draft gives each grant a stable grant_id and five actions over it.
op.WithGrantManagement honours the grant_management_action / grant_id authorization parameters, mounts the grant management endpoint (query / revoke), stamps grant_id onto the token response, and advertises the feature in discovery.
Use it when clients must manage a consent-like grant after the first authorization, especially when the grant can grow or be revoked independently of one browser session. Do not enable it for short-lived, one-shot sign-in flows: regular authorization codes, refresh-token rotation, and normal token revocation are simpler when the client never needs to name a grant.
Only revoke reaches the bottom box. create, replace and merge change what the grant holds; query reads it back; revoke deletes the grant record and tears down the tokens hanging off it.
Experimental — tracks an IETF draft
Grant Management is an IETF draft, not a published RFC. The op.GrantManagementAction enum and op.WithGrantManagement are marked Experimental: in godoc: a draft update may make a wire-incompatible change.
The five actions
create— start a brand-new grant. Agrant_idMUST NOT accompany it; the OP mints one and returns it.replace— overwrite the referenced grant's scope andauthorization_detailswith the new request's. Requiresgrant_id.merge— union the referenced grant's scope andauthorization_detailswith the new request's. Requiresgrant_id.query— read the grant.GET {grant_management_endpoint}/{grant_id}.revoke— delete the grant.DELETE {grant_management_endpoint}/{grant_id}.
create / replace / merge ride the authorization request. query / revoke are operations on the grant management endpoint.
Wiring
import "github.com/libraz/go-oidc-provider/op"
op.New(
/* required options */
op.WithGrantManagement([]op.GrantManagementAction{
op.GrantActionCreate, op.GrantActionReplace, op.GrantActionMerge,
op.GrantActionQuery, op.GrantActionRevoke,
}, false),
)The first argument is the closed set of actions the OP accepts; it is advertised as grant_management_actions_supported, and the endpoint refuses any operation outside it. The second argument is actionRequired: when true, an authorization request that omits grant_management_action is rejected (discovery advertises grant_management_action_required: true). At least one action is required and each must be one of the five values; a duplicate or unknown action is rejected at op.New.
The discovery document then advertises:
{
"grant_management_endpoint": "https://op.example.com/oidc/grant_management",
"grant_management_actions_supported": ["create", "replace", "merge", "query", "revoke"]
}The endpoint path defaults to /grant_management under your mount prefix; override it with op.WithEndpoints(op.Endpoints{GrantManagement: "..."}).
Creating and growing a grant
The client passes grant_management_action (and, for replace / merge, the grant_id) on the authorization request:
# Create — no grant_id allowed
curl -G --data-urlencode 'grant_management_action=create' \
--data-urlencode 'response_type=code' \
--data-urlencode 'client_id=demo' \
--data-urlencode 'redirect_uri=https://rp.example.com/callback' \
--data-urlencode 'scope=openid profile' \
https://op.example.com/oidc/authThe same parameters are validated on a PAR push. After the code exchange, the token response carries the grant's id:
{
"access_token": "...",
"token_type": "Bearer",
"grant_id": "g-9f3c..."
}To grow that grant later, the client re-authorizes with grant_management_action=replace (overwrite the scope / authorization_details) or merge (union them), passing the grant_id it received.
| Action | grant_id | Effect on scope / authorization_details |
|---|---|---|
create | must be absent | sets them to the new request's |
replace | required | overwrites with the new request's |
merge | required | unions the existing with the new request's |
Querying a grant
GET the endpoint with the grant id appended, authenticating as the owning client. The OP enforces client ownership — a client cannot read another client's grant — and returns the grant's current scope and authorization_details:
curl -u demo:demo-secret https://op.example.com/oidc/grant_management/g-9f3c...{
"scopes": [{"scope": "openid profile"}],
"authorization_details": [{"type": "payment_initiation", "actions": ["initiate"]}]
}Revoking a grant
DELETE tears down only the named grant_id and cascades to the artefacts bound to that grant: it tombstones / denylists the grant's JWT access tokens, revokes the opaque access tokens and refresh tokens bound to it, and deletes the grant record so a subsequent query reports it gone. Other grants held by the same subject and client are not touched.
On success, the OP answers 204 No Content and emits a grant_management.revoked audit event for the named grant. A confirmed missing grant or a grant owned by another client returns 404 without attempting a cascade. If the grant store cannot establish whether the grant exists, the OP returns 500 server_error and performs no cascade. If a cascade side effect fails, the OP returns 500 server_error, emits grant_management.revoke_failed, and leaves the grant retryable/queryable; it does not claim revocation succeeded. A final delete that races with another successful deletion is treated as idempotent success and still returns 204.
Treat grant_id as the revocation unit; if a deployment wants to revoke every grant for a subject or client, it must enumerate and delete those grant IDs explicitly.
curl -u demo:demo-secret -X DELETE https://op.example.com/oidc/grant_management/g-9f3c...If an operation is not in the advertised grant_management_actions_supported set, the endpoint answers 405 Method Not Allowed with an Allow header naming only the enabled operations — so a client that registered only query will not see DELETE accepted.
Read next
- Rich authorization requests — the
authorization_detailsa grant carries andmergeunions. - Refresh tokens — what a revoke cascade tears down.
- Audit events — the
grant_management.revokedevent shape.