# /api/auth/exchange (dashboard)

Source-derived dashboard endpoint contract, authentication, request validation, responses, and errors.

Source: https://docs.minds.sh/docs/api/platform/reference/dashboard--api-auth-exchange



This is the **dashboard** service route. Its origin and authentication are described in [Platform API overview](/docs/api/platform) and [Authentication](/docs/api/platform/authentication). A path shared by another service is a different endpoint.

| Property                       | Contract                                                                       |
| ------------------------------ | ------------------------------------------------------------------------------ |
| Methods                        | POST                                                                           |
| Path                           | `/api/auth/exchange`                                                           |
| Path parameters                | None                                                                           |
| Query parameters read in route | None read directly; schema validation below may define additional fields       |
| Headers read in route          | No additional direct header reads; authentication helpers may read credentials |

## Authentication and access [#authentication-and-access]

OAuth browser flow using HTTP-only l1fe\_\* cookies. Exchange/refresh/logout have endpoint-specific inputs and rate limits.

## Request and response definitions [#request-and-response-definitions]

The following named definitions are imported or declared by this route. Optional markers, defaults, bounds, and enum values are shown exactly as implemented. A response type is a source contract, not an example populated with real account data.

### ExchangeRequest [#exchangerequest]

```typescript
interface ExchangeRequest {
  code: string;
  code_verifier: string;
  redirect_uri: string;
}
```

Source: `minds-ui/apps/app/app/api/auth/exchange/route.ts:18`.

## Method behavior [#method-behavior]

The handler excerpt preserves field validation, response envelopes, cookie changes, and exception branches. Values returned by service helpers retain their named response type above; for passthrough routes, the upstream response is authoritative.

### POST [#post]

```typescript
export async function POST(request: NextRequest): Promise<NextResponse> {
  // Rate limit: 10 exchange attempts per minute per IP
  const clientIp = getClientIp(request);
  if (!checkRateLimit(clientIp, 'exchange')) {
    return NextResponse.json(
      { error: 'rate_limited', error_description: 'Too many requests. Please try again later.' },
      { status: 429 }
    );
  }

  let body: ExchangeRequest;
  try {
    body = await request.json();
  } catch {
    return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
  }

  const { code, code_verifier, redirect_uri } = body;

  if (!code || !code_verifier || !redirect_uri) {
    return NextResponse.json(
      { error: 'Missing required fields: code, code_verifier, redirect_uri' },
      { status: 400 }
    );
  }

  // Exchange authorization code for tokens (server-to-server, no CORS)
  const tokenUrl = `${SSO_URL}/v4/oauth/token`;
  const params: Record<string, string> = {
    grant_type: 'authorization_code',
    client_id: CLIENT_ID,
    code,
    redirect_uri,
    code_verifier,
  };

  // Include client_secret only if configured (confidential client)
  if (CLIENT_SECRET) {
    params.client_secret = CLIENT_SECRET;
  }

  try {
    const tokenResponse = await fetch(tokenUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams(params),
    });

    if (!tokenResponse.ok) {
      const errorData = await tokenResponse.json().catch(() => ({}));
      console.error('Token exchange failed:', errorData);
      return NextResponse.json(
        {
          error: (errorData as Record<string, string>).error || 'token_exchange_failed',
          error_description: (errorData as Record<string, string>).error_description || 'Token exchange failed',
        },
        { status: tokenResponse.status }
      );
    }

    const tokens = await tokenResponse.json();

    // Return only non-sensitive session info to the client.
    // Raw tokens stay server-side in httpOnly cookies — never exposed to JS.
    const response = NextResponse.json({
      authenticated: true,
      expires_in: tokens.expires_in,
      token_type: tokens.token_type,
      scope: tokens.scope,
    });
    setAuthTokenCookies(response, tokens, process.env.NODE_ENV === 'production');
    return response;
  } catch (err) {
    console.error('Token exchange error:', err);
    return NextResponse.json(
      { error: 'internal_error', error_description: 'Failed to exchange token' },
      { status: 500 }
    );
  }
}
```

Source: `minds-ui/apps/app/app/api/auth/exchange/route.ts:24`.

## Response and error branches [#response-and-error-branches]

Literal HTTP statuses in the route: 400, 429, 500. Shared management error classes are defined in [Errors and responses](/docs/api/platform/errors).

```typescript
NextResponse.json(
      { error: 'rate_limited', error_description: 'Too many requests. Please try again later.' },
      { status: 429 }
    )
```

Source: `minds-ui/apps/app/app/api/auth/exchange/route.ts:28`.

```typescript
request.json()
```

Source: `minds-ui/apps/app/app/api/auth/exchange/route.ts:36`.

```typescript
NextResponse.json({ error: 'Invalid request body' }, { status: 400 })
```

Source: `minds-ui/apps/app/app/api/auth/exchange/route.ts:38`.

```typescript
NextResponse.json(
      { error: 'Missing required fields: code, code_verifier, redirect_uri' },
      { status: 400 }
    )
```

Source: `minds-ui/apps/app/app/api/auth/exchange/route.ts:44`.

```typescript
tokenResponse.json()
```

Source: `minds-ui/apps/app/app/api/auth/exchange/route.ts:73`.

```typescript
NextResponse.json(
        {
          error: (errorData as Record<string, string>).error || 'token_exchange_failed',
          error_description: (errorData as Record<string, string>).error_description || 'Token exchange failed',
        },
        { status: tokenResponse.status }
      )
```

Source: `minds-ui/apps/app/app/api/auth/exchange/route.ts:75`.

```typescript
tokenResponse.json()
```

Source: `minds-ui/apps/app/app/api/auth/exchange/route.ts:84`.

```typescript
NextResponse.json({
      authenticated: true,
      expires_in: tokens.expires_in,
      token_type: tokens.token_type,
      scope: tokens.scope,
    })
```

Source: `minds-ui/apps/app/app/api/auth/exchange/route.ts:88`.

```typescript
NextResponse.json(
      { error: 'internal_error', error_description: 'Failed to exchange token' },
      { status: 500 }
    )
```

Source: `minds-ui/apps/app/app/api/auth/exchange/route.ts:98`.

## Evidence [#evidence]

Generated from the local source snapshot on 2026-09-12. This page documents implemented code and does not certify a deployed service, permissions configuration, or successful provider operation.

Source file: `minds-ui/apps/app/app/api/auth/exchange/route.ts`. SHA-256: `ceb358b31fee28a198f0f1ebecc767e0aa011a3f3e87980934e0ba8f82a60919`.
