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

# Token Handling

> How to validate, store, rotate, and revoke Busha OAuth2 tokens.

## Token types at a glance

| Token           | Format        | Lifetime         | Notes                                                                       |
| --------------- | ------------- | ---------------- | --------------------------------------------------------------------------- |
| `access_token`  | JWT (RS256)   | \~1 hour         | Validated locally. Cannot be revoked mid-flight — it self-expires at `exp`. |
| `refresh_token` | Opaque string | 30 days, sliding | Single-use. Rotates on every refresh. Replay revokes the entire family.     |
| `id_token`      | JWT (RS256)   | \~1 hour         | Identity proof only. Do not use as a bearer token on API calls.             |

## Validating access tokens

Access tokens are JWTs signed with RS256. Validate them locally on every request — no per-request introspection round-trip to Busha.

<Steps>
  <Step title="Fetch and cache the JWKS">
    Fetch public keys from `<issuer>/.well-known/jwks.json`. Cache for \~5 minutes. Refetch when you encounter a `kid` (key ID) that is not in your cache.

    ```bash theme={null}
    curl https://login.busha.io/.well-known/jwks.json
    ```

    Read the canonical `issuer` string from `/.well-known/openid-configuration` at startup and pin against it — do not hardcode.
  </Step>

  <Step title="Match the key and verify the signature">
    Match the JWT header's `kid` to a key in the JWKS. Verify the RS256 signature with that key.

    <Warning>
      Never decode an unverified JWT and trust its claims. A forged token looks identical until you check the cryptographic signature.
    </Warning>
  </Step>

  <Step title="Verify the standard claims">
    | Claim       | Check                                                        |
    | ----------- | ------------------------------------------------------------ |
    | `iss`       | Must match the canonical issuer from the discovery document. |
    | `exp`       | Must be in the future. Allow \~30 seconds of clock skew.     |
    | `aud`       | Must include your `client_id` (if present).                  |
    | `client_id` | Must match your registered `client_id`.                      |
  </Step>

  <Step title="Enforce scopes on every endpoint">
    Check the `scp` claim (a space-separated string or array depending on your library) against the scope required by the endpoint being called. A token with `scp="balances:read"` cannot call a transfers endpoint.
  </Step>
</Steps>

## Refreshing tokens

Every successful refresh issues a new `(access_token, refresh_token)` pair and immediately invalidates the old refresh token.

```bash theme={null}
curl -X POST https://login.busha.io/oauth2/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "refresh_token=$REFRESH_TOKEN"
```

**Rotation rules:**

* Persist the new pair **atomically before** discarding the old one. If persistence fails, you lose the session.
* If a previously-rotated refresh token is replayed, Busha revokes the **entire token family** immediately.
* Any `invalid_grant` response on a refresh is a hard disconnect — prompt the user to re-authorize.
* The 30-day lifetime is **sliding**: each successful refresh resets the clock by another 30 days.

## Revoking tokens

When a user disconnects your integration, revoke the refresh token:

```bash theme={null}
curl -X POST https://login.busha.io/oauth2/revoke \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "token=$REFRESH_TOKEN" \
  --data-urlencode "token_type_hint=refresh_token"
```

The endpoint returns `200` with an empty body whether or not the token was valid — this is intentional and prevents leaking token validity.

<Note>
  Revoking a refresh token prevents future refreshes immediately, but does not invalidate a currently-issued access token. The access token continues to validate until its `exp` (\~1 hour). For instant revocation on critical paths, consider maintaining a server-side allowlist that your backend checks.
</Note>

## Storage requirements

| Token           | Storage requirement                                                                                                                                    |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `access_token`  | In-memory or short-lived server-side cache. Never in browser localStorage or cookies.                                                                  |
| `refresh_token` | Encrypted at rest, with row-level encryption keyed per user. Never in browser cookies or localStorage.                                                 |
| `client_secret` | Secret manager (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager). Never in source code or environment variables committed to version control. |

## What never to log

Never write these values to application logs, request logs, or error trackers:

* `client_secret`
* `access_token`
* `refresh_token`
* `code_verifier`
* Authorization `code`
* `id_token`

**Safe to log:** `client_id`, `jti`, `sub`, scope list, request IDs, trace IDs.
