OAuth 2.0 / OpenID Connect identity provider built with Go and PostgreSQL.
- 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
- Client Integration Guide — How to integrate your application with this IdP, including user storage, roles, and permissions
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@latest
# golang-migrate/migrate for migrations
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latestThe project uses Tailwind CSS v4 for styling. This requires Node.js (v18+):
npm install # install Tailwind dependenciesThe CSS is built automatically when you run make build or make run. For development with live reload:
make css-watch # in a separate terminalCreate 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-
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). -
Run database migrations:
DATABASE_URL="..." make migrate-up -
Build the server and CLI:
make build
-
Start the server:
ENV=dev ./identity
The server will be available at
http://localhost:8080.
Users can register via the web UI:
http://localhost:8080/oauth/register
Or create users programmatically using the database directly.
Login page (standalone or via OAuth flow):
http://localhost:8080/login
Users can enable TOTP-based two-factor authentication from their account settings:
http://localhost:8080/oauth/account-settings
When MFA is enabled:
- User enters username/password as normal
- User is redirected to MFA verification page
- User enters 6-digit code from their authenticator app (Google Authenticator, Authy, etc.)
- On success, the OAuth flow continues as normal
MFA is off by default. Users can enable/disable it themselves via account settings.
Build binary:
make buildBuild docs:
make docsCreate a new migration:
# Use underscores in migration names
make migrate-new name=create_table_usersRun migrations:
DATABASE_URL="postgresql://..." make migrate-up
DATABASE_URL="postgresql://..." make migrate-downRegenerate sqlc queries and types.
make sqlc-
Build the CLI:
make build
-
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(andclient_secretif confidential).
./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
GET /oauth/authorize- Start OAuth authorization flow- Query params:
client_id,redirect_uri,state,scope,code_challenge,code_challenge_method
- Query params:
POST /oauth/token- Exchange authorization code for tokens- Form params:
grant_type,code,client_id,redirect_uri,code_verifier
- Form params:
POST /oauth/token- Refresh access token- Form params:
grant_type=refresh_token,refresh_token,client_id
- Form params:
GET /oauth/userinfo- Get user info from access token- Header:
Authorization: Bearer <access_token> - Returns:
sub,username,email,email_verified
- Header:
GET /login- Login page (standalone or via OAuth)POST /login- Process login credentialsGET /oauth/register- User registration pagePOST /oauth/register- Create new user accountGET /oauth/success- Post-login success page
GET /oauth/mfa- MFA code entry page (during login, if MFA enabled)POST /oauth/mfa- Validate MFA code and complete loginGET /oauth/mfa-setup- Setup MFA with QR code (from account settings)POST /oauth/mfa-setup- Verify setup code and enable MFAPOST /oauth/mfa-disable- Disable MFA (requires password + current MFA code)
GET /health- Health check endpointGET /docs- API documentation (Swagger/OpenAPI)
For a Single Page Application (SPA) using PKCE:
-
Generate PKCE values in your client:
const codeVerifier = generateRandomString(32); const codeChallenge = await sha256(codeVerifier);
-
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 -
User logs in and is redirected back with authorization code:
http://localhost:5173/oauth/callback? code=AUTHORIZATION_CODE& state=RANDOM_STATE -
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
-
Receive tokens:
{ "access_token": "...", "refresh_token": "...", "expires_in": 3600, "token_type": "Bearer", "scope": ["openid", "profile", "email"] } -
Use access token to call protected APIs:
GET /oauth/userinfo Authorization: Bearer ACCESS_TOKEN
Start the server in development mode:
ENV=dev make runWatch logs for debugging:
ENV=dev LOG_LEVEL=debug ./identity