> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nextlevelmca.com/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth and security

> How MCP clients authenticate: discovery, registration, PKCE, token lifetimes, scopes and roles, tenant isolation and revocation.

NextLevel MCA runs its own OAuth 2.1 authorization server at `https://api.nextlevelmca.com`. MCP clients discover it, register themselves, and send users through a magic-link sign-in before any token is issued. This page is the reference for what the server does; you do not need to implement any of it to use Claude or ChatGPT.

## Discovery

| Document                                 | URL                                                                                                                    |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Protected resource metadata (RFC 9728)   | `https://api.nextlevelmca.com/.well-known/oauth-protected-resource` (also `/.well-known/oauth-protected-resource/mcp`) |
| Authorization server metadata (RFC 8414) | `https://api.nextlevelmca.com/.well-known/oauth-authorization-server`                                                  |
| JWKS                                     | `https://api.nextlevelmca.com/oauth/jwks`                                                                              |

An unauthenticated request to `/mcp` returns `401` with `WWW-Authenticate: Bearer resource_metadata="https://api.nextlevelmca.com/mcp/.well-known/oauth-protected-resource"`, which is how clients find the authorization server without configuration.

```json GET /.well-known/oauth-protected-resource theme={null}
{
  "resource": "https://api.nextlevelmca.com/mcp",
  "authorization_servers": ["https://api.nextlevelmca.com"],
  "scopes_supported": ["businesses:read", "businesses:write", "people:read", "people:write", "deals:read", "deals:write", "documents:read", "documents:write", "lenders:read", "lenders:write", "submissions:write", "offers:read", "offers:write", "advances:read", "advances:write", "portal:send", "webhooks:manage"],
  "bearer_methods_supported": ["header"]
}
```

```json GET /.well-known/oauth-authorization-server theme={null}
{
  "issuer": "https://api.nextlevelmca.com",
  "authorization_endpoint": "https://api.nextlevelmca.com/oauth/authorize",
  "token_endpoint": "https://api.nextlevelmca.com/oauth/token",
  "registration_endpoint": "https://api.nextlevelmca.com/oauth/register",
  "revocation_endpoint": "https://api.nextlevelmca.com/oauth/revoke",
  "jwks_uri": "https://api.nextlevelmca.com/oauth/jwks",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"],
  "scopes_supported": ["businesses:read", "…"]
}
```

## Dynamic client registration

`POST /oauth/register` (RFC 7591) is open, as MCP expects. Send `client_name`, `redirect_uris` and optionally `logo_uri`, `client_uri`, `grant_types` and `token_endpoint_auth_method`. Redirect URIs must be absolute `https` URLs (`http://localhost` and `http://127.0.0.1` on any port are allowed for local clients), with no wildcards or fragments. The response is `201` with a `client_id`; clients are public (`token_endpoint_auth_method: "none"`), there is no client secret. Registration is limited to 10 per hour per IP address, and clients unused for 90 days are removed.

## Authorization flow

1. **Authorize.** `GET /oauth/authorize` with `response_type=code`, `client_id`, `redirect_uri`, `scope`, `state`, `code_challenge`, `code_challenge_method=S256` and optionally `resource`. `client_id` and `redirect_uri` are validated first; on failure the server renders an error page and never redirects to an unvalidated URI. Only PKCE **S256** is accepted; `plain` is rejected. Request parameters are stored server-side under an opaque request id for 30 minutes, not carried in URLs.
2. **Sign in.** The user enters their email. If it belongs to an active user, a magic link is sent; the page says "check your email" either way, so addresses cannot be enumerated. Links are single use, expire after **10 minutes**, and are limited to 3 per email per 15 minutes. Opening one sets a signed, `HttpOnly`, `Secure`, `SameSite=Lax` session cookie that lives 30 minutes.
3. **Location.** Users who belong to several locations pick one; users with one location skip this. The grant is pinned to that location.
4. **Consent.** The user sees the client, the location and each scope in plain words, then approves or denies. An existing, unrevoked consent for the same client, location and scopes skips this step.
5. **Code.** The server issues an authorization code bound to the client, redirect URI, scopes, location and PKCE challenge. Codes expire after **60 seconds** and are single use; presenting a consumed code revokes everything issued from it.
6. **Token.** `POST /oauth/token` (JSON or form-encoded) with `grant_type=authorization_code`, `code`, `redirect_uri`, `client_id` and `code_verifier` returns an access token, a refresh token, `expires_in: 3600` and the granted `scope`. Limited to 60 requests per minute per IP address.

If the client asks for no scopes (Claude and ChatGPT connectors don't send any), the consent screen offers every scope, each with a checkbox ticked by default. Untick what the connection should not be able to do; the token only ever carries the scopes you left ticked, and a client that later asks for more shows the consent screen again.

## Tokens

**Access tokens** are RS256-signed JWTs, valid for **1 hour**, verified against the JWKS by `kid`. Claims: `iss` (`https://api.nextlevelmca.com`), `sub` (the user id), `aud` (the `resource` requested, by default `https://api.nextlevelmca.com/mcp`), `location_id`, `scope`, `client_id`, `iat`, `exp` and `jti`. The MCP server only accepts tokens whose `aud` is its own URL, so a token cannot be replayed against another service; `/v1` accepts tokens minted for either the MCP server or the API.

**Refresh tokens** are opaque, stored hashed, and rotate on every use: each refresh returns a new refresh token in the same family and revokes the old one. Presenting an already-rotated token is treated as theft and revokes the whole family. Lifetime is **30 days, sliding** on each rotation, with an absolute cap of **90 days** from the first grant.

Every token, code and magic link is stored as a SHA-256 hash; raw values are never persisted.

## Scopes and roles

Two independent checks apply to every tool call and API request:

* **Scopes limit breadth.** They are the resources the connection may touch, chosen at consent. A tool without its scope returns a clear error naming the scope.
* **Role limits depth.** The connection acts as the signed-in user, with that user's role from the app: which deals they can see, whether they may withdraw a submission or accept an offer, whether `dob` is returned. Scopes can never grant more than the user's role allows.

The full scope list with descriptions is under [Authentication](/getting-started/authentication#scopes).

## Tenant isolation

A token carries exactly one `location_id`, chosen at the location step. Every read and write is filtered by it; a record from another location does not exist as far as that token is concerned (it returns `404`). On every request the server also checks that the user is still active in that location; a removed or deactivated user's tokens stop working at once, regardless of their expiry.

## Revoking access

* **Settings → Developers → Connected apps** in the app lists each consent for the location with the user, client, scopes, when it was granted and last used. **Revoke** deletes the consent and every refresh-token family for that user, client and location. Outstanding access tokens expire within the hour and cannot be refreshed.
* Clients can call `POST /oauth/revoke` with a refresh token to revoke its family.
* Removing the connector inside Claude or ChatGPT discards the client's tokens but leaves the consent in place; revoke in the app as well when offboarding.
* Deactivating the user in the app invalidates all of their tokens immediately.


## Related topics

- [Connect from ChatGPT](/mcp/connect-from-chatgpt.md)
- [MCP server](/mcp/overview.md)
- [Authentication](/getting-started/authentication.md)
