Use case — Grant management
A grant is the OP's record of what a user consented to for one client: the scopes and, with RAR, the authorization_details. Ordinarily the OP keeps a single grant per user and client and quietly amends it on each new authorization, and the client never names it. When a grant is long-lived (a recurring payment mandate, a standing data-access consent), the client needs to name it, add to it later, read back what it 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. Skip it for short-lived sign-in flows: authorization codes, refresh-token rotation, and ordinary token revocation are simpler when the client never needs to name a grant.
replace that drops an authorization_details element withdraws it from opaque access tokens and refresh chains; a JWT already issued keeps the claim until it expires.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 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. Eachcreatemakes a separate grant, even for the same user and client.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.GrantManagement{
Actions: []op.GrantManagementAction{
op.GrantActionCreate, op.GrantActionReplace, op.GrantActionMerge,
op.GrantActionQuery, op.GrantActionRevoke,
},
ActionRequired: false,
}),
)Actions 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. With ActionRequired set to 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, and so is a second WithGrantManagement call.
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. A create or replace / merge request always goes through an explicit consent screen: the first-party consent skip and a cached grant never cover it. After the code exchange, the token response carries the grant's id:
{
"access_token": "...",
"token_type": "Bearer",
"grant_id": "g-9f3c..."
}To change 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. A grant_id that does not belong to the signed-in user and this client is refused with invalid_grant.
| 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. Only confidential clients may call the endpoint; a public client gets 401 invalid_client, because a grant_id in a URL path is not a secret (it reaches proxy logs and browser history). The OP enforces ownership, so 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"]}]
}A member is omitted when the grant holds nothing for it.
Revoking a grant
DELETE tears down only the named grant_id, and everything bound to it: the grant's JWT access tokens (by tombstone, or by the registry, depending on the revocation strategy), its opaque access tokens and refresh tokens, and finally the grant record, so a later query reports it gone. Other grants held by the same subject and client are not touched. To revoke every grant of a subject or client, enumerate and delete those grant_ids yourself.
| Outcome | Response |
|---|---|
| Revoked | 204 No Content, and a grant_management.revoked audit event for the grant. A delete that races with another successful deletion also returns 204. |
| Grant does not exist, or belongs to another client | 404 invalid_request, with no cascade attempted. |
| The grant store cannot say whether the grant exists | 500 server_error, with no cascade. Retry. |
| A cascade step fails | 500 server_error and a grant_management.revoke_failed audit event. The grant record is kept, so it stays queryable and the delete can be retried. |
| Public client | 401 invalid_client |
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.