# Authentication and scopes

> How a Hekana app authenticates: the OAuth authorization-code flow with PKCE, access and refresh token lifetimes, secret rotation and what each scope allows.

Source: https://hekana.dev/apps/authentication

Apps authenticate with OAuth 2.0 authorization code plus PKCE. The result is a Bearer token tied to one installation of your app on one store.

## The flow

1. Your app sends the store owner to the dashboard page `/oauth/authorize` with `client_id`, `redirect_uri`, `scope`, `state`, `code_challenge` and `code_challenge_method=S256`.
2. The owner approves. Hekana records the installation with exactly the scopes requested, and redirects to your `redirect_uri` with a single-use `code` (valid 60 seconds) and your `state`. If the owner denies, you receive `error=access_denied`.
3. Your server calls `POST /oauth/token` with the code and your PKCE verifier.
4. You call `/apps-api/*` with `Authorization: Bearer <access token>`.

Checks the authorize step makes: the app is approved, `redirect_uri` is one of the registered addresses (exact match), the challenge method is `S256`, and every requested scope was registered with the app. Only a store owner or admin can approve.

## Tokens

| | Lifetime | Notes |
| - | - | - |
| Authorization code | 60 seconds | Single use. A second exchange fails. |
| Access token | 1 hour | Opaque string. Sent as a Bearer token. |
| Refresh token | Rotates on every use; the chain ends 30 days after the first grant | Reusing an already-rotated refresh token revokes the whole chain. |

Refresh with `POST /oauth/token`:

```json title="POST /oauth/token (refresh)"
{
  "grant_type": "refresh_token",
  "refresh_token": "<the latest refresh token>",
  "client_id": "<your client id>",
  "client_secret": "<your client secret>"
}
```

Save the new refresh token from every reply and discard the old one. After 30 days the owner must approve again. Confidential apps must send `client_secret`; public apps rely on PKCE only. Wrong or missing credentials answer `401 invalid_client`; a bad code, verifier or refresh token answers `400 invalid_grant`.

A refreshed token carries only the scopes the store currently grants, so a scope you ask for later does nothing until the owner approves it again.

## When tokens stop working

Calls answer `401 invalid_token` when the token is expired, revoked or unknown, when the store uninstalled, and when the app is not `approved` any more. Suspending an app cuts off every installation at once, and refresh stops too.

> **Changing scopes or redirect URIs on an approved app sends it back to review**, and while it is in review its tokens do not work. Renaming the app or editing its listing does not. Plan scope changes as a new version and expect a pause.

## Secrets

`POST /oauth/apps/:id/rotate-secret` returns a new client secret once and invalidates the old one immediately. Tokens already issued keep working until they expire. Public apps have no secret to rotate.

## Scopes

Register only what you use. Each scope maps to endpoints:

| Scope | Allows |
| - | - |
| `read_orders` | `GET /orders`; webhook topics `orders/*` |
| `write_orders` | `POST /orders` |
| `read_products` | `GET /products`, `GET /products/:id`; webhook topics `products/*` |
| `write_products` | Accepted at registration. **No endpoint uses it yet.** |
| `read_customers` | `GET /customers`, `GET /customers/:id`; webhook topic `customers/created` |
| `read_analytics` | `GET /analytics` |
| `write_discounts` | `POST /discounts` |
| `app_data` | `/data` key-value store for the installation |
| `send_messages` | `POST /messages` |
| `checkout_extensions` | `/checkout-extensions` |
| `billing` | `POST /charge`, `GET /earnings`. Spends the store's wallet; the highest-trust scope. |

A call without the scope answers `403` with `insufficient_scope: <scope>`. The webhook endpoints need no scope, but each topic needs the scope in the table above, and `app/uninstalled` needs none.

## Embedded apps

An app can also be opened inside the dashboard in a frame. The dashboard passes the app a session token that lives 120 seconds and works once; the app's server redeems it with `POST /oauth/session-token/verify` (`client_id`, `client_secret`, `session_token`) and receives the store and the granted scopes. The address that is framed cannot be set through the developer API, so this is not self-serve yet.
