# Authorization Code Flow

Aerion supports the OAuth 2.0 **Authorization Code** grant with PKCE. This is the flow most third-party tools expect — the user is sent to an Aerion-hosted sign-in page, signs in to their own workspace, and your application receives a code it exchanges for tokens. It never sees the user's password, and it works with Google and Microsoft SSO.

This is the flow to use for BI tools and generic OAuth connectors — Power BI, eazyBI, and anything else that asks you for an authorization URL and a token URL. For server-to-server scripts where there is no browser and no interactive user, the older [password grant](/developer/authentication) is still available and still supported.

## Connector settings

Most tools ask for some subset of the following. Everything you need is here:

| Field | Value |
| --- | --- |
| Grant type | `authorization_code` |
| Authorization URL | `https://cloud.aerion.app/auth/login` |
| Token URL | `https://api.aerion.app/oauth2/token` |
| Refresh URL | `https://api.aerion.app/oauth2/token` (same endpoint) |
| Client ID | Issued to you by [registration](#client-ids-and-client-secrets) |
| Client secret | Not used — leave blank |
| Scope | Leave blank |
| Client authentication | None / public client (send parameters in the request body, not a `Basic` header) |
| PKCE | **Required**, `S256` only — see [below](#pkce-is-required) |
| Redirect URL | Supplied by your tool — see [registration](#client-ids-and-client-secrets) |

The authorization endpoint is on `cloud.aerion.app` and the token endpoint is on `api.aerion.app`. That split is deliberate: sign-in happens on a neutral domain so a user can reach any workspace from it. Some connector forms assume a single host for both — make sure the token URL is entered in full rather than derived from the authorization URL.

Unlike the [password grant](/developer/authentication), the token request for this flow does **not** need an `origin`, `referrer`, or `companyurl` header. The workspace is determined when the user signs in and is carried by the authorization code.

## Discovery

If your tool supports OAuth 2.0 metadata discovery, point it at `https://cloud.aerion.app` and it will configure itself — you can skip the table above entirely.

```
https://cloud.aerion.app/.well-known/oauth-authorization-server
```

```json
{
  "issuer": "https://cloud.aerion.app",
  "authorization_endpoint": "https://cloud.aerion.app/auth/login",
  "token_endpoint": "https://api.aerion.app/oauth2/token",
  "registration_endpoint": "https://cloud.aerion.app/auth/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"]
}
```

Our [MCP server](/developer/mcp) additionally publishes protected resource metadata at `https://cloud.aerion.app/.well-known/oauth-protected-resource/mcp`, which is what MCP clients use to find the authorization server. MCP clients discover and register on their own; there is nothing to fill in by hand.

## Client IDs and client secrets

**Aerion issues public clients: you get a `client_id` and there is no `client_secret`.** The proof that ties the flow together is PKCE, not a shared secret — the `code_verifier` you present at the token endpoint is what shows you are the same client that started the flow. If your connector form has a mandatory client secret field, put any non-empty placeholder in it; the value is ignored.

Registering your application is the first step. Pass the redirect URL your tool gave you, and you get back the `client_id` to configure it with:

```bash
curl -X POST 'https://cloud.aerion.app/auth/register' \
  -H 'content-type: application/json' \
  -d '{
    "client_name": "My BI Tool",
    "redirect_uris": ["https://example.com/oauth/callback"],
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "token_endpoint_auth_method": "none"
  }'
```

```json
{
  "client_id": "1cf34d6b03c21f2449bf411321318757bbba4862",
  "client_id_issued_at": 1788793566,
  "client_secret_expires_at": 0,
  "redirect_uris": ["https://example.com/oauth/callback"],
  "client_name": "My BI Tool",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

This is [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) dynamic client registration. Register once and reuse the same `client_id` for every user who connects — it identifies your integration, not the person signing in, and it is not a secret.

Register each redirect URL you will actually use. The `redirect_uri` you send at the start of a flow has to be one you registered, so if your tool has separate staging and production callbacks, include both.

The registration endpoint enforces a few rules worth knowing before you call it:

- `redirect_uris` is required, at most 10 entries, and each must be `https` (or `http` on `localhost`, `127.0.0.1`, or `[::1]` for a native app) with no URL fragment.
- `response_types`, if sent, must be exactly `["code"]`.
- `token_endpoint_auth_method`, if sent, must be `"none"`.
- `grant_types` may only contain `authorization_code` and `refresh_token`.
- `scope` is accepted and discarded. Aerion has no scopes today; a token carries the full permissions of the user who signed in.

> **`client_id` means something different here than in the password grant.** In the [password grant](/developer/authentication) it is a free-form label you choose to describe your app. In this flow it is an identifier Aerion issues you, tied to the redirect URLs you registered with it. Use the one you were given.

A management UI for these in account settings is on the way; until then, registration is over the API as above.

## The flow

### 1. Send the user to the authorization endpoint

```
https://cloud.aerion.app/auth/login
  ?response_type=code
  &client_id={your-client-id}
  &redirect_uri={your-registered-redirect-uri}
  &state={random-value-you-check-later}
  &code_challenge={base64url-sha256-of-your-verifier}
  &code_challenge_method=S256
```

`state` is optional but you should send it. `code_challenge` and `code_challenge_method` are **required** — see below.

#### PKCE is required

Any `redirect_uri` outside Aerion's own domains — which means every third-party connector — must supply `code_challenge` and `code_challenge_method`. Omitting either is rejected before the sign-in page renders:

```json
{"error":"Bad Request","status":400,"summary":"code_challenge and code_challenge_method are required when redirect_uri is outside our domains"}
```

`S256` is the only accepted method; `plain` is rejected with a similar 400. If your connector cannot generate a PKCE challenge, it cannot use this flow — use the [password grant](/developer/authentication) instead.

### 2. The user signs in

Aerion's sign-in page first asks for the user's **Aerion ID** — the subdomain they sign in at, e.g. `mycompany` for `mycompany.aerion.app`. Once entered, they are offered whichever sign-in methods that workspace has enabled: email and password, Continue with Google, or Continue with Microsoft.

> Users frequently do not know their Aerion ID off-hand. It is the first part of the URL they use to reach Aerion, and it is also in their welcome email — worth putting in your own setup instructions, since it is the step people get stuck on.

If your redirect URI is itself on an Aerion workspace subdomain, the workspace is detected from it and the user goes straight to the sign-in options. For a third-party tool, whose redirect URI is on the vendor's own domain, the user will always be asked for the Aerion ID first.

### 3. Aerion redirects back with a code

```
https://example.com/oauth/callback?code={authorization-code}&state={your-state}
```

Check that `state` matches what you sent before continuing.

### 4. Exchange the code for tokens

```bash
curl 'https://api.aerion.app/oauth2/token' \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data-raw 'grant_type=authorization_code&code={authorization-code}&redirect_uri={your-registered-redirect-uri}&client_id={your-client-id}&code_verifier={your-pkce-verifier}'
```

The response is the standard OAuth 2.0 token payload:

```json
{
  "access_token": "0001112223334445556667778899aabbccddeeff",
  "token_type": "Bearer",
  "expires_in": 3599,
  "refresh_token": "0001112223334445556667778899aabbccddeeff"
}
```

Authorization codes are single-use and short-lived. Exchange the code as soon as you receive it.

## Refreshing

Refresh tokens are issued alongside every access token and are redeemed at the same endpoint:

```bash
curl 'https://api.aerion.app/oauth2/token' \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data-raw 'grant_type=refresh_token&refresh_token={your-refresh-token}&client_id={your-client-id}'
```

## Using the access token

Identical to every other Aerion token — a bearer token on the `authorization` header, with no tenant header required:

```bash
curl 'https://api.aerion.app/v1/users/me' \
  -H 'authorization: Bearer {your-access-token}'
```

Tokens carry the permissions of the user who signed in, so a connector only ever sees what that person can see. For scheduled reports and other unattended integrations, we recommend connecting a dedicated utility user scoped to the access the integration actually needs, rather than an individual's account.

## Errors

Token endpoint failures return a JSON body with the reason in `raw`:

```json
{"error":"E_FORBIDDEN","status":403,"summary":"Forbidden","raw":"Invalid grant: authorization code is invalid"}
```

| `raw` | Cause |
| --- | --- |
| `Invalid client: cannot retrieve client credentials` | No `client_id` on the request. |
| `Invalid grant: authorization code is invalid` | The code is expired, already used, or the `code_verifier` does not match the `code_challenge` sent at the start of the flow. |
| `Missing parameter: 'code'` | The `code` parameter was not sent. |
| `Invalid client data` | The `grant_type` is not one Aerion supports. |
