Skip to content

Repository files navigation

Identity Provider

OAuth 2.0 / OpenID Connect identity provider built with Go and PostgreSQL.

Features

  • OAuth 2.0 Authorization Code flow with PKCE
  • OpenID Connect UserInfo endpoint
  • User registration and authentication
  • Two-factor authentication (TOTP) — optional MFA via authenticator apps
  • Client management via CLI
  • Session management with secure cookies
  • Token refresh and revocation

Documentation

  • Client Integration Guide — How to integrate your application with this IdP, including user storage, roles, and permissions

Prerequisites

This project requires a few CLI tools:

# swaggo/swag for generating openapi docs
go install github.com/swaggo/swag/cmd/swag@latest
# sqlc-dev/sqlc for generating type-safe database code
go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.30.0  # or just use `make sqlc`, which pins this
# golang-migrate/migrate for migrations
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# a-h/templ for the server-rendered HTML pages (pkg/views/*.templ)
go install github.com/a-h/templ/cmd/templ@latest

Templates (templ)

The browser-facing pages are templ components in pkg/views/*.templ. templ compiles each .templ into a committed *_templ.go file (so go build/CI don't need the templ binary). After editing any .templ file, run make templ (or templ generate) and commit the regenerated *_templ.go — CI fails if the committed output is stale. make build/make run regenerate automatically.

Tailwind CSS

The project uses Tailwind CSS v4 for styling. This requires Node.js (v18+):

npm install  # install Tailwind dependencies

The CSS is built automatically when you run make build or make run. For development with live reload:

make css-watch  # in a separate terminal

Environment Variables

Create a .env file (or .env.dev for development) in the project root:

# Database connection (required)
DATABASE_URL=postgresql://user:password@localhost:5432/identity?sslmode=disable

# HTTP server address (optional, defaults shown)
HTTP_ADDRESS=http://localhost:8080  # dev
# HTTP_ADDRESS=https://identity.example.com  # production

# Session configuration (optional)
SESSION_SECRET=your-random-secret-key-at-least-32-chars

Setup

  1. Set DATABASE_URL to a non-prod database in .env.dev(I usually copy the connection string for the dev branch of the main db in Neon, but you could also spin up local postgres).

  2. Run database migrations:

    DATABASE_URL="..." make migrate-up
  3. Build the server and CLI:

    make build
  4. Start the server:

    ENV=dev ./identity

    The server will be available at http://localhost:8080.

User Management

Creating Users

Users can register via the web UI:

http://localhost:8080/oauth/register

Or create users programmatically using the database directly.

User Login

Login page (standalone or via OAuth flow):

http://localhost:8080/login

Two-Factor Authentication (MFA)

Users can enable TOTP-based two-factor authentication from their account settings:

http://localhost:8080/oauth/account-settings

When MFA is enabled:

  1. User enters username/password as normal
  2. User is redirected to MFA verification page
  3. User enters 6-digit code from their authenticator app (Google Authenticator, Authy, etc.)
  4. On success, the OAuth flow continues as normal

MFA is off by default. Users can enable/disable it themselves via account settings.

Commands

Build binary:

make build

Build docs:

make docs

Create a new migration:

# Use underscores in migration names
make migrate-new name=create_table_users

Run migrations:

DATABASE_URL="postgresql://..." make migrate-up
DATABASE_URL="postgresql://..." make migrate-down

Regenerate sqlc queries and types.

make sqlc

pkg/db is generated from db/migrations and db/queries — the migrations are the schema, so this needs no database and no DATABASE_URL. The output is committed, and CI runs make sqlc-check to fail if it drifts, the same way it does for templ. The sqlc version is pinned in the Makefile, so run codegen through make sqlc rather than a bare sqlc generate: a different version regenerates the same code with a different banner, which CI reports as stale.

Migrations

Migrations live in db/migrations as {version}_{name}.{up,down}.sql and are applied with golang-migrate.

The server never migrates on startup. db/migrations/embed.go embeds the up migrations so the binary knows the schema version it was built for, and startup calls migrations.Verify, which refuses to start — naming the version mismatch — if the database is not at exactly that version. A forgotten make migrate-up fails fast instead of silently serving against a schema the code doesn't expect. Applying migrations is always a deliberate, separate step:

  • By hand, with the migrate CLI via the Makefile — the usual local and staging/prod path:
    DATABASE_URL="postgresql://..." make migrate-up
  • From the binary, with the migrate subcommand:
    DATABASE_URL="postgresql://..." ./identity migrate   # `auth-service migrate` in the image
    It applies every pending up migration and exits 0; a database already at the latest version is a success, not an error. The migrations are embedded, so this needs no .sql files on disk and no migrate CLI — just the binary and a database. It reads DATABASE_URL and nothing else, so the serving config (JWT_PRIVATE_KEY and friends) is irrelevant to it.

Running the binary with no arguments serves, exactly as it always has. The prod and staging Deployments (k8s/) run it bare and never invoke migrate, so a missed migration still fails closed there.

The subcommand exists for preview environments. A preview gets its own Neon database branch cut from staging's schema, so a branch that adds a migration would otherwise start against a schema that's behind and Verify would (correctly) refuse to start. bifrost's service registry runs a preview's migrations by injecting a migrate initContainer that runs the same image with the same env — so DATABASE_URL already points at that preview's own branch — to completion before the app container starts:

migrate: ["/app/auth-service", "migrate"]

OAuth Client Management

Adding a New Client

  1. Build the CLI:

    make build
  2. Create a public client (for SPAs like React apps):

    ENV=dev ./identity-cli client create \
      --name "My App (Dev)" \
      --redirect-uris "http://localhost:5173/oauth/callback" \
      --scopes "openid,profile,email"

    Or create a confidential client (for backend services):

    ENV=dev ./identity-cli client create \
      --name "My Service" \
      --redirect-uris "https://myservice.com/callback" \
      --scopes "openid,profile,email" \
      --confidential

    Save the returned client_id (and client_secret if confidential).

Other Client Commands

  • ./identity-cli client list - List all clients
  • ./identity-cli client get <client-id> - Get client details
  • ./identity-cli client update <client-id> --name "New Name" - Update client
  • ./identity-cli client delete <client-id> - Delete client

OAuth Endpoints

Authorization & Token Endpoints

  • GET /oauth/authorize - Start OAuth authorization flow
    • Query params: client_id, redirect_uri, state, scope, code_challenge, code_challenge_method
  • POST /oauth/token - Exchange authorization code for tokens
    • Form params: grant_type, code, client_id, redirect_uri, code_verifier
  • POST /oauth/token - Refresh access token
    • Form params: grant_type=refresh_token, refresh_token, client_id

User Info & Session

  • GET /oauth/userinfo - Get user info from access token
    • Header: Authorization: Bearer <access_token>
    • Returns: sub, username, email, email_verified
  • GET /login - Login page (standalone or via OAuth)
  • POST /login - Process login credentials
  • GET /oauth/register - User registration page
  • POST /oauth/register - Create new user account
  • GET /oauth/success - Post-login success page

MFA Endpoints

  • GET /oauth/mfa - MFA code entry page (during login, if MFA enabled)
  • POST /oauth/mfa - Validate MFA code and complete login
  • GET /oauth/mfa-setup - Setup MFA with QR code (from account settings)
  • POST /oauth/mfa-setup - Verify setup code and enable MFA
  • POST /oauth/mfa-disable - Disable MFA (requires password + current MFA code)

Health & Documentation

  • GET /health - Health check endpoint
  • GET /docs - API documentation (Swagger/OpenAPI)

OAuth Flow Example

For a Single Page Application (SPA) using PKCE:

  1. Generate PKCE values in your client:

    const codeVerifier = generateRandomString(32);
    const codeChallenge = await sha256(codeVerifier);
  2. Redirect to authorization endpoint:

    http://localhost:8080/oauth/authorize?
      client_id=YOUR_CLIENT_ID&
      redirect_uri=http://localhost:5173/oauth/callback&
      response_type=code&
      state=RANDOM_STATE&
      scope=openid profile email&
      code_challenge=CODE_CHALLENGE&
      code_challenge_method=S256
    
  3. User logs in and is redirected back with authorization code:

    http://localhost:5173/oauth/callback?
      code=AUTHORIZATION_CODE&
      state=RANDOM_STATE
    
  4. Exchange code for tokens:

    POST /oauth/token
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=authorization_code&
    code=AUTHORIZATION_CODE&
    client_id=YOUR_CLIENT_ID&
    redirect_uri=http://localhost:5173/oauth/callback&
    code_verifier=CODE_VERIFIER
  5. Receive tokens:

    {
      "access_token": "...",
      "refresh_token": "...",
      "expires_in": 3600,
      "token_type": "Bearer",
      "scope": ["openid", "profile", "email"]
    }
  6. Use access token to call protected APIs:

    GET /oauth/userinfo
    Authorization: Bearer ACCESS_TOKEN

Development

Start the server in development mode:

ENV=dev make run

Watch logs for debugging:

ENV=dev LOG_LEVEL=debug ./identity

About

A little IDP

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages