# Authentication

> The OAuth 2.0 Authorization Code flow — register a client, walk a user through consent, exchange a code for tokens, and keep a long-lived connection alive with refresh tokens.

Source: https://www.accountfun.com/docs/api/authentication/

AccountFUN’s public API uses the **OAuth 2.0 Authorization Code grant** (RFC 6749 §4.1, hardened per RFC 9700). This is the standard three-legged flow used by server-side applications that act on behalf of a user.

The flow has three legs:

1. **Authorize** — redirect the user to AccountFUN, where they sign in and approve your app.
2. **Token exchange** — trade the short-lived authorization code for an access token (and optional refresh token) at your server.
3. **Resource calls** — send the access token with every API request.

## Endpoints

| Endpoint                  | Method        | Purpose                                                                                            |
| ------------------------- | ------------- | -------------------------------------------------------------------------------------------------- |
| `/api/v1/oauth/authorize` | `GET`, `POST` | Authorization-code grant entry. `GET` renders the consent screen; `POST` is the Allow/Deny action. |
| `/api/v1/oauth/token`     | `POST`        | Exchange a code for tokens, or rotate a refresh token.                                             |
| `/api/v1/oauth/revoke`    | `POST`        | Revoke an access or refresh token (RFC 7009).                                                      |

All token-endpoint requests must use `Content-Type: application/x-www-form-urlencoded`.

