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

# PKCE Flow

> OAuth2 implementation for SPAs and mobile apps using PKCE. Secures the authorization code flow without storing client_secret.

PKCE (Proof Key for Code Exchange) secures the OAuth2 flow for applications that cannot store a `client_secret` — such as single-page apps (React, Vue, Angular), native mobile apps, and client-side JavaScript. Instead of a client secret, you use a one-time `code_verifier` and its `code_challenge` to prove you initiated the authorization request.

***

## Prerequisites

Before you implement this flow, ensure you have:

* **OAuth2 client** — [Register your app](https://app.aries.com) in Client Center / Manage Account to get `client_id` (no `client_secret` required for PKCE)
* **Redirect URI** — Must be pre-registered in Client Center / Manage Account; use the exact URL where your app handles the callback (e.g., `https://yourapp.com/oauth/callback`)
* **Runtime support** — Browser with `crypto.subtle` or Node.js 18+ / Python 3.8+ for generating `code_verifier` and `code_challenge`
* **Storage for code\_verifier** — Store the `code_verifier` between the redirect and callback (e.g., `sessionStorage` in browser, secure storage in mobile apps)

<Note>
  PKCE is recommended for all public clients (SPAs, mobile apps). Even if your backend eventually handles the token exchange, starting with PKCE improves security.
</Note>

***

## How PKCE works

1. Generate a random `code_verifier` (43–128 characters).
2. Create a `code_challenge` = Base64URL(SHA256(`code_verifier`)).
3. Include `code_challenge` and `code_challenge_method=S256` in the authorization URL.
4. Store `code_verifier` until the callback.
5. When exchanging the code for tokens, send `code_verifier` instead of `client_secret`. Aries verifies it matches the challenge.

***

## Step 1: Generate code\_verifier, code\_challenge, and redirect

Generate a cryptographically random `code_verifier`, derive the `code_challenge`, and redirect the user to Aries. Store the `code_verifier` so you can send it when exchanging the code.

**Endpoint:** `https://app.aries.com/oauth2/authorize`

**Query parameters (including PKCE):**

| Parameter               | Description                       |
| ----------------------- | --------------------------------- |
| `response_type`         | `code`                            |
| `client_id`             | Your OAuth2 client ID             |
| `redirect_uri`          | Must match a registered URI       |
| `scope`                 | Space-separated scopes            |
| `state`                 | Random string for CSRF protection |
| `code_challenge`        | Base64URL(SHA256(code\_verifier)) |
| `code_challenge_method` | `S256`                            |

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  // Generate code_verifier (43–128 chars, URL-safe)
  const codeVerifier = crypto.randomBytes(32).toString('base64url');

  // Derive code_challenge (SHA256, Base64URL)
  const codeChallenge = crypto
    .createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');

  const state = crypto.randomBytes(32).toString('hex');

  // Store code_verifier in session for callback (e.g., req.session.pkce_code_verifier = codeVerifier)
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: process.env.ARIES_CLIENT_ID,
    redirect_uri: process.env.ARIES_REDIRECT_URI,
    scope: 'account:information order:execution market:information',
    state,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
  });

  const authUrl = `https://app.aries.com/oauth2/authorize?${params}`;
  res.redirect(authUrl);
  ```

  ```python Python theme={null}
  import secrets
  import hashlib
  import base64
  from urllib.parse import urlencode

  # Generate code_verifier (43–128 chars, URL-safe)
  code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode('utf-8').rstrip('=')

  # Derive code_challenge (SHA256, Base64URL)
  digest = hashlib.sha256(code_verifier.encode()).digest()
  code_challenge = base64.urlsafe_b64encode(digest).decode('utf-8').rstrip('=')

  state = secrets.token_hex(32)

  # Store code_verifier in session for callback (e.g., session['pkce_code_verifier'] = code_verifier)
  params = urlencode({
      'response_type': 'code',
      'client_id': os.environ['ARIES_CLIENT_ID'],
      'redirect_uri': os.environ['ARIES_REDIRECT_URI'],
      'scope': 'account:information order:execution market:information',
      'state': state,
      'code_challenge': code_challenge,
      'code_challenge_method': 'S256',
  })

  auth_url = f'https://app.aries.com/oauth2/authorize?{params}'
  return redirect(auth_url)
  ```
</CodeGroup>

**Browser (SPA) example** — use `sessionStorage` to persist `code_verifier` across the redirect:

```javascript JavaScript theme={null}
async function generateCodeChallenge() {
  const codeVerifier = crypto.randomUUID() + crypto.randomUUID();
  sessionStorage.setItem('pkce_code_verifier', codeVerifier);

  const encoder = new TextEncoder();
  const data = encoder.encode(codeVerifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');

  const state = crypto.randomUUID();
  sessionStorage.setItem('oauth_state', state);

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: 'YOUR_CLIENT_ID',
    redirect_uri: window.location.origin + '/oauth/callback',
    scope: 'account:information',
    state,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
  });

  window.location.href = `https://app.aries.com/oauth2/authorize?${params}`;
}
```

***

## Step 2: Handle the callback

After the user approves, Aries redirects to your `redirect_uri` with the authorization code and `state`. Retrieve the stored `code_verifier` and exchange the code for tokens. Do not use the code twice — exchange it once and immediately.

<CodeGroup>
  ```javascript Node.js theme={null}
  app.get('/oauth/callback', async (req, res) => {
    if (req.query.state !== req.session.oauth_state) {
      return res.status(400).send('Invalid state parameter');
    }
    if (req.query.error) {
      return res.status(400).send(`Authorization error: ${req.query.error}`);
    }

    const authCode = req.query.code;
    const codeVerifier = req.session.pkce_code_verifier;

    if (!authCode || !codeVerifier) {
      return res.status(400).send('Missing code or code_verifier');
    }

    try {
      const tokens = await exchangeCodeWithPKCE(authCode, codeVerifier);
      req.session.access_token = tokens.access_token;
      req.session.refresh_token = tokens.refresh_token;
      res.redirect('/dashboard');
    } catch (err) {
      res.status(500).send('Token exchange failed');
    }
  });
  ```

  ```python Python theme={null}
  @app.route('/oauth/callback')
  def oauth_callback():
      if request.args.get('state') != session.get('oauth_state'):
          return 'Invalid state parameter', 400
      if request.args.get('error'):
          return f"Authorization error: {request.args.get('error')}", 400

      auth_code = request.args.get('code')
      code_verifier = session.get('pkce_code_verifier')

      if not auth_code or not code_verifier:
          return 'Missing code or code_verifier', 400

      try:
          tokens = exchange_code_with_pkce(auth_code, code_verifier)
          session['access_token'] = tokens['access_token']
          session['refresh_token'] = tokens['refresh_token']
          return redirect('/dashboard')
      except Exception:
          return 'Token exchange failed', 500
  ```
</CodeGroup>

**Browser (SPA) callback** — exchange happens from the client:

```javascript JavaScript theme={null}
async function handleCallback() {
  const params = new URLSearchParams(window.location.search);
  if (params.get('state') !== sessionStorage.getItem('oauth_state')) {
    throw new Error('Invalid state');
  }
  if (params.get('error')) {
    throw new Error(params.get('error_description') || params.get('error'));
  }

  const code = params.get('code');
  const codeVerifier = sessionStorage.getItem('pkce_code_verifier');
  sessionStorage.removeItem('pkce_code_verifier');
  sessionStorage.removeItem('oauth_state');

  const tokens = await exchangeCodeWithPKCE(code, codeVerifier);
  sessionStorage.setItem('access_token', tokens.access_token);
  sessionStorage.setItem('refresh_token', tokens.refresh_token);

  window.location.href = '/dashboard';
}
```

***

## Step 3: Exchange the code for tokens (with code\_verifier)

Send the authorization code and `code_verifier` to the token endpoint. Do **not** include `client_secret` — PKCE uses `code_verifier` instead.

**Endpoint:** `POST https://api.aries.com/v1/oauth2/token`

