A simple Bun server that implements GitHub OAuth authentication for protecting multiple services in your homelab with a single authentication point.
- GitHub OAuth 2.0 authentication flow
- Session management with cookies shared across subdomains
- Auth verification endpoint for reverse proxy integration
- CSRF protection with state parameter
- Automatic session cleanup
- Dynamic redirect back to requested app after authentication
This server is designed to protect multiple apps on subdomains with a single OAuth flow:
- User visits
app1.domain.com→ redirected to auth → returns toapp1.domain.com - User visits
app2.domain.com→ already authenticated (shared cookie) → access granted - Works for unlimited apps:
app3.domain.com,app4.domain.com, etc.
- Go to https://github.com/settings/developers
- Click "New OAuth App"
- Fill in the details:
- Application name: Your homelab auth
- Homepage URL:
https://auth.mydomain.com - Authorization callback URL:
https://auth.mydomain.com/auth/callback⚠️ Must include/auth/callback
- Save the Client ID and Client Secret
cp .env.example .envUpdate .env for your homelab:
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
PUBLIC_URL=https://auth.mydomain.com
PORT=3000
COOKIE_DOMAINS=.mydomain.com
ALLOWED_USERS=your_github_username
NODE_ENV=productionImportant:
PUBLIC_URL- Your auth server's public URL (without/auth/callback). The callback URL is automatically constructed asPUBLIC_URL/auth/callbackCOOKIE_DOMAINS- Comma-separated list of domains (with leading dot) for cookie sharing across subdomains- Single domain:
COOKIE_DOMAINS=.domain.com - Multiple domains:
COOKIE_DOMAINS=.domain1.com,.domain2.com - The server will automatically select the correct domain based on the request hostname
- Single domain:
ALLOWED_USERS- Comma-separated list of GitHub usernames authorized to access. Leave empty to allow all users.
bun install
bun run devbun install --production
bun run src/index.tsdocker run -d \
--name github-oauth \
-p 3000:3000 \
-e GITHUB_CLIENT_ID=your_client_id \
-e GITHUB_CLIENT_SECRET=your_client_secret \
-e PUBLIC_URL=https://auth.mydomain.com \
-e COOKIE_DOMAINS=.mydomain.com \
-e ALLOWED_USERS=your_github_username \
-e NODE_ENV=production \
ghcr.io/YOUR_USERNAME/github-oauth:latestOr with docker-compose:
services:
github-oauth:
image: ghcr.io/YOUR_USERNAME/github-oauth:latest
ports:
- "3000:3000"
environment:
- GITHUB_CLIENT_ID=your_client_id
- GITHUB_CLIENT_SECRET=your_client_secret
- PUBLIC_URL=https://auth.mydomain.com
- COOKIE_DOMAINS=.mydomain.com
- ALLOWED_USERS=your_github_username
- NODE_ENV=production
restart: unless-stoppedOr build locally:
docker build -t github-oauth .
docker run -d -p 3000:3000 --env-file .env github-oauthInitiates GitHub OAuth. Optional redirect parameter specifies where to return after auth.
OAuth callback. Redirects user back to original app after successful authentication.
Verifies authentication. Returns 200 if authenticated, 401 if not.
Destroys session and clears cookies.
Health check endpoint.
auth.domain.com {
reverse_proxy localhost:3000
}This exposes your auth server at https://auth.domain.com. Caddy automatically handles HTTPS with Let's Encrypt.
# this import takes two args
# args[0] is reverse proxy config : {{upstreams 8081}}
# args[1] is fqdn of the app : https://app1.domain.com
(github) {
# github-oauth is the hosname of the container on the proxy network
forward_auth github-oauth:3000 {
uri /auth/verify
copy_headers X-Auth-User
@unauthorized status 401
handle_response @unauthorized {
# uri includes the full path requested by the user in the original request
redir {env.AUTH_SERVER_URL}/auth/login?redirect={args[1]}{uri}
}
@error {
status 403 500 502 503 504
}
handle_response @error {
respond "An error occurred: {http.error.status_code}" {http.error.status_code}
}
}
reverse_proxy {args[0]} # the actual app that is being protected
}
app1.domain.com {
import github localhost:8081 https://app1.domain.com
}
app2.domain.com {
import github localhost:8082 https://app2.domain.com
}How it works:
-
forward_auth localhost:3000- Before proxying to your app, Caddy makes a subrequest to the auth serveruri /auth/verify- Calls the/auth/verifyendpoint with the user's cookiescopy_headers X-Auth-User- Forwards auth headers to your app (optional)
-
@error status 401- Matches when/auth/verifyreturns 401 (not authenticated) -
handle_response @error- When user is not authenticated:- Redirects to
https://auth.domain.com/auth/login - Adds
?redirect=parameter so user returns to the original URL after login {uri}includes the full path (e.g.,/some/page)
- Redirects to
-
reverse_proxy localhost:8081- If authenticated, proxy to your actual app
Repeat for each app you want to protect:
All apps share the same session cookie because COOKIE_DOMAIN=.domain.com is set. Authenticate once, access all apps.
- User visits
https://app1.domain.com - Reverse proxy calls
/auth/verifyto check authentication - Not authenticated → redirect to
https://auth.domain.com/auth/login?redirect=https://app1.domain.com - OAuth flow completes
- Session cookie set with matching domain from
COOKIE_DOMAINS(e.g.,.domain.com) - User redirected back to
https://app1.domain.com - Now all
*.domain.comapps are accessible without re-authentication
Cookies Used:
-
oauth_state(temporary, 10 min) - CSRF protection- Random UUID created on login
- Sent to GitHub and verified on callback
- Prevents attackers from forging login requests
-
redirect_after_auth(temporary, 10 min) - Remembers destination- Stores the original URL user wanted to access
- After OAuth completes, redirects user back to this URL
-
github_auth_session(persistent, 24 hours) - Authentication session- Contains random session ID
- Checked by Caddy on every request via
/auth/verify - Session data (user info) stored server-side
The Complete Flow:
1. User visits app1.domain.com
→ No github_auth_session cookie
→ Caddy redirects to auth server
2. /auth/login
→ Sets oauth_state (CSRF protection)
→ Sets redirect_after_auth (remember destination)
→ Redirects to GitHub
3. GitHub OAuth
→ User authorizes
→ GitHub redirects back with code + state
4. /auth/callback
→ Verifies oauth_state matches (CSRF check)
→ Exchanges code for GitHub access token
→ Gets user info from GitHub API
→ Checks user whitelist (if configured)
→ Creates github_auth_session cookie
→ Clears temporary cookies
→ Redirects to original destination
5. Future requests
→ Caddy checks github_auth_session via /auth/verify
→ If valid → access granted
→ If expired/invalid → redirect to login
The session cookie is configured to work across all subdomains:
- Domain: Automatically selected from
COOKIE_DOMAINSbased on request hostname - Multiple Domains: Supports different domains in homelab (e.g.,
.domain1.comand.domain2.com) - HttpOnly: Prevents JavaScript access
- Secure: HTTPS only (in production)
- SameSite: Lax (allows navigation from GitHub)
- Max-Age: 24 hours
- Sessions stored in-memory (for production: use Redis)
- Sessions expire after 24 hours
- Automatic cleanup every hour
- Logout clears cookie across all subdomains
By default, any GitHub user can authenticate. To restrict access to specific users, set the ALLOWED_USERS environment variable:
ALLOWED_USERS=alice,bob,charlie- Comma-separated list of GitHub usernames (case-insensitive)
- Users not in the list will see "Access denied" after GitHub OAuth
- Leave empty or unset to allow all GitHub users
- Session Storage: Use Redis instead of in-memory for persistence
- HTTPS: Required for secure cookies (use Let's Encrypt with Caddy)
- User Whitelist: Use
ALLOWED_USERSto restrict access to your homelab - Rate Limiting: Protect auth endpoints
- Monitoring: Log authentication events
The Docker image uses oven/bun:1-alpine for a minimal footprint, resulting in:
- Smaller final image size (~50MB)
- Fast startup time
- Includes Bun runtime and all dependencies
GitHub Actions automatically builds and publishes multi-platform Docker images (amd64 & arm64) to GitHub Container Registry on:
- Every push to
mainbranch (tagged aslatest) - Every version tag (e.g.,
v1.0.0)
The images are available at: ghcr.io/YOUR_USERNAME/github-oauth
To pull and run:
docker pull ghcr.io/YOUR_USERNAME/github-oauth:latestMIT