OAuth Resource Enforcement#
OAuth Resource Enforcement is the per-client access control layer in Better Auth's OAuth provider, introduced in PR #9648. It replaces the previous flat validAudiences list with a persistent entity-based system that models each protected resource explicitly and enforces which OAuth clients are authorized to access it.
The system has two interlocking parts:
oauthResource— a first-class database entity representing a protected resource server (e.g.,https://api.example.com), carrying per-resource token policy (TTLs, scope allowlists, signing config, custom claims).oauthClientResource— a join table that links specific clients to specific resources, enabling per-client entitlement checks at token issuance.
The enforcePerClientResources flag determines whether the linkage table is consulted: when true (the default), every client must be explicitly linked to a resource before it can request tokens for it; when false, all enabled resources are implicitly available to any client .
Data Model#
oauthResource Table#
Defined in packages/oauth-provider/src/schema.ts, each row represents a protected resource server with the following key fields:
| Field | Description |
|---|---|
identifier | Business key — RFC 8707 absolute URI; used as the aud claim in issued tokens |
name | Human-friendly label for admin UIs |
accessTokenTtl / refreshTokenTtl | Per-resource token lifetimes (null = inherit plugin default) |
signingAlgorithm / signingKeyId | Optional JWS algorithm/key pin for this resource |
allowedScopes | Scope allowlist — requested scopes are narrowed to the intersection |
customClaims | Per-resource custom JWT claims (RFC 9068 reserved names stripped at issuance) |
disabled | Lifecycle flag — disables new issuance; existing tokens still verify |
policyVersion | Forward-migration anchor |
oauthClientResource Join Table#
Defined in packages/oauth-provider/src/schema.ts, this table links clients to resources. Composite uniqueness of (clientId, resourceId) is enforced by using a deterministic primary key: the row id is set to ${clientId}::${resourceId} at write time, making duplicate inserts a no-op against the PK constraint .
Known issue (v1.7.0-rc.2): When
advanced.database.generateId: "uuid"is configured, the core UUID transformer rejects the composite string ID and substitutes a random UUID instead. This breaks the PK-based uniqueness mechanism, leaving no compound unique constraint on(clientId, resourceId). See issue #10531 for details and possible fixes.
enforcePerClientResources Flag#
Configured on the oauthProvider plugin, this flag defaults to true — the secure-by-default posture per RFC 8707 §3. The resolved value is logged at plugin initialization so operators can confirm which default applies .
| Value | Behavior |
|---|---|
true (default) | Clients must be explicitly linked via oauthClientResource to every requested resource. Unlinked requests return invalid_target. |
false | All enabled (non-disabled) resources are implicitly accessible to any client — reproduces pre-PR #9648 behavior. |
Resource Policy Resolution Pipeline#
When a client includes an RFC 8707 resource parameter in a token or authorization request, resolveResourcePolicy() in resources.ts executes these steps in order:
- Resolve
resourceparameter to a list of identifiers - Look up
oauthResourcerows — missing or disabled resources →invalid_targeterror - Per-client linkage check — if
enforcePerClientResourcesistrue, verify each resource is linked to the requesting client viaoauthClientResource - Intersect requested scopes with each resource's
allowedScopes(narrowing only, never widening) - Pick the shortest
accessTokenTtlandrefreshTokenTtlacross all requested resources - Validate signing config compatibility — conflicting
signingAlgorithmorsigningKeyIdacross resources raisesinvalid_request - Merge per-resource
customClaims, stripping RFC 9068 reserved claim names
Client-Resource Linking#
Admin Endpoints#
The oauthResource/endpoints.ts exposes two admin-only endpoints for managing linkage:
POST /admin/oauth2/resources/:identifier/clients/:client_id— Links a client to a resource. The insert is idempotent: a duplicate write (detected via UNIQUE PK conflict) returns{ linked: true, alreadyLinked: true }rather than an error .DELETE /admin/oauth2/resources/:identifier/clients/:client_id— Unlinks a client from a resource. Idempotent — no error if the link doesn't exist .
Both endpoints respect the resourcePrivileges RBAC callback configured on the plugin, gating access with the "link" and "unlink" actions respectively.
Dynamic Client Registration (DCR)#
Clients can declare resources at registration time via RFC 7591 §2 extension :
{
"client_name": "my-client",
"redirect_uris": ["https://client.example.com/callback"],
"resources": ["https://api.example.com"]
}
Resources are validated (must exist and not be disabled) before client creation. Client-resource links are created transactionally with client creation — no half-created state is possible. Duplicate entries in the resources array result in exactly one oauthClientResource row.
Resource Seeding#
The plugin's resources option seeds oauthResource rows at boot. Behavior is controlled by resourceSeedMode :
| Mode | Behavior |
|---|---|
"insertOnly" (default) | Insert missing rows only; admin edits are preserved on restart |
"merge" | Insert missing + update only explicitly configured fields |
"overwrite" | Insert missing + replace all policy columns with config values |
Key Files#
| File | Purpose |
|---|---|
packages/oauth-provider/src/schema.ts | oauthResource and oauthClientResource table definitions |
packages/oauth-provider/src/resources.ts | resolveResourcePolicy(), seedResources(), resolveEnforcePerClientResources() |
packages/oauth-provider/src/oauthResource/endpoints.ts | Admin CRUD + link/unlink endpoints for resources |
packages/oauth-provider/src/oauthResource/index.ts | Resource module exports |
packages/oauth-provider/src/token.ts | Token issuance — applies resource policy to access token creation |
packages/oauth-provider/src/authorize.ts | Authorization endpoint — validates resource parameter |
packages/oauth-provider/src/register.ts | DCR — transactional resource linking during client registration |