AtAuth

ATAuth Homelab Deployment Guide#

ATAuth provides OIDC-based authentication for your homelab using AT Protocol identities. Users sign in with their @handle (Bluesky or self-hosted PDS) and your apps receive standard OIDC tokens.

Prerequisites#

  • Docker and Docker Compose
  • A domain name with DNS pointing to your server
  • A reverse proxy that terminates TLS (Caddy, Traefik, or nginx)
  • Optional: Your own PDS for fully self-hosted identity

Deploy#

git clone https://github.com/Cache8063/atauth.git
cd atauth
cp gateway/.env.example .env

Edit .env:

  1. Replace every auth.yourdomain.com with your actual domain
  2. Set CORS_ORIGINS to the domains of apps that will use ATAuth
  3. Generate three secrets (openssl rand -hex 32 for each) and fill in ADMIN_TOKEN, OIDC_KEY_SECRET, and MFA_ENCRYPTION_KEY

See gateway/.env.example for all available options including email, forward-auth proxy, and passkey configuration.

Start the gateway:

docker compose up -d

The gateway runs on 127.0.0.1:3100. Set up a reverse proxy with TLS (see Reverse Proxy Configuration below).

Verify it's running:

curl https://your-atauth-domain/.well-known/openid-configuration

Register Apps#

Open https://your-atauth-domain/admin/login, enter your admin token, then use the Setup Wizard to register apps. It includes presets for:

  • Audiobookshelf, Jellyfin, Gitea/Forgejo, Nextcloud, Immich
  • Grafana, Wiki.js, Portainer, Outline, Mealie
  • Paperless-ngx, Vaultwarden, Miniflux, Mattermost, Vikunja
  • Plane, GoToSocial, Stirling-PDF, Tandoor Recipes, FreshRSS

Each preset auto-fills the correct redirect URI, scopes, and grant types.

Option 2: Admin API#

curl -X POST https://your-atauth-domain/admin/oidc/clients \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "jellyfin",
    "name": "Jellyfin",
    "redirect_uris": ["https://jellyfin.example.com/sso/OID/redirect/ATAuth"],
    "grant_types": ["authorization_code", "refresh_token"],
    "scopes": ["openid", "profile", "email"],
    "require_pkce": true,
    "token_endpoint_auth_method": "client_secret_basic"
  }'

Save the returned client_secret -- it is only shown once.

Configure Your App#

Point your app's OIDC settings to ATAuth's discovery URL:

https://your-atauth-domain/.well-known/openid-configuration

This auto-discovers all endpoints. Most apps need:

Setting Value
Discovery URL https://your-atauth-domain/.well-known/openid-configuration
Client ID The ID you chose when registering
Client Secret The secret returned at registration
Scopes openid profile email

Forward-Auth Proxy#

For apps without OIDC support, ATAuth provides nginx auth_request based SSO. Enable it in .env:

FORWARD_AUTH_ENABLED=true
FORWARD_AUTH_SESSION_SECRET=<generate-with-openssl-rand-hex-32>

Then:

  1. Register the origin in the admin dashboard under Proxy Origins
  2. Add access rules under Access Rules
  3. Use the Proxy Setup Wizard to generate nginx config snippets

Example nginx config:

location / {
    auth_request /auth/verify;
    auth_request_set $auth_did $upstream_http_x_auth_did;
    auth_request_set $auth_handle $upstream_http_x_auth_handle;
    proxy_set_header X-Auth-DID $auth_did;
    proxy_set_header X-Auth-Handle $auth_handle;
    proxy_pass http://your-app;
}

location = /auth/verify {
    internal;
    proxy_pass http://atauth:3100;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
}

Reverse Proxy Configuration#

Caddy#

auth.example.com {
    reverse_proxy localhost:3100
}

Traefik#

labels:
  - "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"

nginx#

server {
    listen 443 ssl http2;
    server_name auth.example.com;

    ssl_certificate /etc/ssl/certs/auth.example.com.crt;
    ssl_certificate_key /etc/ssl/private/auth.example.com.key;

    location / {
        proxy_pass http://localhost: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#

With your own PDS, ATAuth becomes fully independent:

  • Users get handles like @alice.your-domain.com
  • No dependency on Bluesky
  • Works on air-gapped networks

The gateway auto-discovers the user's PDS from their handle. No special configuration needed -- just ensure the PDS is reachable.

Access Control#

ATAuth supports per-user access rules:

  • Allow/deny by DID: Target specific AT Protocol identities
  • Allow/deny by handle pattern: *.your-domain.com matches all handles on a PDS domain
  • Deny overrides: Deny rules always win regardless of order
  • Per-origin scoping: Rules can target a specific origin or apply globally

Manage rules via the admin dashboard or API.

Backup#

# Backup the SQLite database
docker compose exec atauth cp /app/data/gateway.db /app/data/gateway.db.backup

# Or backup the entire data volume
docker run --rm -v atauth_atauth-data:/data -v $(pwd):/backup alpine \
  tar czf /backup/atauth-backup.tar.gz -C /data .

Updating#

Using the pre-built image from GHCR:

docker pull ghcr.io/cache8063/atauth-gateway:latest
docker compose up -d

Or if building locally:

git pull
docker compose build
docker compose up -d

Troubleshooting#

OIDC discovery returns 404#

Ensure OAUTH_CLIENT_ID in .env matches your public URL and the gateway is running. Check logs with docker compose logs atauth.

Users can't authenticate#

  1. Check the PDS is reachable from the ATAuth container: docker compose exec atauth wget -qO- https://bsky.social/xrpc/_health
  2. Verify the user's handle resolves correctly
  3. Check logs: docker compose logs atauth

"invalid_redirect_uri"#

The redirect URI in the OIDC request must exactly match one registered for the client (including scheme, host, port, and path).

CORS errors in browser console#

Add the app's origin to CORS_ORIGINS in .env and restart: docker compose restart atauth.