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

# Security Best Practices

> The non-negotiables: PKCE, state, redirect URIs, and refresh rotation

OAuth is only as secure as the weakest link in your implementation. This page enumerates the practices Flexslot **requires** and the ones we strongly recommend. Most of these come straight from [RFC 9700](https://www.rfc-editor.org/rfc/rfc9700) (BCP 240, the OAuth 2.0 Security Best Current Practice). Treat them as MUSTs.

## The non-negotiables

<CardGroup cols={2}>
  <Card title="PKCE S256 always" icon="lock">
    Every flow, every client type. `plain` is forbidden.
  </Card>

  <Card title="Exact redirect URI match" icon="bullseye">
    No wildcards. No prefix matching. Byte-for-byte.
  </Card>

  <Card title="state on every request" icon="shield-halved">
    Random, single-use, bound to the user's session.
  </Card>

  <Card title="Validate iss on callback" icon="signature">
    [RFC 9207](https://www.rfc-editor.org/rfc/rfc9207). Reject if missing or wrong.
  </Card>

  <Card title="Refresh token rotation" icon="rotate">
    Store the new token, drop the old.
  </Card>

  <Card title="HTTPS everywhere" icon="lock-keyhole">
    No exceptions for production.
  </Card>
</CardGroup>

## PKCE (Proof Key for Code Exchange)

**What it does:** binds the authorization code to your client, so an intercepted code is useless to anyone else.

**Required at Flexslot for:**

* Every client type — public and confidential.
* Every authorization code request.

**Required method:** `S256`. `plain` is rejected.

```
code_challenge = BASE64URL(SHA256(code_verifier))
```

<Steps>
  <Step title="Generate a 32-byte random code_verifier">
    Base64url-encoded, no padding. 43 characters.
  </Step>

  <Step title="Hash to produce code_challenge">
    `SHA256(verifier)`, base64url-encoded, no padding.
  </Step>

  <Step title="Send the challenge on /authorize">
    With `code_challenge_method=S256`.
  </Step>

  <Step title="Send the verifier on /token">
    Flexslot recomputes the hash and rejects if it doesn't match.
  </Step>
</Steps>

See [Authorization Code Flow](/oauth/authorization-code-flow#generating-pkce-values) for working code in Node, Python, and bash.

<Warning>
  PKCE downgrade defense: if you registered with `code_challenge_method=S256`, sending `plain` later is rejected. Flexslot also rejects token requests that include `code_verifier` if the original authorization request had no `code_challenge`.
</Warning>

## redirect\_uri matching

Flexslot performs **exact string match** on the `redirect_uri` parameter against the URIs registered for your client. There is **no** wildcard support, no path-prefix tolerance, and no port-flexibility (except `127.0.0.1` and `[::1]` for native apps' loopback redirects).

| Registered URI                           | Request URI                                    | Match?                          |
| ---------------------------------------- | ---------------------------------------------- | ------------------------------- |
| `https://app.example.com/oauth/callback` | `https://app.example.com/oauth/callback`       | Yes                             |
| `https://app.example.com/oauth/callback` | `https://app.example.com/oauth/callback/`      | **No** (trailing slash differs) |
| `https://app.example.com/oauth/callback` | `https://app.example.com/oauth/callback?foo=1` | **No** (query string differs)   |
| `https://app.example.com/oauth/callback` | `https://APP.example.com/oauth/callback`       | **No** (case differs)           |
| `https://app.example.com/oauth/callback` | `http://app.example.com/oauth/callback`        | **No** (scheme differs)         |

### Recommended practice

* **One URI per environment.** Register `https://dev.app.example.com/oauth/callback` AND `https://app.example.com/oauth/callback`. Don't try to use one with a wildcard.
* **Avoid query strings.** Keep state in a session, not in the URI.
* **Use HTTPS.** `http://` is allowed only for `localhost`, `127.0.0.1`, and `[::1]` (for local development and native apps).

## Client lifecycle

How you manage the client itself matters as much as how you run the flow. See [Managing OAuth Clients](/oauth/client-management) for the full lifecycle.

* **Client secrets are shown once.** A confidential client's secret is displayed only at creation and rotation. Store it in a secret manager immediately; there is no retrieval endpoint. If it leaks, rotate — rotation invalidates the previous secret instantly.
* **Public clients carry no secret.** They authenticate by PKCE alone. Never embed a secret in a CLI, native app, or SPA; create a public client instead.
* **Revocation cascades.** Revoking a client also revokes every grant and every access and refresh token issued under it. Use this as your kill switch if a client is compromised — a single revoke immediately stops all tokens that client minted, not just future authorizations.
* **Keep your client roster small.** Each partner is capped at 10 active clients. Revoke clients you no longer use rather than letting stale credentials accumulate — every live secret is attack surface.

## state parameter

The `state` parameter is a CSRF token: it proves that the callback corresponds to a flow your application initiated.

### Requirements

* Cryptographically random — at least 128 bits of entropy.
* Bound to the user agent's session (a cookie, server-side session storage).
* Single-use — invalidate after the callback consumes it.
* Validated on callback — strict equality compared to the stored value.

<Tip>
  Don't try to encode application data into `state` (return URL, intent, anything). Use a random opaque token, and store any data you need server-side keyed by that token. This keeps the CSRF surface minimal and avoids accidental information disclosure.
</Tip>

### What happens if you skip it

PKCE protects against code interception, but `state` is the universally-compatible defense against the simpler attack of "trick the user's browser into hitting your callback with an attacker-controlled code". Without `state`, an attacker who can convince the user to visit a URL can bind the user's session to the attacker's Flexslot account.

## Issuer identification (RFC 9207)

Every callback from Flexslot includes an `iss` query parameter:

```
GET /oauth/callback?code=…&state=…&iss=https://api.flexslot.gg
```

**You must verify `iss == "https://api.flexslot.gg"`.** If it's missing or different, abort the flow — that's a [mix-up attack](https://www.rfc-editor.org/rfc/rfc9207#section-1) signal.

The mix-up attack: a malicious or compromised authorization server tricks your client into sending a code intended for AS A to AS B. The `iss` parameter cryptographically labels which AS issued the code so your client can tell.

## Refresh token rotation

Every successful `grant_type=refresh_token` exchange:

1. Returns a **new** refresh token in the response.
2. **Invalidates** the refresh token you just used.

If you replay an old refresh token, Flexslot treats that as a theft signal and **revokes the entire grant**. The user has to consent again.

### What this means for your code

<Steps>
  <Step title="Call /token with the current refresh_token">
    Get back `access_token` + new `refresh_token`.
  </Step>

  <Step title="Store the new refresh_token immediately">
    Atomically replace the old one in your database. The old one is dead.
  </Step>

  <Step title="If the response is invalid_grant">
    Treat the user as logged out. Their next action will need a fresh authorization flow.
  </Step>
</Steps>

<Warning>
  **Race conditions are real.** If two parallel requests both refresh, one of them will lose. Serialize refresh calls per-user (a mutex, a row-level lock, or an idempotency key) and have the loser re-read the now-stored token instead of erroring.
</Warning>

## Token storage

| Where                             | Acceptable for                                       |
| --------------------------------- | ---------------------------------------------------- |
| Encrypted database column         | Server-side apps — recommended                       |
| Encrypted Redis                   | Server-side apps — recommended                       |
| HTTP-only cookie                  | Refresh token for web apps using a BFF pattern       |
| In-memory variable                | SPA access tokens (no JavaScript-accessible storage) |
| `localStorage` / `sessionStorage` | **Never** for tokens — XSS reads it all              |
| URL query string                  | **Never**. RFC 9700 forbids tokens in URIs.          |
| Server logs                       | **Never**. Mask tokens before logging.               |

## TLS

* All Flexslot endpoints require TLS 1.2 or higher.
* Loopback (`http://127.0.0.1`) is allowed for development and native apps.
* We send `Cache-Control: no-store` on every token response — your client should not cache them.

## CSRF on the token endpoint

The token endpoint is `POST`-only and requires either:

* HTTP Basic auth with the client credentials (for confidential clients), or
* `client_id` in the form body **plus** the PKCE `code_verifier` (for public clients).

Browsers can't forge either of these from a third-party origin without an XSS or stolen credentials, so the token endpoint is not a CSRF target in the same way `/authorize` is. CSRF protection on `/authorize` is the job of the `state` parameter.

## Logging out / revocation

When a user logs out of your app:

1. **Call `/revoke`** with their refresh token. This invalidates both the refresh token and any active access tokens for the grant.
2. **Clear local state** — remove tokens from your store.

```bash theme={null}
curl -X POST https://api.flexslot.gg/api/public/v1/oauth/revoke \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d "token=$REFRESH_TOKEN" \
  -d "token_type_hint=refresh_token"
```

A successful revocation returns `200` with an empty body ([RFC 7009](https://www.rfc-editor.org/rfc/rfc7009)).

## When tokens leak

If a token is logged, exposed in an error page, or shows up in a screenshot — treat it as compromised and **revoke it immediately**, then rotate any related secrets. The 1-hour access token expiry limits blast radius, but a leaked refresh token is good for 30 days unless revoked.

## Sender-constrained tokens (DPoP / mTLS)

Bearer tokens are exactly that — whoever holds one can use it. For high-value integrations, consider sender-constraining with DPoP ([RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)) so that a stolen token can't be replayed by an attacker who doesn't have your client's private key. See [DPoP](/oauth/dpop).

## Threats this defends against

| Threat                                      | Defense                              | Where                                                              |
| ------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------ |
| Authorization code interception             | PKCE                                 | [authorization-code-flow](/oauth/authorization-code-flow#why-pkce) |
| CSRF on callback                            | `state`                              | This page                                                          |
| Mix-up attack                               | `iss` validation                     | This page                                                          |
| Open redirector via wildcard `redirect_uri` | Exact URI match                      | This page                                                          |
| Refresh token theft                         | Rotation + revoke on replay          | This page                                                          |
| XSS exfiltrating tokens                     | In-memory storage + httpOnly cookies | This page                                                          |
| Token replay across services                | DPoP / mTLS sender constraint        | [DPoP](/oauth/dpop)                                                |

## Further reading

* [RFC 9700](https://www.rfc-editor.org/rfc/rfc9700) — The complete OAuth 2.0 Security BCP. Every implementer should read this once.
* [RFC 9207](https://www.rfc-editor.org/rfc/rfc9207) — Issuer Identification for mix-up defense.
* [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636) — The PKCE spec.
* [OWASP OAuth2 Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/OAuth2_Cheat_Sheet.html) — Threat-to-mitigation mapping.
