---
name: 3rd-party-auth
description: Use when building an application that needs users to sign in with their Adom account ("Sign in with Adom"), or when integrating external OAuth providers (Google, GitHub, Slack) into an Adom service. Covers Authentication Intents (confirmation code flow), Google OAuth sign-in, the OAuth Gateway, and session management.
---

# Third-Party Authentication

Let users sign in with their Adom account from any application — CLI tools, desktop apps, web apps, or embedded devices. The flow uses **Authentication Intents** with confirmation codes for security.

## Authentication Intents (Sign in with Adom)

The primary way for 3rd party apps to authenticate users. The confirmation code prevents phishing — the user must verify what they see in the browser matches what the app shows them.

### How It Works

```text
1. App creates an intent         POST /auth/intents
2. App shows confirmation code   e.g. "BRTK-MXVZ" in the terminal
3. App opens browser             → hydrogen/auth/intents/{token}
4. User logs in (if needed)      email/password or Google
5. User enters confirmation code → matches what the app showed
6. Carbon creates a session      → links it to the intent
7. App receives session token    via polling or SSE
```

### Step 1: Create an Intent

```bash
curl -X POST https://carbon.adom.inc/auth/intents \
  -H "Content-Type: application/json" \
  -d '{"max_age": 7776000}'
```

**Response:**

```json
{
  "token": "V1StGXR8_Z5jdHi6B-myT",
  "confirmation_code": "BRTK-MXVZ",
  "created_at": "2026-05-15T10:00:00Z",
  "expires_at": "2026-05-15T10:15:00Z",
  "requested_max_age": 7776000,
  "updates_url": "https://carbon.adom.inc/auth/intents/V1StGXR8_Z5jdHi6B-myT"
}
```

| Field | Description |
|-------|-------------|
| `token` | Unique identifier for this intent (nanoid) |
| `confirmation_code` | 8 consonants formatted as `XXXX-XXXX` — show this to the user |
| `expires_at` | Intent expires in 15 minutes |
| `requested_max_age` | Session TTL in seconds (default: 90 days) |
| `updates_url` | Full URL for polling status |

### Step 2: Show the Code and Open the Browser

Display the confirmation code prominently in your app, then open the browser:

```javascript
console.log(`\nConfirmation code: ${intent.confirmation_code}\n`);
console.log('Opening browser to complete sign-in...');

const loginUrl = `https://hydrogen.adom.inc/auth/intents/${intent.token}`;
open(loginUrl); // opens default browser
```

The user sees Hydrogen's login page. After logging in, they see the "Authorize Device" page with a code input field.

### Step 3: Wait for Authentication

**Option A — Server-Sent Events (recommended):**

```javascript
const eventSource = new EventSource(
  `https://carbon.adom.inc/auth/intents/${intent.token}/status`,
  { headers: { Accept: 'text/event-stream' } }
);

eventSource.addEventListener('authenticated', (e) => {
  const { session_token } = JSON.parse(e.data);
  console.log('Authenticated! Session token:', session_token);
  eventSource.close();
});

eventSource.addEventListener('timeout', () => {
  console.log('Intent expired. Please try again.');
  eventSource.close();
});
```

**Option B — Short polling:**

```javascript
async function pollForAuth(token) {
  while (true) {
    const res = await fetch(
      `https://carbon.adom.inc/auth/intents/${token}/status`
    );
    const data = await res.json();

    if (data.state === 'authenticated') {
      return data.session_token;
    }

    // Still pending — wait 2 seconds
    await new Promise(r => setTimeout(r, 2000));
  }
}
```

### Step 4: Use the Session Token

The returned `session_token` is a Carbon session. Use it as a Bearer token:

```bash
curl https://carbon.adom.inc/user \
  -H "Authorization: Bearer ${SESSION_TOKEN}"
```

Session tokens have the prefix `u_` (user) or `c_` (container). Application sessions created by intents use the `Application` session kind and respect the `max_age` you requested.

## Complete Example (Node.js CLI)

```javascript
import open from 'open';

const CARBON_API = 'https://carbon.adom.inc';

