my own indieAuth provider! indiko.dunkirk.sh/docs
indieauth oauth2-server

README.md

Indiko #

screenshot of oauth page

No that was not a typo the project's name actually is indiko! This is a small IndieAuth server (authorization endpoint + token endpoint) running on bun with sqlite and serving as the authentication provider for my homelab / side projects. It also supports custom clients with roles over the OAuth 2.0 spec.

It follows the IndieAuth Living Standard, 11 July 2024 and passes every applicable check in the indieauth.rocks server suite (81 passed, 0 failed, 0 warnings; the 3 skipped are the JWT access-token checks, which do not apply to opaque tokens). See the docs for the full list of what is and is not supported.

The canonical repo for this is hosted on tangled over at dunkirk.sh/indiko

Quick Start #

Prerequisites #

  • Bun v1.0 or higher
  • A domain with HTTPS (required for WebAuthn)

Installation #

  1. Clone the repository:
git clone https://github.com/taciturnaxolotl/indiko.git
cd indiko
  1. Install dependencies:
bun install
  1. Create a .env file:
cp .env.example .env

Configure the following environment variables:

ORIGIN=https://your-indiko-domain.com
RP_ID=your-indiko-domain.com
PORT=3000
NODE_ENV=production
  • ORIGIN - Full URL where Indiko is hosted (must match RP_ID)
  • RP_ID - Domain for WebAuthn (no protocol, matches ORIGIN domain)
  • PORT - Port to run the server on
  • NODE_ENV - Environment (dev/production)

The database will be automatically created at ./indiko.db on first run.

  1. Start the server:
# Development (with hot reload)
bun run dev

# Production
bun run start

First User Setup #

On first run, you'll need to create an admin user:

  1. Visit https://your-indiko-domain.com/login
  2. Register with a passkey
  3. This first user will automatically be an admin

After the first user is created, the bootstrap invite is disabled. Subsequent users must be invited by an admin.

Usage #

Client Types #

Indiko supports two types of OAuth clients:

Auto-registered Clients (IndieAuth) #

Any app can use Indiko without admin approval. On first authorization:

  • Use any valid URL as your client_id (e.g., https://myapp.example.com)
  • Indiko fetches metadata from your client_id URL
  • App is automatically registered
  • MUST use PKCE (code_verifier) for security
  • No client secret (public client)
  • Cannot use role-based access control

This is perfect for IndieAuth-compatible apps and personal projects.

Pre-registered Clients (OAuth 2.0 with Secrets) #

For apps requiring client secrets or role-based access control, admins can pre-register clients:

  1. Go to /admin/clients

  2. Click "Create OAuth Client"

  3. Fill in:

    • Name - Display name for your app
    • Logo URL - (Optional) URL to app logo
    • Description - (Optional) Brief description
    • Redirect URIs - One or more OAuth callback URLs
    • Available Roles - (Optional) Define roles users can be assigned
    • Default Role - (Optional) Auto-assign this role on first auth
  4. Save and copy the generated credentials:

    • Client ID - Format: ikc_xxxxxxxxxxxxxxxxxxxxx
    • Client Secret - Format: iks_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

    [!IMPORTANT] The client secret is only shown once! Save it securely.

Pre-registered clients:

  • MUST use both PKCE and client_secret in token requests
  • Support role assignment for RBAC (Role-Based Access Control)
  • Admin-managed metadata and permissions
  • Generated client ID format: ikc_ prefix

Using as an IndieAuth Provider #

Add these tags to your website's <head>:

<link
  rel="authorization_endpoint"
  href="https://your-indiko-domain.com/auth/authorize"
/>
<link rel="token_endpoint" href="https://your-indiko-domain.com/auth/token" />
<link rel="me" href="https://your-indiko-domain.com/u/your-username" />

Now you can sign in to IndieAuth-compatible sites using https://your-domain.com/ as your identity.

Using as an OpenID Connect (OIDC) Provider #

Indiko also supports OpenID Connect (OIDC) for modern authentication flows:

Discovery endpoint:

https://your-indiko-domain.com/.well-known/openid-configuration

Key features:

  • Authorization Code Flow with PKCE
  • ID Token with RS256 signing
  • JWKS endpoint for token verification
  • Support for openid, profile, and email scopes
  • Userinfo endpoint for retrieving user claims

Test your OIDC setup using the OIDC Debugger.

API Reference #

OAuth 2.0 / OpenID Connect Endpoints #

  • GET /auth/authorize - Authorization endpoint (OAuth 2.0 / OIDC)
  • POST /auth/token - Token exchange endpoint (returns access token and ID token for OIDC)
  • GET /userinfo - OIDC userinfo endpoint (returns user claims)
  • GET /.well-known/openid-configuration - OIDC discovery document
  • GET /jwks - JSON Web Key Set for ID token verification
  • POST /auth/logout - Session logout

User Profile #

  • GET /u/:username - Public h-card profile

Admin API (requires admin token) #

  • GET /api/admin/users - List all users
  • POST /api/admin/invites - Create invite
  • GET /api/admin/invites - List invites
  • GET /api/admin/clients - List OAuth clients
  • POST /api/admin/clients - Create OAuth client
  • GET /api/admin/clients/:clientId - Get client details
  • PUT /api/admin/clients/:clientId - Update client
  • DELETE /api/admin/clients/:clientId - Delete client

Production Deployment #

Reverse Proxy Configuration #

Indiko should be deployed behind a reverse proxy (nginx, Caddy, Traefik) for production use. The proxy should add security headers.

nginx Example #

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

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # Security headers
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # Content Security Policy
    add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; script-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;

    location / {
        proxy_pass http://localhost:3000;
        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;
    }
}

Caddy Example #

auth.example.com {
    reverse_proxy localhost:3000

    header {
        X-Frame-Options "DENY"
        X-Content-Type-Options "nosniff"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
        Permissions-Policy "geolocation=(), microphone=(), camera=()"
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; script-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
    }
}

Security Headers Explained #

  • X-Frame-Options: Prevents clickjacking attacks
  • X-Content-Type-Options: Prevents MIME-sniffing
  • X-XSS-Protection: Enables browser XSS filter
  • Referrer-Policy: Controls referrer information
  • Permissions-Policy: Restricts browser features
  • Strict-Transport-Security: Enforces HTTPS
  • Content-Security-Policy: Prevents XSS and data injection attacks

Note

The CSP allows Google Fonts and user-provided profile images (img-src https:). Adjust based on your security requirements.

Development #

# Run with hot reload
bun run dev

# Format code
bun run format

# Type check (handled by Bun)
bun run src/index.ts

ยฉ 2025-present Kieran Klukas