An Identity Provider implementing OAuth 2.0 Authorization Code with PKCE, OpenID Connect Discovery, JWT signing (RS256 with kid), session-based login, refresh token rotation, token revocation tracking, and JWKS publishing.
- Login form creates a session (
sentinel_session) and protects routes via middleware. /authorizeissues single-use authorization codes (PKCES256required)./tokenexchanges codes foraccess_token,id_token, andrefresh_token; supports refresh rotation./logoutrevokes the current access token (byjti) and clears cookies (CSRF protected)./.well-known/openid-configurationserves OIDC discovery./jwks.jsonserves JWKS for public key verification./revoked?jti=...checks if an access tokenjtihas been revoked.
- Read the blog post: https://medium.com/@goforsamyak.c/sentinel-an-identity-provider-with-auth-2-0-and-oidc-framework-0d091f574f3e
- Set up database and run migrations
createdb sentinel
psql -d sentinel -f migrations/001_init.sql
psql -d sentinel -f migrations/002_oauth.sql
psql -d sentinel -f migrations/003_revoke.sql
psql -d sentinel -f migrations/004_refresh_tokens.sql
psql -d sentinel -f migrations/005_rbac.sql
psql -d sentinel -f migrations/006_signing.sql- Generate an RSA signing key pair and insert into DB
# Generate 2048-bit RSA key pair
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
# Choose a key ID
KID="kid-1"
# Insert into the signing_keys table (adjust psql connection as needed)
psql -d sentinel <<SQL
INSERT INTO signing_keys (kid, private_key_pem, public_key_pem, active)
VALUES ('${KID}', '$(sed "s/'/''/g" private.pem)', '$(sed "s/'/''/g" public.pem)', true);
SQL- Create a user and OAuth client
Generate a bcrypt password hash (Go one-liner):
cat > /tmp/hash.go <<'GO'
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func main(){
h,_ := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)
fmt.Printf("%s\n", string(h))
}
GO
go run /tmp/hash.go > /tmp/pass.hashInsert user and client:
psql -d sentinel <<SQL
INSERT INTO users (username, password_hash) VALUES ('alice', '$(cat /tmp/pass.hash)');
INSERT INTO oauth_clients (client_id, redirect_uri) VALUES ('client-123', 'http://localhost:3000/callback');
SQL- Configure environment and run the server
export DATABASE_URL="postgres://localhost:5432/sentinel?sslmode=disable"
go run ./cmd/serverServer listens on :8080 with issuer http://localhost:8080.
- Login:
GET/POST /login→ servesweb/templates/login.html, setssentinel_sessioncookie. - Home:
GET /→ requires session, returns "Sentinel running". - Authorize:
GET /authorize→ requires session; params:client_id,redirect_uri,code_challenge,code_challenge_method=S256,state. - Token:
POST /token→grant_type=authorization_code|refresh_token. - Logout:
POST /logout→ CSRF protected; revokes currentsentinel_accessbyjti. - JWKS:
GET /jwks.json→ current public keys andkids. - OIDC Discovery:
GET /.well-known/openid-configuration→ metadata. Note: implementation returnsjwks_urias${issuer}/jwks, while the endpoint is/jwks.json. - Revocation Check:
GET /revoked?jti=...→ 200 if revoked, 404 otherwise.
- Login in browser at
https://localhost:8080/loginusing the seeded user. - Create PKCE values:
VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')
CHALLENGE=$(echo -n "$VERIFIER" | openssl dgst -binary -sha256 | openssl base64 -A | tr '+/' '-_' | tr -d '=')- Start authorization request (in browser, due to session requirement):
http://localhost:8080/authorize?client_id=client-123&redirect_uri=http://localhost:3000/callback&code_challenge=${CHALLENGE}&code_challenge_method=S256&state=xyz
- Exchange code for tokens:
curl -X POST http://localhost:8080/token \
-d grant_type=authorization_code \
-d client_id=client-123 \
-d code=RECEIVED_CODE \
-d code_verifier="${VERIFIER}"Response:
{
"access_token": "...",
"id_token": "...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_in": 900
}curl -X POST http://localhost:8080/token \
-d grant_type=refresh_token \
-d client_id=client-123 \
-d refresh_token=PREVIOUS_REFRESH_TOKENReturns a new access_token and rotated refresh_token. The previous refresh token is revoked.
- Requires a
csrf_tokencookie and matchingX-CSRF-Tokenheader.
curl -X POST http://localhost:8080/logout \
-H "X-CSRF-Token: $(cat /tmp/csrf)" \
--cookie "csrf_token=$(cat /tmp/csrf)"Clears sentinel_access cookie and records token jti in revoked_tokens.
- Discovery:
curl http://localhost:8080/.well-known/openid-configuration - JWKS:
curl http://localhost:8080/jwks.json
access_token and id_token use RS256 and include a kid. Verify signatures against the JWKS keys.
- RBAC tables (
roles,scopes,role_scopes) determinescopeclaim of access tokens. - Assign a
role_idto users and map role→scopes to influence issued token scopes.
- HTTPS: The session cookie uses
Secure: true. Serve via HTTPS locally or adjust cookie flags for development only. - PKCE: Only
S256is supported. - Issuer: Currently hardcoded to
http://localhost:8080incmd/server/main.go. - Keys: Active signing key must exist in
signing_keyswithactive=true. Keys are reloaded from DB every minute.
DATABASE_URL not set: export a proper Postgres DSN.no active signing key: insert at least one active RSA key insigning_keys.- Cannot stay logged in locally: ensure HTTPS or relax cookie
Secureflag in dev.
This repository is for learning and experimentation. Integrate responsibly and review security considerations before production use.