async function signIn() {
  // 1. Create intent
  const res = await fetch(`${CARBON_API}/auth/intents`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ max_age: 60 * 60 * 24 * 90 }),
  });
  const intent = await res.json();

  // 2. Show code and open browser
  console.log(`\n  Confirmation code: ${intent.confirmation_code}\n`);
  console.log('  Opening browser to sign in...\n');
  await open(`https://hydrogen.adom.inc/auth/intents/${intent.token}`);

  // 3. Wait for authentication via SSE
  const session_token = await new Promise((resolve, reject) => {
    const es = new EventSource(`${intent.updates_url}/status`, {
      headers: { Accept: 'text/event-stream' },
    });
    es.addEventListener('authenticated', (e) => {
      resolve(JSON.parse(e.data).session_token);
      es.close();
    });
    es.addEventListener('timeout', () => {
      reject(new Error('Login timed out'));
      es.close();
    });
  });

  console.log('Signed in successfully!');
  return session_token;
}
```

## Confirmation Code Details

- **Format:** 8 uppercase consonants, split as `XXXX-XXXX`
- **Alphabet:** B C D F G H J K L M N P Q R S T V W X Z (no vowels — avoids accidental profanity)
- **Verification:** Case-insensitive, dashes/spaces stripped before comparison
- **Purpose:** Prevents phishing — a malicious link can't trick a user into authorizing the wrong app because they'd need to see the code displayed by the legitimate app

## Intent Lifecycle

| State | Description |
|-------|-------------|
| **Created** | Intent exists, `session_id` is null, waiting for user |
| **Authenticated** | User entered correct code, session created and linked |
| **Consumed** | Application retrieved the session token |
| **Expired** | 15 minutes elapsed without completion |

Intents are one-time use. Once a session is linked, the intent cannot be reused. The session token can only be retrieved once (consumed flag).

## Google OAuth Sign-In

Users can now sign in to Hydrogen with their Google account. This is built into the login page — no extra integration needed from 3rd party apps.

**Flow:**
1. User clicks "Sign in with Google" on the login page
2. Redirected to Google's consent screen (OIDC)
3. Google sends an auth code back to Carbon
4. Carbon verifies the ID token, extracts email + profile
5. If the email matches an existing user → logs them in
6. If new user → redirects to username selection page

The Google sign-in is part of the login page that Authentication Intent users see, so 3rd party apps get it for free.

## OAuth Gateway (External Providers)

For integrating external OAuth providers (YouTube, GitHub, Slack) into Adom services, use the OAuth Gateway. This is a separate pattern from Authentication Intents — see the `oauth` skill for the full reference.

**Key difference:**
- **Authentication Intents** = "Sign in with Adom" (user authenticates TO your app via Adom)
- **OAuth Gateway** = "Connect YouTube/GitHub/etc." (user grants your Adom service access to an external provider)

## API Reference

### POST /auth/intents

Create a new authentication intent.

**Body:**
```json
{ "max_age": 7776000 }
```

`max_age` is optional (default: 90 days). Sets the TTL of the session that will be created.

**Response:** `201 Created` with the intent object.

### GET /auth/intents/{token}

Retrieve intent details. Returns the same object as creation.

**Errors:**
- `404` — Intent not found
- `410` — Intent expired

### GET /auth/intents/{token}/status

Check authentication status. Supports two modes:

**JSON (default):**
```json
{ "state": "pending" }
// or
{ "state": "authenticated", "session_token": "u_..." }
```

**SSE (Accept: text/event-stream):**
- Event `authenticated` with `{ "session_token": "u_..." }` data
- Event `timeout` when the intent expires
- Keep-alive pings to maintain connection

### PATCH /auth/intents/{token}

Link a user session to the intent. Requires authentication (session cookie) and the confirmation code.

**Body:**
```json
{ "confirmation_code": "BRTK-MXVZ" }
```

**Errors:**
- `403` — Wrong confirmation code
- `409` — Intent already has a linked session
- `410` — Intent expired

## Security

- Confirmation codes prevent session fixation and phishing attacks
- Auth codes and session tokens are never exposed in URLs or logs
- Intents expire after 15 minutes
- Sessions created by intents use the `Application` kind (distinguishable from user sessions)
- The `consumed` flag ensures one-time retrieval of session tokens
