OAuth 2.1
Summary
Benevia Core includes a complete OAuth 2.1 authorization server. It lets OAuth-capable clients — such as Claude and ChatGPT custom connectors — get delegated, scoped access to your API on behalf of a signed-in user. Clients register themselves, the user approves access on a consent screen your app hosts, and the client receives tokens capped to the access level the user chose.
Key properties:
- Stateless client registration — the
client_idis the registration data, HMAC-signed by the server. Nothing is stored, so any client can self-register without an admin step. - PKCE everywhere — all clients are public clients; authorization code + PKCE is the only interactive flow.
- Multi-tenant without tenant URLs — endpoints are rootless (
/connect/...); the tenant travels inside the authorization code and token claims. - Scoped access — the user grants read, read+write, or full access per connection. The cap is enforced on every OData request and MCP tool call, on top of the user's own RBAC permissions.
How a client connects
Discovery → Registration → Authorize → Sign-in + Consent → Code → Tokens → Bearer requests
- The client reads
/.well-known/oauth-protected-resourceand/.well-known/openid-configurationto find the endpoints. - It registers itself at
POST /connect/registerand receives a self-encodedclient_id. - It opens
GET /connect/authorizein the user's browser. Core redirects to your app's consent page. - The user signs in and approves an access level. Your app records the choice via
POST api/oauth/consent/approve. - The browser returns to
/connect/authorize; Core issues an authorization code bound to the user's tenant. - The client exchanges the code (plus its PKCE verifier) at
POST /connect/tokenfor access and refresh tokens. - The client calls
/mcpand the OData API withAuthorization: Bearer <access token>.
Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/.well-known/openid-configuration |
GET | Authorization-server discovery document |
/.well-known/oauth-protected-resource |
GET | Protected-resource metadata for MCP clients (mapped by UseCoreMcp()) |
/connect/register |
POST | Dynamic Client Registration (anonymous) |
/connect/authorize |
GET | Authorization endpoint — consent and code issuance |
/connect/token |
POST | Token endpoint — code exchange and refresh |
/connect/revoke |
POST | Token revocation |
api/oauth/consent/info |
GET | Client name and scope descriptions for your consent UI |
api/oauth/consent/approve |
POST | Records the user's consent choice |
api/oauth/connected-apps |
GET | The current user's connected apps |
api/oauth/connected-apps/{grantId}/disconnect |
POST | Revokes a connection and all its tokens |
Scopes
| Scope | Grants |
|---|---|
benevia.read |
Read data |
benevia.write |
Create and update data |
benevia.delete |
Delete data |
offline_access |
Refresh tokens (plumbing — not shown to users) |
The data scopes form a per-connection access ceiling: the highest granted scope decides the level (delete → full, write → read+write, otherwise read-only). The ceiling only caps what the user could already do through RBAC — it never adds access. Writes and deletes above the ceiling are rejected on both the OData API and MCP tools.
Guides
| Guide | Covers |
|---|---|
| Setup | Wiring AddCoreOAuth into your host and the required configuration |
| Consent page | The consent UI contract your client app must implement |
| Connected apps | Letting users list and disconnect their connections |
| MCP connectors | Connecting Claude / ChatGPT end to end, and troubleshooting |
Notes
- Token and grant state (codes, refresh tokens, grants) is stored per tenant; client registrations are never stored.
- A user can connect the same client several times; each connection is an independent grant.
- See Expose entities via MCP for the MCP server itself and Use your API for JWT sign-in used by first-party clients.