skill / oauth

Install

Use when implementing OAuth authentication for any Adom service or feature — YouTube, GitHub, Slack, or any provider that uses OAuth 2.0.

adom-wiki pkg install kyle/oauth

Latest: v1.0.0, published

Contents

Adom OAuth

Add OAuth 2.0 authentication to any Adom service using the central OAuth Gateway. Users click one button, authorize in their browser, and they're connected. No per-user setup, no CLI commands.

Architecture

Every Adom user container has a unique hostname. OAuth providers (Google, GitHub, etc.) require a fixed redirect_uri. The OAuth Gateway solves this by providing a single static callback URL that routes auth codes to the correct container via WebSocket.

User clicks "Connect" in their container
  → Container connects to OAuth Gateway via WebSocket
  → Container registers a unique state token
  → User is redirected to OAuth provider (Google, GitHub, etc.)
     with redirect_uri = https://<gateway-host>/callback
  → User authorizes the app
  → Provider redirects to Gateway: /callback?code=XXX&state=YYY
  → Gateway looks up which WebSocket owns that state
  → Sends the auth code back over WebSocket
  → Container exchanges code for tokens locally
  → Container disconnects from Gateway

Key properties:

  • One fixed redirect URI for all users, all services, all providers
  • Credentials and tokens never pass through the gateway — only the auth code
  • Gateway is stateless — just routes state tokens to WebSocket connections
  • Connections are short-lived (connect, get code, disconnect)

OAuth Gateway Service

Location: gallia/services/oauth-gateway/ Port: 8795 Container: john-service-oauth-4e370c6271ae0433 Callback URL: https://oauth-4e370c62.adom.cloud/callback

HTTP Endpoints

Method Route Description
GET /health Health check with pending count and uptime
GET /callback OAuth callback — routes to the container that registered the state
GET /status JSON status (pending count, uptime)

WebSocket Protocol

Clients connect to the gateway via WebSocket and exchange JSON messages:

Client → Gateway:

{ "type": "register", "state": "<unique-uuid>", "provider": "google" }
{ "type": "unregister", "state": "<unique-uuid>" }

Gateway → Client:

{ "type": "registered", "state": "<uuid>" }
{ "type": "callback", "state": "<uuid>", "code": "<auth-code>", "query": { ... } }
{ "type": "error", "state": "<uuid>", "error": "<message>" }

Registrations auto-expire after 10 minutes. When a WebSocket disconnects, all its registrations are cleaned up.

Client Library

Location: gallia/services/oauth-gateway/client.js

Quick Start — startOAuthFlow()

The simplest way to add OAuth to any service:

import { startOAuthFlow } from '../services/oauth-gateway/client.js';

// 1. Start the flow (connects to gateway, generates state, builds auth URL)
const flow = startOAuthFlow({
  provider: 'google-youtube',
  authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
  clientId: 'YOUR_CLIENT_ID.apps.googleusercontent.com',
  scopes: 'https://www.googleapis.com/auth/youtube.upload',
  extraParams: { access_type: 'offline', prompt: 'consent' },
});

// 2. Redirect the user to the auth URL
// (in an HTTP handler: res.writeHead(302, { Location: flow.authRedirectUrl }))

// 3. Wait for the gateway to deliver the auth code
const { code } = await flow.waitForCode();

// 4. Exchange the code for tokens (using your app's client secret)
const tokens = await exchangeCodeForTokens(code, flow.redirectUri);

Environment Variables

Variable Default Description
OAUTH_GATEWAY_URL https://oauth-4e370c62.adom.cloud Gateway HTTP URL (for building redirect_uri)
OAUTH_GATEWAY_WS wss://oauth-4e370c62.adom.cloud Gateway WebSocket URL

These defaults are hardcoded in client.js — no env vars needed on user containers.

Adding OAuth to a New Service

Step 1: Bundle App Credentials

Store your OAuth client ID and secret in a JSON file in your service directory. This ships with the gallia repo — all users get the same app credentials.