**Request body (PKCE):**

| Field           | Required | Description                                                           |
| --------------- | -------- | --------------------------------------------------------------------- |
| `client_id`     | Yes      | Your OAuth2 client ID                                                 |
| `grant_type`    | Yes      | `code`                                                                |
| `code`          | Yes      | The authorization code from the callback                              |
| `redirect_uri`  | Yes      | Must match the redirect URI used in Step 1                            |
| `code_verifier` | Yes      | The original random value (not the hash) you sent as `code_challenge` |

<CodeGroup>
  ```bash cURL theme={null}
  # Replace AUTHORIZATION_CODE and CODE_VERIFIER with values from your flow
  curl -X POST 'https://api.aries.com/v1/oauth2/token' \
    -H 'Content-Type: application/json' \
    -d '{
      "client_id": "YOUR_CLIENT_ID",
      "grant_type": "code",
      "code": "AUTHORIZATION_CODE",
      "redirect_uri": "YOUR_REDIRECT_URI",
      "code_verifier": "YOUR_CODE_VERIFIER"
    }'
  ```

  ```javascript Node.js theme={null}
  async function exchangeCodeWithPKCE(authCode, codeVerifier) {
    const response = await fetch('https://api.aries.com/v1/oauth2/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        client_id: process.env.ARIES_CLIENT_ID,
        grant_type: 'code',
        code: authCode,
        redirect_uri: process.env.ARIES_REDIRECT_URI,
        code_verifier: codeVerifier,
      }),
    });

    if (!response.ok) {
      const err = await response.json();
      throw new Error(err.error_description || `Token exchange failed: ${response.status}`);
    }

    return await response.json();
  }
  ```

  ```python Python theme={null}
  def exchange_code_with_pkce(auth_code: str, code_verifier: str):
      response = requests.post(
          'https://api.aries.com/v1/oauth2/token',
          json={
              'client_id': os.environ['ARIES_CLIENT_ID'],
              'grant_type': 'code',
              'code': auth_code,
              'redirect_uri': os.environ['ARIES_REDIRECT_URI'],
              'code_verifier': code_verifier,
          }
      )
      response.raise_for_status()
      return response.json()
  ```
