Protected resources and CORS
Since BetterAuth 1.7, OAuth protected resources (RFC 8707 audiences) are
first-class rows in the oauth_resource table instead of an in-memory list.
Each application URL is its own resource identifier, and every client is
linked to its resource through oauth_client_resource. CORS origins remain
dynamic runtime state.
How it works
Protected resources
- Creating an application with a
urlcreates anoauth_resourcerow whoseidentifieris that URL, and links the application's OAuth client to it. - Updating the URL re-points the resource link; clearing the URL removes the client's links (the
oauth_resourcerow itself is kept). - Deleting an application removes the client's links.
- Clients can send
resource=<app url>on authorization and token requests to receive a signed JWT access token whoseaudis that URL. Without a matching resource, an opaque token is issued, unlessDEFAULT_AUDIENCEis enabled (by default only in theoktaandkeycloakcompatibility profiles), in which case the application'surlis used as the resource.
CORS origins
At startup, allowed origins are seeded from CORS_ORIGINS and from every
applications.url. Creating, updating, or deleting an application updates the
origin set immediately — no restart needed. An origin stays trusted while
any registration still names it: if two applications share an origin, or an
application uses one listed in CORS_ORIGINS, removing or changing one of them
does not withdraw it.
This allow-list governs every authenticated route. The two discovery documents
at the root — /.well-known/openid-configuration and
/.well-known/oauth-authorization-server — are the one exception: a client
application's browser has to read them before any credential exists, and a
client that is not registered yet still has to resolve the issuer, so they are
served with Access-Control-Allow-Origin: * and without credentials. They
return public metadata only, so nothing is exposed by that, and no other route
is widened.
Configuration
# Comma-separated list of allowed CORS origins (static seed)
CORS_ORIGINS=https://app.example.com,https://admin.example.com
There is no free-form environment seed for audiences: resources are derived
from registered applications, and auth-service bootstraps its own
BETTER_AUTH_URL as a hidden system application so other clients can request a
JWT for the wallet self-service API.
Example
POST /api/admin/applications
Content-Type: application/json
{
"name": "My API",
"slug": "my-api",
"url": "https://api.example.com",
"redirectUris": ["https://app.example.com/callback"]
}
After this request:
https://api.example.comexists as a protected resource, is linked to themy-apiclient and is a valid CORS origin (the applicationurl).https://app.example.comis only a redirect URI: redirect URIs are not added to the CORS origins. Add that origin toCORS_ORIGINS, or give the application that serves it aurl.- Clients can include
resource=https://api.example.comin authorization requests to receive a JWT.
A client can only request a resource it is linked to: its own url, plus every
entry of its allowedResources list. Requesting any other resource fails with
invalid_target. To let an application call https://api.example.com, add
that URL to the application's allowedResources:
PATCH /api/admin/applications/:id
{ "allowedResources": ["https://api.example.com"] }
Every entry must be the url of another existing application (a registered resource), and must not be the application's own url, which is always linked. Omitting
allowedResources in an update leaves the list unchanged.
Runtime state
The configured integrations are exposed via the admin services endpoint. Only non-secret configuration state is returned — provider client secrets never leave the server.
GET /api/admin/services
Cookie: <admin session cookie>
{
"stripe": true,
"compat": {
"profile": "default",
"tokenAuthMethod": "basic",
"signingAlg": "EdDSA",
"defaultAudience": false,
"audienceParam": false,
"claimMappings": [],
"aliases": []
},
"providers": {
"google": { "enabled": false },
"github": { "enabled": true, "clientId": "gh_..." },
"linkedin": { "enabled": false },
"microsoft": { "enabled": false },
"apple": { "enabled": false }
}
}
Security considerations
- Audience validation — Always validate the
audclaim in received JWTs to ensure the token was intended for your service. - Per-client resources — A client may only request a resource it is linked to, so a compromised client cannot mint tokens for another application's audience.
- CORS origins — Only include origins you trust. Wildcard
*is not supported on authenticated routes; it applies solely to the two public discovery documents described above. - Cross-subdomain cookies — Set
SESSION_DOMAINto enable cross-subdomain session cookies (e.g.,SESSION_DOMAIN=example.com).