// your-service/credentials.json
{
  "clientId": "YOUR_CLIENT_ID.apps.googleusercontent.com",
  "clientSecret": "YOUR_CLIENT_SECRET"
}

Register the gateway's callback URL as an authorized redirect URI in the provider's console:

https://<gateway-host>/callback

Step 2: Per-User Token Storage

Each user's tokens go in their home directory:

import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { dirname } from 'path';
import { homedir } from 'os';

const TOKEN_PATH = `${homedir()}/.config/your-service-tokens.json`;

function loadTokens() {
  if (!existsSync(TOKEN_PATH)) return null;
  return JSON.parse(readFileSync(TOKEN_PATH, 'utf-8'));
}

function saveTokens(tokens) {
  const dir = dirname(TOKEN_PATH);
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2) + '\n');
}

Step 3: Add Auth Route to Your Server

import { startOAuthFlow } from '../services/oauth-gateway/client.js';
import credentials from './credentials.json' with { type: 'json' };

// GET /your-service/auth — starts the OAuth flow
app.get('/your-service/auth', (req, res) => {
  const flow = startOAuthFlow({
    provider: 'your-provider',
    authUrl: 'https://provider.com/oauth/authorize',
    clientId: credentials.clientId,
    scopes: 'scope1 scope2',
    extraParams: { access_type: 'offline' },
  });

  // Redirect user to provider
  res.redirect(flow.authRedirectUrl);

  // Wait for callback in background
  flow.waitForCode().then(async ({ code }) => {
    const tokenRes = await fetch('https://provider.com/oauth/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        code,
        client_id: credentials.clientId,
        client_secret: credentials.clientSecret,
        redirect_uri: flow.redirectUri,
        grant_type: 'authorization_code',
      }),
    });
    const tokens = await tokenRes.json();
    saveTokens(tokens);
  }).catch(err => console.error('OAuth failed:', err.message));
});

Step 4: Token Refresh

const TOKEN_URL = 'https://provider.com/oauth/token';
const REFRESH_MARGIN_MS = 60_000;

async function getAccessToken() {
  let tokens = loadTokens();
  if (!tokens?.refreshToken) throw new Error('Not connected');

  // Return cached if still valid
  if (tokens.accessToken && tokens.expiresAt > Date.now() + REFRESH_MARGIN_MS) {
    return tokens.accessToken;
  }

  // Refresh
  const res = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id: credentials.clientId,
      client_secret: credentials.clientSecret,
      refresh_token: tokens.refreshToken,
      grant_type: 'refresh_token',
    }),
  });
  const newTokens = await res.json();
  tokens.accessToken = newTokens.access_token;
  tokens.expiresAt = Date.now() + (newTokens.expires_in * 1000);
  if (newTokens.refresh_token) tokens.refreshToken = newTokens.refresh_token;
  saveTokens(tokens);
  return tokens.accessToken;
}

Step 5: UI — Connect Button

In your HTML, show a "Connect" button when the service isn't configured:

// Check connection status
const res = await fetch('/your-service/status');
const { configured } = await res.json();

if (!configured) {
  // Show "Connect" button that opens /your-service/auth in a new tab
  window.open('/your-service/auth', '_blank');
  // Poll /your-service/status every 2s until configured
}

Remote Management via Container Conduit

The OAuth gateway container has Container Conduit installed, so you can manage it remotely from your main gallia editor without opening a separate VS Code session.

Container name: oauth-gateway

Common Operations

Check if the gateway is running:

container_exec on oauth-gateway: curl -sf http://127.0.0.1:8795/health

Check watchdog status:

container_exec on oauth-gateway: curl -sf http://127.0.0.1:8796/status

Restart the gateway (via watchdog):

container_exec on oauth-gateway: curl -sf -X POST http://127.0.0.1:8796/restart

Start/stop the gateway:

container_exec on oauth-gateway: curl -sf -X POST http://127.0.0.1:8796/start
container_exec on oauth-gateway: curl -sf -X POST http://127.0.0.1:8796/stop

View recent logs:

container_exec on oauth-gateway: tail -50 /tmp/oauth-gateway.log

