From f9bd059a80a49d40b8642d236ad3f7117fe83ec2 Mon Sep 17 00:00:00 2001 From: Bryan Brooks Date: Thu, 11 Dec 2025 10:27:23 -0600 Subject: [PATCH] Add Docker support and homelab deployment documentation - Add multi-stage Dockerfile for gateway with non-root user and health checks - Add docker-compose.yml with Traefik labels and volume persistence - Add GitHub Actions workflow for building/pushing to ghcr.io (multi-arch) - Add comprehensive HOMELAB.md deployment guide with reverse proxy examples - Update README.md to emphasize self-hosted PDS as primary use case - Add .env.example with configuration template --- .env.example | 40 +++++ .github/workflows/docker.yml | 65 +++++++ README.md | 333 +++++++++++++++-------------------- docker-compose.yml | 65 +++++++ docs/HOMELAB.md | 269 ++++++++++++++++++++++++++++ gateway/.dockerignore | 31 ++++ gateway/Dockerfile | 56 ++++++ 7 files changed, 666 insertions(+), 193 deletions(-) create mode 100644 .env.example create mode 100644 .github/workflows/docker.yml create mode 100644 docker-compose.yml create mode 100644 docs/HOMELAB.md create mode 100644 gateway/.dockerignore create mode 100644 gateway/Dockerfile diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8f43c61 --- /dev/null +++ b/.env.example @@ -0,0 +1,40 @@ +# ATAuth Gateway Configuration +# Copy this file to .env and customize for your deployment + +# ============================================================================= +# REQUIRED SETTINGS +# ============================================================================= + +# Admin API token for registering applications +# Generate with: openssl rand -hex 32 +ADMIN_TOKEN=your-secure-admin-token-here + +# Your gateway's public URL (where users will be redirected) +# This must be accessible from the internet for OAuth to work +OAUTH_CLIENT_ID=https://auth.yourdomain.com/client-metadata.json +OAUTH_REDIRECT_URI=https://auth.yourdomain.com/auth/callback + +# ============================================================================= +# OPTIONAL SETTINGS +# ============================================================================= + +# Allowed CORS origins (comma-separated) +# Add your app domains here +CORS_ORIGINS=https://app1.yourdomain.com,https://app2.yourdomain.com + +# ============================================================================= +# SELF-HOSTED PDS CONFIGURATION +# ============================================================================= +# If you run your own PDS, your users authenticate with handles like: +# @alice.your-pds.com +# @bob.your-pds.com +# +# The gateway automatically discovers the user's PDS from their handle. +# No additional configuration needed - just ensure your PDS is accessible. +# +# Benefits of self-hosted PDS: +# - Full control over your identity infrastructure +# - No dependency on Bluesky's servers +# - Works on air-gapped networks (if PDS is local) +# - Your data stays on your infrastructure +# ============================================================================= diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..3bc3ae2 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,65 @@ +name: Build and Push Docker Image + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}-gateway + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./gateway + file: ./gateway/Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 + + - name: Generate SBOM + if: github.event_name != 'pull_request' + uses: anchore/sbom-action@v0 + with: + image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest diff --git a/README.md b/README.md index 072294b..4455ecc 100644 --- a/README.md +++ b/README.md @@ -1,214 +1,198 @@ -# ATAuth - AT Protocol Authentication Library +# ATAuth - Decentralized Authentication for AT Protocol -A complete, plug-and-play authentication system for AT Protocol (Bluesky) OAuth integration. +A complete, plug-and-play authentication system using AT Protocol (Bluesky) OAuth. Use it with Bluesky or **run your own PDS** for fully self-hosted, decentralized identity. + +## Why ATAuth? + +**No more password databases.** Users authenticate with their AT Protocol identity (`@user.bsky.social` or `@alice.your-pds.com`). You verify tokens - the identity provider handles the rest. + +| Traditional Auth | ATAuth | +|-----------------|--------| +| You store passwords | PDS manages identity | +| You implement MFA | PDS handles MFA | +| Password resets, account recovery | Not your problem | +| LDAP, Active Directory complexity | Simple token verification | + +**With a self-hosted PDS**, ATAuth becomes a fully independent auth system - no different from running your own Keycloak, but with portable, decentralized identities. + +## Quick Start (Docker) + +```bash +# Clone and configure +git clone https://github.com/Cache8063/atauth.git +cd atauth +cp .env.example .env +echo "ADMIN_TOKEN=$(openssl rand -hex 32)" >> .env + +# Start the gateway +docker compose up -d + +# Register your first app +curl -X POST http://localhost:3100/admin/apps \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"id": "myapp", "name": "My Application"}' +``` + +See [docs/HOMELAB.md](docs/HOMELAB.md) for complete deployment guide. ## Components | Component | Description | |-----------|-------------| -| **[gateway/](gateway/)** | Node.js OAuth gateway server | +| **[gateway/](gateway/)** | Node.js OAuth gateway server ([Docker image](https://ghcr.io/cache8063/atauth-gateway)) | | **[src/](src/)** | Rust token verification library | | **[ts/](ts/)** | TypeScript/React frontend utilities | -## Features - -- **OAuth Gateway**: Ready-to-deploy AT Protocol OAuth server -- **Token Verification**: Secure HMAC-SHA256 with constant-time comparison -- **Session Management**: Trait-based storage with SQLite and PostgreSQL backends -- **Rate Limiting**: IP-based rate limiting with configurable thresholds -- **Input Validation**: DID and handle format validation -- **TypeScript Support**: Full TypeScript/React utilities for frontend integration - ## Architecture ```text ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Your Frontend │────▶│ ATAuth Gateway │────▶│ Bluesky PDS │ -│ (React/Next) │ │ (Node.js) │ │ (OAuth Provider)│ +│ Your Apps │────▶│ ATAuth Gateway │────▶│ PDS │ +│ (any stack) │ │ (one place) │ │ (yours or bsky) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ - │ token │ HMAC secret + │ HMAC token │ AT Protocol OAuth ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ Your Backend │────▶│ Token Verify │ -│ (Rust/Node) │ │ (atauth lib) │ +│ (Rust/Node/?) │ │ (atauth lib) │ └─────────────────┘ └─────────────────┘ ``` -## Installation +**The gateway handles the complex OAuth flow once.** Your apps receive simple HMAC-signed tokens to verify. -### Rust +## Self-Hosted PDS = Full Independence -Add to your `Cargo.toml`: +When you run your own PDS: +- Users get handles like `@alice.your-domain.com` +- No dependency on Bluesky servers +- Works on air-gapped networks +- Same security model as enterprise OAuth +- Your identity, your infrastructure -```toml -[dependencies] -atauth = { git = "https://github.com/Cache8063/atauth" } +```yaml +# Add to your docker-compose.yml +services: + pds: + image: ghcr.io/bluesky-social/pds:latest + # ... configure for your domain +``` + +## Installation + +### Docker (Recommended) + +```bash +docker pull ghcr.io/cache8063/atauth-gateway:latest ``` -Or with specific features: +### Rust Library ```toml [dependencies] -atauth = { git = "https://github.com/Cache8063/atauth", features = ["session-sqlite", "rate-limit"] } +atauth = { git = "https://github.com/Cache8063/atauth" } ``` ### TypeScript/JavaScript ```bash npm install atauth -# or -pnpm add atauth -``` - -## Quick Start - -### Rust - Basic Token Verification - -```rust -use atauth::{TokenVerifier, TokenPayload}; - -// Create a verifier with your HMAC secret (shared with auth gateway) -let verifier = TokenVerifier::new(b"your-secret-key"); - -// Verify a token from the auth gateway -match verifier.verify("token-from-client") { - Ok(payload) => { - println!("Authenticated: {} ({})", payload.handle, payload.did); - // payload.user_id contains app-specific user ID if linked - } - Err(e) => { - eprintln!("Auth failed: {}", e); - } -} ``` -### Rust - With Session Store +## Usage -```rust -use atauth::{TokenVerifier, SessionManager}; -use atauth::session::SqliteSessionStore; - -// Setup -let verifier = TokenVerifier::new(b"your-secret-key"); -let session_store = SqliteSessionStore::new("sessions.db")?; -let sessions = SessionManager::new(session_store); - -// On login -if let Ok(payload) = verifier.verify(token) { - let session = sessions.create_session(&payload, token.to_string())?; - // Return session.token to client as session cookie -} +### Gateway - Register Apps -// On subsequent requests -match sessions.validate(session_token) { - Ok(session) => { - // User is authenticated - println!("User: {}", session.handle); - } - Err(_) => { - // Invalid or expired session - } -} - -// On logout -sessions.invalidate(session_token)?; +```bash +curl -X POST https://auth.example.com/admin/apps \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "jellyfin", + "name": "Jellyfin", + "callback_url": "https://jellyfin.example.com/sso/callback" + }' +# Returns: { "hmac_secret": "..." } - save this! ``` -### Rust - With Rate Limiting +### Rust - Verify Tokens ```rust -use atauth::rate_limit::{RateLimiter, RateLimiterConfig}; -use std::time::Duration; +use atauth::TokenVerifier; -let config = RateLimiterConfig::default() - .with_max_attempts(5) - .with_lockout(Duration::from_secs(300)); +let verifier = TokenVerifier::new(b"your-hmac-secret"); -let limiter = RateLimiter::new(config); - -// Before authentication attempt -let client_ip = "192.168.1.1".parse().unwrap(); -if let Err(e) = limiter.check(&client_ip) { - return Err(e); // Rate limited +match verifier.verify(token) { + Ok(payload) => { + println!("Welcome, {}!", payload.handle); + // payload.did, payload.user_id, payload.app_id available + } + Err(e) => eprintln!("Auth failed: {}", e), } - -// After failed attempt -limiter.record_failure(&client_ip); - -// After successful attempt -limiter.record_success(&client_ip); ``` -### TypeScript - React Integration +### TypeScript - Frontend Integration ```tsx -import { initAuthStore, useAuthStore } from 'atauth/react'; - -// Initialize once at app startup -initAuthStore({ - gatewayUrl: 'https://auth.example.com', - appId: 'myapp', - callbackUrl: 'https://myapp.com/auth/callback', -}); +import { useAuthStore } from 'atauth/react'; function LoginButton() { - const { isAuthenticated, user, login, logout, isLoading } = useAuthStore(); - - if (isLoading) { - return
Loading...
; - } + const { isAuthenticated, user, login, logout } = useAuthStore(); if (isAuthenticated) { return ( -
+ <> Welcome, {user?.handle}! -
+ ); } - return ; + return ; } ``` -### TypeScript - Manual Token Handling - -```typescript -import { - decodeToken, - handleCallback, - isOAuthCallback, - redirectToAuth -} from 'atauth'; - -// Redirect to auth -redirectToAuth({ - gatewayUrl: 'https://auth.example.com', - appId: 'myapp', - callbackUrl: window.location.origin + '/callback', -}); - -// Handle callback page -if (isOAuthCallback()) { - const result = handleCallback({ gatewayUrl: 'https://auth.example.com' }); - - if (result.success) { - console.log('Logged in as:', result.user?.handle); - // Redirect to app - window.location.href = result.returnTo || '/'; - } else { - console.error('Auth failed:', result.error); +### Node.js - Verify Tokens + +```javascript +import crypto from 'crypto'; + +function verifyToken(token, secret) { + const [payloadB64, signature] = token.split('.'); + const expected = crypto + .createHmac('sha256', secret) + .update(payloadB64) + .digest('base64url'); + + // Constant-time comparison + if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { + return null; } + + const payload = JSON.parse(Buffer.from(payloadB64, 'base64url')); + return payload.exp > Date.now() / 1000 ? payload : null; } ``` +## Features + +- **Multi-App Support**: One gateway serves all your apps with isolated secrets +- **Rate Limiting**: Built-in IP-based rate limiting (configurable) +- **Session Management**: SQLite or PostgreSQL session stores +- **Security Hardened**: Constant-time comparison, CSRF protection, secure token transport +- **Docker Ready**: Multi-arch images (amd64/arm64) +- **Homelab Friendly**: Works with Traefik, Caddy, nginx + ## Token Format -Tokens follow a JWT-like structure: `base64url(payload).base64url(signature)` +Simple HMAC-signed tokens (not full JWT): -### Payload Structure +``` +base64url(payload).base64url(hmac_sha256(payload, secret)) +``` +Payload: ```json { "did": "did:plc:abc123...", @@ -221,65 +205,28 @@ Tokens follow a JWT-like structure: `base64url(payload).base64url(signature)` } ``` -## API Reference - -### Rust - -#### `TokenVerifier` - -- `new(secret: &[u8])` - Create verifier with HMAC secret -- `from_hex(hex: &str)` - Create from hex-encoded secret -- `from_base64(b64: &str)` - Create from base64-encoded secret -- `verify(token: &str) -> Result` - Verify and decode token -- `sign(payload: &TokenPayload) -> Result` - Sign a payload (for gateways) - -#### `SessionManager` - -- `new(store: S)` - Create manager with store -- `create_session(payload, token)` - Create session from token -- `validate(token)` - Validate and optionally extend session -- `invalidate(token)` - Delete session -- `invalidate_all_for_did(did)` - Delete all sessions for user -- `cleanup()` - Remove expired sessions - -#### `RateLimiter` - -- `new(config)` - Create with configuration -- `check(ip)` - Check if IP can attempt auth -- `record_failure(ip)` - Record failed attempt -- `record_success(ip)` - Clear on success - -### TypeScript - -#### Token Utilities - -- `decodeToken(token)` - Decode without verification -- `isTokenExpired(payload)` - Check expiration -- `getTokenRemainingSeconds(payload)` - Time until expiry -- `isValidDid(did)` - Validate DID format -- `isValidHandle(handle)` - Validate handle format +## Documentation -#### OAuth Utilities +- [Homelab Deployment Guide](docs/HOMELAB.md) - Docker, reverse proxy configs, self-hosted PDS +- [Security Policy](SECURITY.md) - Reporting vulnerabilities +- [Contributing](CONTRIBUTING.md) - How to contribute -- `redirectToAuth(config, state?)` - Start OAuth flow -- `handleCallback(config)` - Process callback -- `isOAuthCallback()` - Check if on callback page -- `buildLogoutUrl(config)` - Get logout URL +## Use Cases -#### React Hooks +- **Homelab SSO**: Single sign-on for Jellyfin, NextCloud, Gitea, etc. +- **Multi-tenant Apps**: Users bring their own identity +- **AT Protocol Apps**: Games, social apps, tools for the ATmosphere +- **Enterprise**: Self-hosted PDS for internal identity -- `useAuthStore()` - Main auth hook -- `useUser()` - Get current user -- `useIsAuthenticated()` - Check auth status -- `useNeedsRefresh()` - Check if token needs refresh +## Security -## Security Considerations +- HMAC-SHA256 with constant-time verification +- Rate limiting on all endpoints +- CSRF protection via cryptographic nonces +- Tokens in URL fragments (not query params) +- Sanitized error responses -- **Server-side verification**: Always verify tokens server-side. Client-side decoding is for display only. -- **Constant-time comparison**: Signature verification uses constant-time comparison to prevent timing attacks. -- **Rate limiting**: Enable rate limiting in production to prevent brute force attacks. -- **HTTPS only**: Always use HTTPS in production. -- **Secure secrets**: Store HMAC secrets securely, never in client code. +See [SECURITY.md](SECURITY.md) for reporting vulnerabilities. ## License diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6794e38 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,65 @@ +# ATAuth - Self-Hosted AT Protocol Authentication +# +# Quick Start: +# 1. Copy .env.example to .env and configure +# 2. docker compose up -d +# 3. Register your first app via the admin API +# +# For use with your own PDS (recommended): +# - Set OAUTH_CLIENT_ID to your gateway's public URL +# - Point your apps to authenticate via this gateway +# - Users sign in with their AT Protocol handle (@user.your-pds.com) + +services: + atauth: + build: + context: ./gateway + dockerfile: Dockerfile + image: ghcr.io/cache8063/atauth-gateway:latest + container_name: atauth-gateway + restart: unless-stopped + ports: + - "3100:3100" + environment: + # Gateway configuration + - PORT=3100 + - HOST=0.0.0.0 + + # OAuth client identity (your gateway's public URL) + - OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID:-https://auth.example.com/client-metadata.json} + - OAUTH_REDIRECT_URI=${OAUTH_REDIRECT_URI:-https://auth.example.com/auth/callback} + + # Admin API token (generate with: openssl rand -hex 32) + - ADMIN_TOKEN=${ADMIN_TOKEN} + + # CORS origins (comma-separated) + - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:3000,http://localhost:5173} + + # Database path + - DB_PATH=/app/data/gateway.db + volumes: + - atauth-data:/app/data + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3100/health"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 10s + labels: + # Traefik labels (optional - uncomment if using Traefik) + # - "traefik.enable=true" + # - "traefik.http.routers.atauth.rule=Host(`auth.example.com`)" + # - "traefik.http.routers.atauth.entrypoints=websecure" + # - "traefik.http.routers.atauth.tls.certresolver=letsencrypt" + # - "traefik.http.services.atauth.loadbalancer.server.port=3100" + + # Homepage dashboard (optional) + - "homepage.group=Authentication" + - "homepage.name=ATAuth Gateway" + - "homepage.icon=lock" + - "homepage.href=https://auth.example.com" + - "homepage.description=AT Protocol SSO Gateway" + +volumes: + atauth-data: + driver: local diff --git a/docs/HOMELAB.md b/docs/HOMELAB.md new file mode 100644 index 0000000..4075f77 --- /dev/null +++ b/docs/HOMELAB.md @@ -0,0 +1,269 @@ +# ATAuth Homelab Deployment Guide + +ATAuth provides decentralized authentication for your homelab using AT Protocol. Instead of managing passwords, users authenticate with their AT Protocol identity (like `@alice.your-pds.com`). + +## Why AT Protocol for Homelab Auth? + +| Traditional SSO | ATAuth + AT Protocol | +|----------------|---------------------| +| You manage passwords | PDS manages identity | +| You handle MFA | PDS handles MFA | +| You store credentials | No credentials stored | +| Single point of failure | Decentralized identity | +| Complex setup (LDAP, etc.) | Simple gateway deployment | + +**With a self-hosted PDS**, you have: +- Full control over your identity infrastructure +- No external dependencies (works offline) +- Same security model as enterprise OAuth +- Portable identity across the AT Protocol network + +## Quick Start + +### 1. Prerequisites + +- Docker and Docker Compose +- A domain name (e.g., `auth.homelab.local`) +- Reverse proxy with TLS (Caddy, Traefik, or nginx) +- Optional: Your own PDS for full independence + +### 2. Deploy the Gateway + +```bash +# Clone the repository +git clone https://github.com/Cache8063/atauth.git +cd atauth + +# Configure environment +cp .env.example .env + +# Generate admin token +echo "ADMIN_TOKEN=$(openssl rand -hex 32)" >> .env + +# Edit .env with your domain +nano .env + +# Start the gateway +docker compose up -d +``` + +### 3. Configure Your Domain + +Update `.env`: +```env +OAUTH_CLIENT_ID=https://auth.homelab.local/client-metadata.json +OAUTH_REDIRECT_URI=https://auth.homelab.local/auth/callback +CORS_ORIGINS=https://jellyfin.homelab.local,https://nextcloud.homelab.local +``` + +### 4. Register Your Apps + +```bash +# Register Jellyfin +curl -X POST https://auth.homelab.local/admin/apps \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "jellyfin", + "name": "Jellyfin Media Server", + "callback_url": "https://jellyfin.homelab.local/sso/callback" + }' + +# Save the returned hmac_secret for your app's backend +``` + +## Reverse Proxy Configuration + +### Caddy (Recommended) + +```caddyfile +auth.homelab.local { + reverse_proxy atauth:3100 +} +``` + +### Traefik + +```yaml +# docker-compose.yml labels +labels: + - "traefik.enable=true" + - "traefik.http.routers.atauth.rule=Host(`auth.homelab.local`)" + - "traefik.http.routers.atauth.entrypoints=websecure" + - "traefik.http.routers.atauth.tls.certresolver=letsencrypt" + - "traefik.http.services.atauth.loadbalancer.server.port=3100" +``` + +### nginx + +```nginx +server { + listen 443 ssl http2; + server_name auth.homelab.local; + + ssl_certificate /etc/ssl/certs/auth.homelab.local.crt; + ssl_certificate_key /etc/ssl/private/auth.homelab.local.key; + + location / { + proxy_pass http://atauth:3100; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +## Self-Hosted PDS Setup + +For complete independence from Bluesky, run your own PDS: + +```yaml +# docker-compose.pds.yml +services: + pds: + image: ghcr.io/bluesky-social/pds:latest + environment: + - PDS_HOSTNAME=pds.homelab.local + - PDS_JWT_SECRET=your-jwt-secret + - PDS_ADMIN_PASSWORD=your-admin-password + - PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=your-key + - PDS_DATA_DIRECTORY=/pds + - PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks + - PDS_DID_PLC_URL=https://plc.directory + - PDS_BSKY_APP_VIEW_URL=https://api.bsky.app + - PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app + - PDS_REPORT_SERVICE_URL=https://mod.bsky.app + - PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac + - PDS_CRAWLERS=https://bsky.network + volumes: + - pds-data:/pds + ports: + - "3000:3000" +``` + +Then your users can create accounts like `@alice.pds.homelab.local` and authenticate through ATAuth. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Your Homelab Network │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Jellyfin │ │ NextCloud │ │ Gitea │ │ +│ │ │ │ │ │ │ │ +│ │ (app_id: │ │ (app_id: │ │ (app_id: │ │ +│ │ jellyfin) │ │ nextcloud) │ │ gitea) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ +│ └─────────────────┼─────────────────┘ │ +│ │ │ +│ ┌───────▼───────┐ │ +│ │ ATAuth │ │ +│ │ Gateway │ │ +│ │ (one place) │ │ +│ └───────┬───────┘ │ +│ │ │ +│ ┌───────▼───────┐ │ +│ │ Your PDS or │ │ +│ │ Bluesky │ │ +│ └───────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Integrating Apps + +### Generic OAuth Integration + +Most apps with OAuth support can use ATAuth: + +1. **Register the app** with ATAuth admin API +2. **Configure OAuth** in the app: + - Authorization URL: `https://auth.homelab.local/auth/init` + - Callback URL: Your app's callback +3. **Verify tokens** in your app's backend using the HMAC secret + +### Token Verification (Backend) + +For apps you control, verify tokens server-side: + +**Node.js:** +```javascript +import crypto from 'crypto'; + +function verifyToken(token, secret) { + const [payloadB64, signature] = token.split('.'); + const expected = crypto + .createHmac('sha256', secret) + .update(payloadB64) + .digest('base64url'); + + if (signature !== expected) return null; + + const payload = JSON.parse(Buffer.from(payloadB64, 'base64url')); + if (payload.exp < Date.now() / 1000) return null; + + return payload; // { did, handle, user_id, app_id, exp } +} +``` + +**Rust:** +```rust +use atauth::TokenVerifier; + +let verifier = TokenVerifier::new(b"your-hmac-secret"); +match verifier.verify(token) { + Ok(payload) => println!("Authenticated: {}", payload.handle), + Err(e) => println!("Invalid token: {}", e), +} +``` + +## Security Considerations + +1. **Always use TLS** - OAuth requires HTTPS +2. **Protect admin token** - Only use for initial app registration +3. **HMAC secrets** - Store securely, rotate periodically +4. **Rate limiting** - Built-in, but consider additional WAF rules +5. **Network isolation** - Keep ATAuth on a trusted network segment + +## Troubleshooting + +### OAuth Error: "Client not found" + +Your `OAUTH_CLIENT_ID` must be a publicly accessible URL that returns valid client metadata. Verify: +```bash +curl https://auth.homelab.local/client-metadata.json +``` + +### Users Can't Authenticate + +1. Check PDS is reachable from ATAuth container +2. Verify user's handle resolves correctly +3. Check ATAuth logs: `docker compose logs atauth` + +### Token Verification Fails + +1. Ensure HMAC secret matches between gateway and your app +2. Check token hasn't expired +3. Verify `app_id` matches the registered app + +## Updating + +```bash +docker compose pull +docker compose up -d +``` + +## Backup + +The gateway stores data in a SQLite database: +```bash +# Backup +docker compose exec atauth cp /app/data/gateway.db /app/data/gateway.db.backup + +# Or backup the volume +docker run --rm -v atauth_atauth-data:/data -v $(pwd):/backup alpine \ + tar czf /backup/atauth-backup.tar.gz -C /data . +``` diff --git a/gateway/.dockerignore b/gateway/.dockerignore new file mode 100644 index 0000000..248f249 --- /dev/null +++ b/gateway/.dockerignore @@ -0,0 +1,31 @@ +# Dependencies +node_modules/ + +# Build output (rebuilt in container) +dist/ + +# Development files +*.log +.env +.env.* +!.env.example + +# IDE +.vscode/ +.idea/ + +# Test files +*.test.ts +__tests__/ +coverage/ + +# Git +.git/ +.gitignore + +# Documentation +*.md +!README.md + +# Data (mounted as volume) +data/ diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 0000000..0b86090 --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,56 @@ +# ATAuth Gateway +# AT Protocol OAuth gateway for self-hosted authentication +# +# Build: docker build -t atauth-gateway . +# Run: docker run -p 3100:3100 -v ./data:/app/data atauth-gateway + +FROM node:20-alpine AS builder + +WORKDIR /app + +# Install dependencies +COPY package*.json ./ +RUN npm ci + +# Build TypeScript +COPY tsconfig.json ./ +COPY src/ ./src/ +RUN npm run build + +# Production image +FROM node:20-alpine + +WORKDIR /app + +# Create non-root user +RUN addgroup -g 1001 atauth && \ + adduser -u 1001 -G atauth -s /bin/sh -D atauth + +# Install production dependencies only +COPY package*.json ./ +RUN npm ci --omit=dev && npm cache clean --force + +# Copy built files +COPY --from=builder /app/dist ./dist + +# Create data directory +RUN mkdir -p /app/data && chown -R atauth:atauth /app/data + +# Switch to non-root user +USER atauth + +# Environment defaults +ENV NODE_ENV=production \ + PORT=3100 \ + HOST=0.0.0.0 \ + DB_PATH=/app/data/gateway.db + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3100/health || exit 1 + +EXPOSE 3100 + +VOLUME ["/app/data"] + +CMD ["node", "dist/index.js"] -- 2.51.2