No description
| cmd/api | ||
| docs | ||
| internal | ||
| migrations/postgres | ||
| pkg | ||
| .gitignore | ||
| docker-compose.yaml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| my_code_rules.md | ||
| openapi.json | ||
| README.md | ||
| sqlc.yaml | ||
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=nonesupport,noncePOST /public/v1/token—application/x-www-form-urlencoded; grants:authorization_code(+PKCE),refresh_token,client_credentials; client auth via Basic orclient_id/client_secretform 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_verifiedisfalsefor 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_tokenis not issued onrefresh_tokengrants (spec-optional).- Ephemeral signing key unless
IDP_JWT_PRIVATE_KEY_PEMis set (see config). /private/v1has 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 ./...