Protect a resource
Register an API audience, validate access tokens, and enforce scopes and organization boundaries.
An API must validate each access token before trusting its claims. Configure your language's OAuth resource-server middleware with the identity issuer, signing keys, and your API's resource identifier. Better Auth has a resource client for JavaScript services; any library that implements the same checks can be used.
Register the API audience
Ask an identity administrator to register or confirm your OAuth resource. Supply an absolute URI that identifies the API, its allowed scopes, and any token lifetime overrides.
{
"identifier": "https://dev.api.graphxserver.io/orders",
"name": "Orders API",
"allowedScopes": ["orders.read", "orders.write"],
"accessTokenTtl": 3600,
"refreshTokenTtl": 604800,
"disabled": false
}This is a resource-policy example for the administrator, not a public registration request. Identity's admin backend exposes resource creation and updates through admin.resources.create and admin.resources.update. Those operations require application-management permissions. They are outside the published Better Auth HTTP OpenAPI contract.
The server also seeds resources from its service URL configuration. The source defaults are:
| API | Resource identifier |
|---|---|
| Orders | https://dev.api.graphxserver.io/orders |
| Billing | https://dev.api.graphxserver.io/billing |
| Catalog | https://dev.api.graphxserver.io/catalog |
| Product lines | https://dev.api.graphxserver.io/pdln |
| Payments | https://dev.api.graphxserver.io/payment |
| Storage | https://dev.api.graphxserver.io/storage |
| Identity UserInfo | https://dev.auth.graphxserver.io/api/auth/oauth2/userinfo |
An administrator can change persisted resource policies, and deployment configuration can override defaults. Confirm the identifier before using it. A resource policy cannot introduce an arbitrary OAuth scope: the scope must exist in the identity server's configured scope list too.
The current provider configuration sets enforcePerClientResources: false. Do not assume that assigning a client to a resource is an authorization boundary. Validate audience and scope at the API, and configure explicit client restrictions where your service needs them.
Validate every request
For a JWT access token, configure these checks in your resource-server library:
- Read a single bearer credential from the
Authorizationheader. Reject missing or malformed credentials. - Verify the signature against the trusted issuer's JWKS. The current server signs with
RS256; allow only the algorithms approved for your deployment. - Require
issto equal the configured issuer exactly. - Require
audto contain your registered resource identifier. Handle both a string audience and an array. - Require an unexpired
expand validatenbfwhen present, with a small configured clock tolerance. - Read
scopeas a space-separated set and require the scope for this operation. - Apply the service's organization, subject-type, and permission checks before accessing data.
Read discovery and JWKS from your configured issuer. Do not choose a key URL or issuer from unverified token fields. Cache signing keys and support key rotation through your library. Decoding a JWT without verifying it proves nothing about who issued it.
token = read_bearer(request)
claims = verify_jwt(
token,
jwks = trusted_discovery.jwks_uri,
issuer = configured_issuer,
audience = configured_resource_identifier,
algorithms = ["RS256"],
required_claims = ["iss", "aud", "exp", "sub"]
)
require_scope(claims.scope, "orders.read")
organization = require_verified_organization_if_endpoint_is_tenant_scoped(claims)
require_record_belongs_to_organization(record, organization)
require_user_permission_if_endpoint_acts_for_a_user(token)This is pseudocode. Use a maintained verification library for the cryptographic checks.
Return 401 for a missing, expired, or invalid token, with an appropriate WWW-Authenticate: Bearer challenge. Return 403 when a valid token lacks the required scope or permission. A token issued only for Billing must fail at Orders, even if it contains a scope name that Orders recognizes.
Keep organization boundaries
User access tokens can include these Graphxsource claims:
| Claim | Meaning |
|---|---|
sub | The user subject for a user-delegated token; a client subject for a client-only token. |
sid | The associated identity session, when present. |
active_organization.id | The organization selected for this token. |
active_organization.name, logo, slug | Organization details when org.name is granted. |
active_organization.roles | Organization roles when org.roles is granted. |
org_list | The user's organizations when org.list is granted. |
Request an organization scope during sign-in if your API needs an organization-bound token. Check that the verified organization ID matches any organization in the route, body, and records you load. A caller-supplied organization header is not evidence of membership.
The permission-check API takes the organization from the presented token. Changing the active organization in another browser tab does not rewrite an existing token. Obtain a token for the intended organization before switching the application's tenant context.
Role claims describe the token at issuance. For decisions that need current membership or grants, use permission checks and enforce the result on the server. A UI that hides a button does not protect its backing endpoint.
Client credentials tokens have no user membership by default. Choose an explicit service authorization policy for them. An organization API key exchange can issue a token with an organization context, but it still represents a client rather than a user.
Check current token status
Local JWT verification avoids a network request on each call. It cannot by itself tell you that a client was disabled, a session was revoked, or another server-side policy changed after issuance.
When you need current status, use the discovered introspection_endpoint with an authorized confidential client's credentials. Check the endpoint's client ownership and audience rules with the identity administrator. Do not assume one resource's credentials may introspect every client's token.
curl --fail-with-body \
--request POST "$IDENTITY_ORIGIN/api/auth/oauth2/introspect" \
--user "$CLIENT_ID:$CLIENT_SECRET" \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "token=$ACCESS_TOKEN" \
--data-urlencode 'token_type_hint=access_token'This example assumes client_secret_basic. Require active: true and still check the returned issuer, audience, expiry, and scopes. Fail closed if the required status or permission check cannot complete. If you cache decisions, choose a lifetime that matches how quickly your service must honor revocation.
The Better Auth JWT guide explains signing keys. The OAuth provider guide covers resource-bound tokens, introspection, and revocation.