</CodeGroup>

**Response** (same as Authorization Code flow):

```json theme={null}
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ...",
  "scope": "account:information order:execution market:information"
}
```

***

## Step 4: Make authenticated API requests

Use the access token in the `Authorization` header for every API request.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET 'https://api.aries.com/v1/users/me/accounts' \
    -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
  ```

  ```javascript Node.js theme={null}
  async function callApi(endpoint, accessToken) {
    const response = await fetch(`https://api.aries.com${endpoint}`, {
      headers: { 'Authorization': `Bearer ${accessToken}` },
      signal: AbortSignal.timeout(10000),
    });

    if (response.status === 401) {
      throw new Error('Token expired — refresh or re-authenticate');
    }
    if (response.status === 403) {
      throw new Error('Insufficient permissions');
    }
    if (!response.ok) throw new Error(`HTTP ${response.status}`);

    return await response.json();
  }
  ```

  ```python Python theme={null}
  def call_api(endpoint: str, access_token: str):
      response = requests.get(
          f'https://api.aries.com{endpoint}',
          headers={'Authorization': f'Bearer {access_token}'},
          timeout=10,
      )
      if response.status_code == 401:
          raise Exception('Token expired — refresh or re-authenticate')
      response.raise_for_status()
      return response.json()
  ```
</CodeGroup>

***

## Step 5: Refresh the access token

Access tokens expire after `expires_in` seconds (typically 1 hour). Use the refresh token to get a new access token. For **public clients** (SPAs, mobile apps without a backend), refresh typically requires `client_secret`. If your PKCE app has no backend, consider using a Backend-for-Frontend (BFF) to perform refresh, or prompt the user to re-authenticate when the access token expires.

**Endpoint:** `POST https://api.aries.com/v1/oauth2/token`

