> ## 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.