Clients that discover endpoints automatically can read the authorization server metadata (RFC 8414) at `/.well-known/oauth-authorization-server`. This is how [AI assistants](https://www.accountfun.com/docs/api/mcp/) find them. If an authorization request leaves out `scope`, the client’s registered scopes are requested.

## Step 1 — Redirect the user to authorize

Direct the user’s browser to the authorize endpoint with your `client_id`, the `redirect_uri` you registered, the `scope` you need, and a random `state` value:

```text
GET https://app.accountfun.com/api/v1/oauth/authorize \
  ?response_type=code \
  &client_id=YOUR_CLIENT_ID \
  &redirect_uri=https://your-app.example.com/callback \
  &scope=invoices:read%20invoices:write%20offline_access \
  &state=RANDOM_UNGUESSABLE_STRING
```

| Parameter               | Required | Description                                                                                                                                                                                                       |
| ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `response_type`         | Yes      | Must be `code`.                                                                                                                                                                                                   |
| `client_id`             | Yes      | Your client identifier.                                                                                                                                                                                           |
| `redirect_uri`          | Yes      | Must **exactly match** a URI registered on your client. HTTPS is required in production (`http://localhost` is allowed for development).                                                                          |
| `scope`                 | No       | Space-delimited list of scopes. Scopes your client isn’t registered for are ignored; if none of the requested scopes is registered, the request fails. Omit it to request all of your client’s registered scopes. |
| `state`                 | Yes      | An unguessable value you generate. It is echoed back in the redirect and **must** be verified to prevent CSRF.                                                                                                    |
| `code_challenge`        | **Yes**  | PKCE challenge — `base64url(SHA-256(code_verifier))`, 43 characters (see [PKCE](#pkce-required)).                                                                                                                 |
| `code_challenge_method` | **Yes**  | Must be `S256`. `plain` is not accepted.                                                                                                                                                                          |

state is mandatory

The `state` parameter is **required**. AccountFUN rejects authorize requests that omit it, and your app must confirm the value returned in the redirect matches the one it sent before doing anything else.

### What the user sees

The user signs in to AccountFUN (if they aren’t already), selects which of their organizations to connect, and reviews the requested scopes on a consent screen. They click **Allow** or **Deny**.

* **Approving is opt-in.** `bills:approve` and `invoices:approve` are listed under **Only if you tick it:** as unticked checkboxes. If the user doesn’t tick one, it’s left out of the grant.
* **The user’s role sets the limit.** The list follows the selected organization: access the user’s role there can’t grant is shown as “(not available with your role in this organization)” and left out of the grant, and the connection works with the rest. If the role can grant none of the requested access, the connection is refused.
* **Only the creator’s organizations.** The picker offers only organizations that the person who registered the app (under **Settings → API & Developers**) belongs to. Members of other organizations see ”… was set up in another organization, so it can’t be connected here” and should ask their own admin to register the app.
* The screen tells the user how to take the access back later: they can disconnect the app under **Settings → Account → Connected Apps**, and an admin of the organization can revoke it under **Settings → Authorized Applications**.

The `scope` in the [token response](#success-response) lists exactly what was granted, which can be less than you asked for.

### Request errors

If something is wrong with the request — `response_type` isn’t `code`, `state` or a valid PKCE `code_challenge` is missing, the `redirect_uri` doesn’t match a registered URI, or none of the requested scopes is registered on your client — AccountFUN shows an error page and does **not** redirect back to your app. The browser is sent to your `redirect_uri` only after the user clicks **Allow** or **Deny**, and never to an unregistered URI.

Requested scopes that your client isn’t registered for are ignored, not treated as an error.

### User denies

Clicking **Deny** is the only case that redirects back to you with an error. You receive:

```text
GET https://your-app.example.com/callback?error=access_denied&state=RANDOM_UNGUESSABLE_STRING
```

Handle this gracefully — show the user a “try again later” message.

## Step 2 — Receive the authorization code

On **Allow**, AccountFUN redirects to your `redirect_uri` with a single-use `code`:

```text
GET https://your-app.example.com/callback?code=AUTHORIZATION_CODE&state=RANDOM_UNGUESSABLE_STRING
```

Before exchanging the code, **verify that `state` matches** what you sent in step 1. If it doesn’t, discard the request.

The authorization code is:

* **Single-use** — it can be exchanged exactly once.
* **Short-lived** — it expires after **60 seconds**.
* **Bound to your `client_id` and `redirect_uri`** — both must match at exchange time.

Exchange it immediately. Do not store it.

## Step 3 — Exchange the code for tokens

From your server, POST to the token endpoint:

```bash
curl -X POST https://app.accountfun.com/api/v1/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d code=AUTHORIZATION_CODE \
  -d redirect_uri=https://your-app.example.com/callback \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET
```

You may authenticate the client with HTTP Basic instead of the form body (RFC 6749 §2.3.1), but you **must not** mix both methods in one request:

```text
Authorization: Basic base64(YOUR_CLIENT_ID:YOUR_CLIENT_SECRET)
```

### Success response

```json
{
  "access_token": "ab12cd34...64-hex-chars",
  "token_type": "Bearer",
  "expires_in": 1800,
  "scope": "invoices:read invoices:write offline_access",
  "organization_id": "00000000-0000-0000-0000-000000000000",
  "refresh_token": "ef34ab12...64-hex-chars"
}
```

| Field             | Description                                                                                                                                                                                                                                               |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `access_token`    | The opaque bearer token. Send it in the `Authorization` header on every API call.                                                                                                                                                                         |
| `token_type`      | Always `Bearer`.                                                                                                                                                                                                                                          |
| `expires_in`      | Seconds until the access token expires (1800 = 30 minutes).                                                                                                                                                                                               |
| `scope`           | The scopes granted to this token (space-delimited). This is exactly what the user granted, which can be less than you requested: an approve scope they didn’t tick, or access their role can’t grant. Check it before using a feature that needs a scope. |
| `organization_id` | The organization the user selected at consent. All API calls are scoped here.                                                                                                                                                                             |
| `refresh_token`   | Present only if `offline_access` was granted. Use it to mint new token pairs without re-prompting the user.                                                                                                                                               |

Store tokens securely

Treat access and refresh tokens as credentials. Store them server-side, encrypted at rest. **Never** put them in browser `localStorage`, a URL fragment, or client-side state. A leaked refresh token gives an attacker long-lived access until the user revokes your app.

## Calling the API

Once you have an access token, call any resource endpoint:

```bash
curl https://app.accountfun.com/api/v1/invoices \
  -H "Authorization: Bearer ab12cd34...64-hex-chars"
```

Every request is automatically scoped to the `organization_id` the token was issued for. You cannot read or write data in any other organization with that token — the boundary is enforced server-side on every query.

## Refreshing tokens

Access tokens expire after 30 minutes. When the `offline_access` scope was granted, you also received a refresh token. Use it to mint a new access + refresh pair **before** the old access token expires:

```bash
curl -X POST https://app.accountfun.com/api/v1/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=refresh_token \
  -d refresh_token=CURRENT_REFRESH_TOKEN \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET
```

The response has the same shape as the authorization-code response. The new refresh token **replaces** the old one — keep only the latest.

### Refresh-token rotation

AccountFUN rotates the refresh token on every use:

* The presented refresh token is **revoked** immediately.
* A **new** refresh token is returned, chained to the old one.
* Each successful rotation resets the **30-day inactivity window**. There is **no absolute lifetime cap**; a refresh token expires after 30 days without use.

### Reuse detection

If an already-rotated refresh token is presented again, AccountFUN treats this as a theft signal and **revokes that token’s family** — the rotation chain it belongs to and every access token minted along it. The next call with any refresh token from that chain returns `invalid_grant`; resource requests using an access token from it return `401 invalid_token`.

A family is one authorization: the tokens issued when a code was exchanged, plus everything rotated from them. If the same user has authorized your app more than once (for example from two devices), each authorization is its own family, and a reuse in one leaves the others working. The consent itself (the grant) is not revoked either — the user does not have to consent again, your app only has to run the authorization flow again to get a new family.

This means: **always discard the old refresh token the moment you’ve used it.** If two parts of your system race and both try to rotate the same token, one will succeed and the other will trigger family invalidation. To avoid this, serialize refreshes per-session in your app.

If refresh fails with invalid\_grant

When a refresh returns `invalid_grant`, that family is dead. The cleanest recovery is to send the user through the authorization flow again. This happens naturally after 30 days without a refresh too.

### Refresh-token lifetimes

| Property | Value                                              |
| -------- | -------------------------------------------------- |
| Lifetime | Rolling 30 days of inactivity; no absolute cap     |
| Rotation | On every use (old token revoked, new token issued) |

## PKCE (required)

**PKCE** (Proof Key for Code Exchange, RFC 7636) binds the authorization code to the client instance that started the flow, so a leaked or injected code is useless to anyone else. AccountFUN **requires PKCE with the `S256` method on every authorization request**, in line with the OAuth 2.0 Security Best Current Practice (RFC 9700). This applies even though AccountFUN clients are confidential (they authenticate with a secret).

1. Generate a high-entropy `code_verifier`: 43–128 characters from `A–Z a–z 0–9 - . _ ~`.
2. Derive the `code_challenge`: `base64url(SHA-256(code_verifier))` with no padding (always 43 characters).
3. Send `code_challenge` + `code_challenge_method=S256` in the **authorize** request.
4. Send the original `code_verifier` in the **token** exchange.

```text
# Authorize
&code_challenge=BASE64URL_SHA256_OF_VERIFIER
&code_challenge_method=S256


# Token exchange
&code_verifier=ORIGINAL_PLAIN_VERIFIER
```

An authorize request without a valid `S256` challenge is rejected with an AccountFUN error page; the user isn’t sent back to your app. The token endpoint verifies the `code_verifier` against the stored challenge; a missing or mismatched verifier returns `invalid_grant`.

## Revoking tokens

To revoke an access or refresh token (e.g. when a user disconnects your app), call the revocation endpoint (RFC 7009):

```bash
curl -X POST https://app.accountfun.com/api/v1/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d token=TOKEN_TO_REVOKE \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET
```

| Parameter         | Required | Description                                                                |
| ----------------- | -------- | -------------------------------------------------------------------------- |
| `token`           | No       | The access or refresh token to revoke.                                     |
| `token_type_hint` | No       | `access_token` or `refresh_token` (a hint only — both tables are checked). |
| `client_id`       | Yes      | Your client identifier.                                                    |
| `client_secret`   | Yes      | Your client secret.                                                        |

The endpoint **always returns `200 OK`**, even if the token was unknown, already revoked, or belonged to a different client — this prevents leaking token validity. A `401` with `{ "error": "invalid_client" }` is returned only when your own client credentials are wrong, and a `429` (with `Retry-After`) when you exceed the [rate limits](https://www.accountfun.com/docs/api/errors-and-limits/) — the same bodies the token endpoint sends.

You may revoke only tokens that belong to **your** client.

### Revoking from the user side

Inside AccountFUN, a user can disconnect your app at any time under **Settings → Account → Connected Apps**, and an admin of the organization can revoke any member’s connection under **Settings → Authorized Applications**; the consent screen tells users both. Either one immediately invalidates the consent grant and all derived access and refresh tokens — the next API call returns `401 invalid_token`.

AccountFUN also revokes every connection a user has made when their account may have been at risk: a password reset or change, two-factor authentication turned off with a recovery code or reset by AccountFUN support, and “sign out other sessions”. Your refresh then fails with `invalid_grant`: send the user through authorization again.

## The role ceiling

A token’s authority is the **intersection** of two things:

1. **The scopes** the token carries (what the user consented to).
2. **The user’s live role** in the organization (re-checked on every request).

A token minted when the user was an admin becomes inert the instant the user is demoted or removed from the org — the token’s scope string hasn’t changed, but the live role no longer permits the action. This is enforced on every single API request; there is no staleness window.

Every resource scope maps to a permission the authorizing user’s role must actually hold — whatever the role is called. With [custom roles](https://www.accountfun.com/docs/guides/roles/), that means the specifics of the role, not its name, decide what a token can do: a custom role with the right permissions can use the API even though it isn’t a seeded role, and a role without them cannot use the API at all.

The same check runs at consent: access the user’s role can’t grant in the selected organization is left out of the grant. `bills:approve` and `invoices:approve` need the **Approve Documents** permission, and each approves only its own document type — `invoices:approve` can’t approve a bill.

## Client authentication methods

The token and revoke endpoints accept client credentials in two ways:

| Method     | How                                                     |
| ---------- | ------------------------------------------------------- |
| Form body  | `client_id` + `client_secret` in the URL-encoded body.  |
| HTTP Basic | `Authorization: Basic base64(client_id:client_secret)`. |

Pick one per request. Mixing both is rejected.

## Security checklist

When building your integration:

* **Store the `client_secret` on your server only.** Never ship it in a mobile app, single-page app, or any code that runs in the user’s browser.
* **Verify `state` on every callback.** This is your CSRF defense.
* **Read the granted `scope`** in the token response. Don’t assume the user granted everything you asked for — approving, for one, is opt-in.
* **Store tokens encrypted at rest.** Rotate your storage key periodically.
* **Discard old refresh tokens immediately after rotation.** Never keep a stash — reuse triggers family invalidation.
* **Use HTTPS everywhere.** Redirect URIs must be HTTPS in production.
* **Handle `401 invalid_token` gracefully** by re-prompting authorization.
* **Never put tokens in URLs** (query strings, fragments). Use the `Authorization` header.
