No description
Find a file
2026-08-31 19:59:57 +04:00
cmd/api init 2026-08-31 19:59:57 +04:00
docs init 2026-08-31 19:59:57 +04:00
internal init 2026-08-31 19:59:57 +04:00
migrations/postgres init 2026-08-31 19:59:57 +04:00
pkg init 2026-08-31 19:59:57 +04:00
.gitignore init with gitignore 2026-08-31 19:56:44 +04:00
docker-compose.yaml init 2026-08-31 19:59:57 +04:00
Dockerfile init 2026-08-31 19:59:57 +04:00
go.mod init 2026-08-31 19:59:57 +04:00
go.sum init 2026-08-31 19:59:57 +04:00
main.go init 2026-08-31 19:59:57 +04:00
my_code_rules.md init 2026-08-31 19:59:57 +04:00
openapi.json init 2026-08-31 19:59:57 +04:00
README.md init 2026-08-31 19:59:57 +04:00
sqlc.yaml init 2026-08-31 19:59:57 +04:00

my_id_provider

Self-hosted OIDC identity provider (OpenID Connect + OAuth2), built with github.com/P4elme6ka/gen-router and PostgreSQL.

Quick start

docker compose up -d --build

The API listens on 127.0.0.1:8080. Migrations are applied automatically at startup.

Or run natively:

DATABASE_URL="postgres://pelmen:pelmen@127.0.0.1:5532/postgres" go run ./cmd/api

Configuration (env vars)

Var Default Description
DATABASE_URL — (required) PostgreSQL connection string
PORT 8080 Listen port
IDP_ISSUER http://localhost:8080/public/v1 OIDC issuer — iss claim and discovery base. Must be the publicly reachable URL; keep the /public/v1 suffix
IDP_LOGIN_URL http://localhost:8080/public/v1/login Where unauthenticated browsers are redirected from /authorize (your login page)
IDP_COOKIE_SECURE false Sets Secure on the session cookie — enable when serving over HTTPS
IDP_JWT_PRIVATE_KEY_PEM — (ephemeral) PKCS#8 Ed25519 private key. Without it, keys rotate on every restart and all tokens are invalidated. Generate: openssl genpkey -algorithm ed25519 -out key.pem and pass the PEM contents

API surfaces

Surface Auth Purpose
/public/v1/ none Register/login/password flows, OIDC authorize/token, discovery, JWKS
/user/v1/ Bearer access token User self-service (profile, sessions, identities, password)
/private/v1/ none (internal network) Users/providers/identities management, OAuth2 client registration
/service/v1/ Bearer (userinfo) Introspection, OIDC userinfo

OIDC endpoints

  • GET /public/v1/.well-known/openid-configuration — discovery (also reachable at {IDP_ISSUER}/.well-known/openid-configuration)
  • GET /public/v1/.well-known/jwks.json — Ed25519 signing key (OKP JWK)
  • GET /public/v1/authorize — authorization code flow with PKCE (S256/plain), prompt=none support, nonce
  • POST /public/v1/token — application/x-www-form-urlencoded; grants: authorization_code (+PKCE), refresh_token, client_credentials; client auth via Basic or client_id/client_secret form params. Not in openapi.json — this endpoint is form-encoded and bypasses the JSON router.
  • GET /service/v1/userinfo — Authorization: Bearer <access_token>; claims gated by granted scopes (email → email, email_verified; profile → name)

Creating an OAuth2 client

curl -s -X POST http://localhost:8080/private/v1/clients \
  -H 'Content-Type: application/json' \
  -d '{"name":"grafana","redirect_uris":["http://localhost:3000/login/generic_oauth"],"scopes":["openid","profile","email"]}'

The response contains client_secret in plaintext — exactly once. Store it.

End-to-end flow (curl)

# 1. Register + login (the response sets the idp_session cookie)
curl -s -c cookies.txt -X POST http://localhost:8080/public/v1/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"alice@example.com","password":"correct-horse"}'

# 2. Authorize (browser: with the session cookie, redirected back with a code)
VERIFIER="dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '=')
curl -s -b cookies.txt "http://localhost:8080/public/v1/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=openid+profile+email&state=st&code_challenge=$CHALLENGE&code_challenge_method=S256" \
  | grep -o 'code=[^&]*'

# 3. Exchange the code
curl -s -X POST http://localhost:8080/public/v1/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=CODE_FROM_STEP_2" \
  --data-urlencode "redirect_uri=REDIRECT_URI" \
  --data-urlencode "code_verifier=$VERIFIER" \
  --data-urlencode "client_id=CLIENT_ID"

# 4. Use the access token
curl -s http://localhost:8080/service/v1/userinfo -H "Authorization: Bearer ACCESS_TOKEN"

Browser login flow

/public/v1/login is a JSON API (no HTML). Point IDP_LOGIN_URL at a page you host that renders a login form, POSTs credentials to /public/v1/login, stores the returned tokens and lets the browser keep the idp_session cookie, then navigates back to return_to. Consent is auto-approved for all clients (no consent screen).

Current limitations

  • email_verified is false for new users — registration doesn't generate a verification token yet (no mailer). Clients that refuse unverified emails (e.g. Grafana) need this fixed or verified manually in the DB.
  • No consent screen — first-party clients only.
  • id_token is not issued on refresh_token grants (spec-optional).
  • Ephemeral signing key unless IDP_JWT_PRIVATE_KEY_PEM is set (see config).
  • /private/v1 has no auth — keep it off public networks (or front it with your reverse proxy's auth).
  • Rate limiting / brute-force protection not implemented yet.

Documentation

  • Architecture — overall architecture, layers, data model, project structure
  • API Design — four API surfaces, OAuth2/OIDC flows, endpoint specifications
  • Credential & Secret Storage — password hashing, token signing, merge handling
  • Roadmap — phased implementation plan with task breakdown

Tests

go test ./...