Pull latest code from gallia and restart:

container_exec on oauth-gateway: cd /home/adom/gallia && git pull
container_exec on oauth-gateway: curl -sf -X POST http://127.0.0.1:8796/restart

Full system status (disk, memory, uptime):

container_status on oauth-gateway

Bootstrapping Container Conduit (if reinstall needed)

If the Conduit agent stops working, open a terminal on the OAuth container and run:

curl -sL https://conduit-d4d7f7f2.adom.cloud/agent/install | CC_BASE_URL=https://conduit-f280e93f.adom.cloud CC_NAME=oauth-gateway sudo -E bash

The agent auto-starts on reboot via cron. Config is at /opt/container-conduit/config.json.

Watchdog + Management Dashboard

The service container runs a watchdog (watchdog.js, port 8796) alongside the gateway. It polls /health every 10 seconds and auto-restarts the gateway after 3 consecutive failures.

The watchdog also exposes a control API:

Method Route Description
GET /status Watchdog state, last health check, restart count
POST /start Start the gateway
POST /stop Stop the gateway
POST /restart Restart the gateway
POST /watchdog Toggle auto-restart on/off

A management dashboard (dashboard.html) can be pushed to the Adom Viewer on the service container via show-dashboard.sh. This is an ops interface — start/stop/restart buttons, watchdog toggle, and a state-change event log. It's separate from the read-only service dashboard that end users see in AV's dropdown menu.

Both the gateway and watchdog are started by start-oauth-gateway.sh, which is called on container boot.

File Locations

Path Description
gallia/services/oauth-gateway/server.js Gateway server (port 8795)
gallia/services/oauth-gateway/watchdog.js Watchdog + control API (port 8796)
gallia/services/oauth-gateway/client.js Client library for services
gallia/services/oauth-gateway/dashboard.html Ops management dashboard (pushed to AV)
gallia/services/oauth-gateway/start-oauth-gateway.sh Starts gateway + watchdog (idempotent)
gallia/services/oauth-gateway/show-dashboard.sh Pushes dashboard to Adom Viewer
gallia/services/oauth-gateway/service.json Service manifest
gallia/youtube/credentials.json YouTube app credentials (bundled)
gallia/youtube/youtube-api.js YouTube token management + upload
~/.config/youtube-tokens.json Per-user YouTube tokens

Supported Providers

The gateway is provider-agnostic. Any OAuth 2.0 provider works:

Provider Auth URL Scopes
Google (YouTube) accounts.google.com/o/oauth2/v2/auth youtube.upload
Google (Drive) accounts.google.com/o/oauth2/v2/auth drive.file
GitHub github.com/login/oauth/authorize repo, user
Slack slack.com/oauth/v2/authorize chat:write, etc.

Test Users (IMPORTANT)

Until the app passes Google's OAuth verification, only test users explicitly added in Google Cloud Console can authorize. Anyone not on the list gets a hard block (not just a warning).

To add a test user: Google Cloud Console → OAuth consent screen → Test users → Add users → enter their Gmail address.

When a user hits this error: The YouTube upload dialog in Movie Maker should detect the 403 access_denied error and show a message like: "YouTube access is restricted. Ask an Adom admin to add your Google account as a test user, or email [email protected]." This saves users from a confusing dead end.

To remove the restriction permanently: Submit the app for OAuth verification (Google Cloud Console → OAuth consent screen → Publish app). Google reviews the app and removes the test user gate. This requires a privacy policy URL, homepage, and possibly a demo video.

Google Cloud Setup (for Adom admins)

  1. Create a Google Cloud project at console.cloud.google.com
  2. Enable the required API (e.g. YouTube Data API v3)
  3. Create OAuth credentials → Web application
  4. Add authorized redirect URI: https://oauth-4e370c62.adom.cloud/callback
  5. Copy client ID + secret to the service's credentials.json
  6. Add test users — OAuth consent screen → Test users → add Gmail addresses of anyone who needs access
  7. Submit for OAuth verification when ready (removes "unverified app" / test user restriction)
  8. Request quota increase if needed