Third-Party Authentication
Public UnreviewedSign in with Adom — let users authenticate to your app using their Adom account via Authentication Intents with confirmation codes. Covers the full flow, API reference, code examples, and Google OAuth
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
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
curl -X POST https://carbon.adom.inc/auth/intents \
-H "Content-Type: application/json" \
-d '{"max_age": 7776000}'
Response:
{
"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:
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):
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:
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:
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)
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:
- User clicks "Sign in with Google" on the login page
- Redirected to Google's consent screen (OIDC)
- Google sends an auth code back to Carbon
- Carbon verifies the ID token, extracts email + profile
- If the email matches an existing user → logs them in
- 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:
{ "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 found410— Intent expired
GET /auth/intents/{token}/status
Check authentication status. Supports two modes:
JSON (default):
{ "state": "pending" }
// or
{ "state": "authenticated", "session_token": "u_..." }
SSE (Accept: text/event-stream):
- Event
authenticatedwith{ "session_token": "u_..." }data - Event
timeoutwhen 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:
{ "confirmation_code": "BRTK-MXVZ" }
Errors:
403— Wrong confirmation code409— Intent already has a linked session410— 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
Applicationkind (distinguishable from user sessions) - The
consumedflag ensures one-time retrieval of session tokens
---
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