If you have a backend (e.g., BFF or server-side PKCE), use the same refresh format as the Authorization Code flow:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://api.aries.com/v1/oauth2/token' \
    -H 'Content-Type: application/json' \
    -d '{
      "client_id": "YOUR_CLIENT_ID",
      "client_secret": "YOUR_CLIENT_SECRET",
      "grant_type": "refresh_token",
      "refresh_token": "YOUR_REFRESH_TOKEN",
      "redirect_uri": "YOUR_REDIRECT_URI"
    }'
  ```

  ```javascript Node.js theme={null}
  async function refreshAccessToken(refreshToken) {
    const response = await fetch('https://api.aries.com/v1/oauth2/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        client_id: process.env.ARIES_CLIENT_ID,
        client_secret: process.env.ARIES_CLIENT_SECRET,
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        redirect_uri: process.env.ARIES_REDIRECT_URI,
      }),
    });

    if (!response.ok) throw new Error(`Token refresh failed: ${response.status}`);
    return await response.json();
  }
  ```

  ```python Python theme={null}
  def refresh_access_token(refresh_token):
      response = requests.post(
          'https://api.aries.com/v1/oauth2/token',
          json={
              'client_id': os.environ['ARIES_CLIENT_ID'],
              'client_secret': os.environ['ARIES_CLIENT_SECRET'],
              'grant_type': 'refresh_token',
              'refresh_token': refresh_token,
              'redirect_uri': os.environ['ARIES_REDIRECT_URI'],
          }
      )
      response.raise_for_status()
      return response.json()
  ```
</CodeGroup>

***

## Complete SPA example (React)

Minimal React example for a client-side PKCE flow:

```javascript JavaScript theme={null}
// Login: generate challenge and redirect
async function login() {
  const codeVerifier = crypto.randomUUID().replace(/-/g, '') + crypto.randomUUID().replace(/-/g, '');
  sessionStorage.setItem('pkce_code_verifier', codeVerifier);

  const encoder = new TextEncoder();
  const hash = await crypto.subtle.digest('SHA-256', encoder.encode(codeVerifier));
  const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

  const state = crypto.randomUUID();
  sessionStorage.setItem('oauth_state', state);

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: 'YOUR_CLIENT_ID',
    redirect_uri: `${window.location.origin}/callback`,
    scope: 'user:information account:information',
    state,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
  });

  window.location.href = `https://app.aries.com/oauth2/authorize?${params}`;
}

// Callback: exchange code for tokens
async function handleCallback() {
  const params = new URLSearchParams(window.location.search);
  if (params.get('state') !== sessionStorage.getItem('oauth_state')) {
    alert('Invalid state');
    return;
  }
  const code = params.get('code');
  const codeVerifier = sessionStorage.getItem('pkce_code_verifier');

  const res = await fetch('https://api.aries.com/v1/oauth2/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: 'YOUR_CLIENT_ID',
      grant_type: 'code',
      code, redirect_uri: `${window.location.origin}/callback`,
      code_verifier: codeVerifier,
    }),
  });

  const tokens = await res.json();
  sessionStorage.setItem('access_token', tokens.access_token);
  sessionStorage.removeItem('pkce_code_verifier');
  sessionStorage.removeItem('oauth_state');

  window.location.href = '/dashboard';
}
```

***

## Next steps

<CardGroup cols={2}>
  <Card title="OAuth2 Overview" icon="shield-halved" href="/api-reference/oauth2/guide">
    Scopes, security, rate limits, and troubleshooting.
  </Card>

  <Card title="Authorization Code Flow" icon="server" href="/api-reference/oauth2/auth-code-flow">
    Server-side flow with `client_secret`.
  </Card>

  <Card title="Token API Reference" icon="key" href="/api-reference/oauth2/token">
    Interactive token endpoint reference.
  </Card>

  <Card title="Quick Start" icon="bolt" href="/api-reference/quickstart">
    Get started in minutes.
  </Card>
</CardGroup>
