oauth

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 16, 2026 License: MIT Imports: 23 Imported by: 0

README

kite-mcp-oauth

Go Reference

OAuth 2.0 + JWT session + dynamic client registration + Google SSO + admin MFA TOTP for the algo2go ecosystem. Bundles authorize/token/ callback/registration handlers, RequireAuth middleware with token- expiry detection, JWT issuance/verification, persistent ClientStore (AES-256-GCM encrypted client_secrets), Google SSO callback flow, and admin MFA TOTP enrolment + verification.

Used by Sundeepg98/kite-mcp-server for MCP client OAuth, dashboard SSO, admin RBAC gating, and session lifecycle management.

Why a separate module?

OAuth 2.0 + JWT + dynamic client registration is a substantial authentication surface (~16K LOC). Hosting as a module:

  • Centralizes the authentication contract across consumers
  • Lets OAuth flow + JWT signature + ClientStore version independently of business logic
  • Pairs cleanly with algo2go/kite-mcp-templates (callback HTML) and algo2go/kite-mcp-users (admin store + MFA backend) for the full identity stack

Stability promise

v0.x — unstable. Type signatures and OAuth flow specifics may evolve as MCP-Remote spec patterns mature. Pin v0.1.0 deliberately. v1.0 ships only after the public API (handlers, middleware, JWT config, ClientStore methods) is reviewed for stability and at least one external consumer ships against it.

Install

go get github.com/algo2go/kite-mcp-oauth@v0.1.0

Public API (selected)

Handlers
  • NewHandler(...) — composes the full OAuth handler set
  • Authorize / Token / Callback / Registration HTTP handlers
  • Browser login + admin MFA enrolment routes
Middleware
  • RequireAuth(...) — gates routes with JWT + token-expiry detection
  • Returns 401 for unauthenticated AND expired Kite tokens (forces seamless re-auth via mcp-remote)
JWT
  • JWTConfig — config struct (24h MCP bearer, 7d dashboard cookie)
  • IssueJWT(email) string / VerifyJWT(token) (Session, error)
ClientStore
  • ClientStore — persistent OAuth client_id → encrypted client_secret registry (AES-256-GCM via HKDF from OAUTH_JWT_SECRET)
  • RegisterClient(...) / LookupClient(client_id) (Client, error)
Google SSO
  • Google SSO callback flow with userinfo + admin role injection
Admin MFA
  • TOTP enrolment + verification (admin-only per kc/users role gate)

Dependencies

  • github.com/algo2go/kite-mcp-templates v0.1.0 — callback HTML
  • github.com/algo2go/kite-mcp-users v0.1.0 — admin store + MFA backend
  • github.com/algo2go/kite-mcp-alerts v0.1.0 (indirect via users)
  • github.com/algo2go/kite-mcp-broker, kite-mcp-domain, kite-mcp-isttz, kite-mcp-logger, kite-mcp-money (indirect via deeper transitive)
  • github.com/golang-jwt/jwt/v5 — JWT signing
  • golang.org/x/oauth2 — Google SSO flow
  • golang.org/x/crypto — HKDF + AES-GCM
  • github.com/zerodha/gokiteconnect/v4 — Kite token validation
  • modernc.org/sqlite — ClientStore backend

All algo2go deps are published modules; no upstream replace directives needed.

Reference consumer

Sundeepg98/kite-mcp-server — consumed by 100+ files including app/, kc/audit, kc/billing, kc/ops, kc/papertrading, kc/riskguard, mcp/admin, mcp/middleware, plugins/rolegate, plugins/telegramnotify.

License

MIT — see LICENSE.

Authors

Original design: Sundeepg98 (Zerodha Tech). Multi-module promotion (2026-05-10): algo2go contributors.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrAuthCodeStoreFull = errors.New("auth code store is full")

ErrAuthCodeStoreFull is returned when the auth code store has reached its capacity.

Functions

func ContextWithEmail

func ContextWithEmail(ctx context.Context, email string) context.Context

ContextWithEmail returns a new context with the given email set. Useful for testing handlers that depend on EmailFromContext.

func EmailFromContext

func EmailFromContext(ctx context.Context) string

EmailFromContext extracts the authenticated email from the request context.

Types

type AdminUserStore

type AdminUserStore interface {
	GetRole(email string) string
	GetStatus(email string) string
	VerifyPassword(email, password string) (bool, error)
	// EnsureGoogleUser auto-creates a trader account on first Google SSO login.
	// Existing users are left unchanged (admins keep admin role).
	EnsureGoogleUser(email string)
	// HasTOTP reports whether the given user has an enrolled TOTP secret.
	// Used by the MFA gate to decide between enrollment vs verification.
	HasTOTP(email string) bool
	// SetTOTPSecret persists a TOTP secret for the user (encrypted at rest).
	// Returns error if the user is not admin or no encryption key is wired.
	SetTOTPSecret(email, plaintextSecret string) error
	// VerifyTOTP checks the supplied 6-digit code against the stored secret.
	// Returns (false, nil) for any non-match (wrong code, not enrolled).
	VerifyTOTP(email, code string) (bool, error)
	// ClearTOTPSecret removes a user's TOTP enrollment (admin recovery flow).
	ClearTOTPSecret(email string) error
}

AdminUserStore provides user lookup, password verification, and auto-provisioning for login. Implemented by users.Store to avoid direct import of the users package.

MFA methods (HasTOTP/SetTOTPSecret/VerifyTOTP/ClearTOTPSecret) gate admin actions per docs/access-control.md §8. Implementations must reject SetTOTPSecret on non-admin users (defence in depth).

type AuthCodeEntry

type AuthCodeEntry struct {
	ClientID      string
	CodeChallenge string // S256-hashed code_challenge from the client
	RedirectURI   string
	Email         string // set at callback for normal flow (global credentials)
	RequestToken  string // set at callback for deferred exchange (per-user Kite credentials)
	ExpiresAt     time.Time
}

AuthCodeEntry stores data associated with an issued authorization code.

type AuthCodeStore

type AuthCodeStore struct {
	// contains filtered or unexported fields
}

AuthCodeStore is a thread-safe in-memory store for OAuth authorization codes.

func NewAuthCodeStore

func NewAuthCodeStore() *AuthCodeStore

NewAuthCodeStore creates a store and starts a background cleanup goroutine.

func (*AuthCodeStore) Close

func (s *AuthCodeStore) Close()

Close stops the background cleanup goroutine. Safe to call multiple times.

func (*AuthCodeStore) Consume

func (s *AuthCodeStore) Consume(code string) (*AuthCodeEntry, bool)

Consume retrieves and deletes an authorization code (one-time use).

func (*AuthCodeStore) Generate

func (s *AuthCodeStore) Generate(entry *AuthCodeEntry) (string, error)

Generate creates a new random authorization code and stores the entry.

type Claims

type Claims struct {
	jwt.RegisteredClaims
}

Claims represents the JWT claims for an MCP access token.

type ClientEntry

type ClientEntry struct {
	ClientSecret string
	RedirectURIs []string
	ClientName   string
	CreatedAt    time.Time
	IsKiteAPIKey bool // true if client_id is a user's Kite API key (per-user credentials)
}

ClientEntry stores a dynamically registered client.

type ClientLoadEntry

type ClientLoadEntry struct {
	ClientID     string
	ClientSecret string
	RedirectURIs string // JSON-encoded []string
	ClientName   string
	CreatedAt    time.Time
	IsKiteAPIKey bool
}

ClientLoadEntry represents a client loaded from persistence.

type ClientPersister

type ClientPersister interface {
	SaveClient(clientID, clientSecret, redirectURIsJSON, clientName string, createdAt time.Time, isKiteKey bool) error
	LoadClients() ([]*ClientLoadEntry, error)
	DeleteClient(clientID string) error
}

ClientPersister provides optional persistence for OAuth clients.

type ClientStore

type ClientStore struct {
	// contains filtered or unexported fields
}

ClientStore is a thread-safe in-memory store for dynamically registered OAuth clients. Optionally backed by a ClientPersister for persistence via SetPersister.

func NewClientStore

func NewClientStore() *ClientStore

NewClientStore creates a new client store.

func (*ClientStore) AddRedirectURI

func (s *ClientStore) AddRedirectURI(clientID, uri string)

AddRedirectURI adds a redirect URI to an existing client if not already present. Capped at maxRedirectURIs per client to prevent abuse.

func (*ClientStore) Get

func (s *ClientStore) Get(clientID string) (*ClientEntry, bool)

Get retrieves a client by ID. Returns a copy to prevent callers from mutating shared state.

func (*ClientStore) IsKiteClient

func (s *ClientStore) IsKiteClient(clientID string) bool

IsKiteClient returns true if the client_id was registered as a Kite API key client.

func (*ClientStore) LoadFromDB

func (s *ClientStore) LoadFromDB() error

LoadFromDB populates the in-memory store from the persister.

func (*ClientStore) Register

func (s *ClientStore) Register(redirectURIs []string, clientName string) (clientID, clientSecret string, err error)

Register creates a new client with a random ID and secret.

func (*ClientStore) RegisterKiteClient

func (s *ClientStore) RegisterKiteClient(clientID string, redirectURIs []string)

RegisterKiteClient auto-registers a client where client_id is a Kite API key. No client_secret is stored — validation happens via Kite's GenerateSession at token exchange.

func (*ClientStore) SetLogger

func (s *ClientStore) SetLogger(logger *slog.Logger)

SetLogger sets the logger for persistence error reporting.

func (*ClientStore) SetPersister

func (s *ClientStore) SetPersister(p ClientPersister)

SetPersister enables write-through persistence for OAuth clients.

func (*ClientStore) ValidateRedirectURI

func (s *ClientStore) ValidateRedirectURI(clientID, uri string) bool

ValidateRedirectURI checks if the URI is registered for the client.

type Config

type Config struct {
	KiteAPIKey  string // Kite API key for generating login URLs (optional: per-user credentials via oauth_client_id)
	JWTSecret   string
	ExternalURL string // e.g. https://kite-mcp-server.fly.dev
	TokenExpiry time.Duration
	Logger      *slog.Logger
}

Config holds OAuth 2.1 configuration.

func (*Config) Validate

func (c *Config) Validate() error

Validate checks that all required fields are set.

type ConsentRecorder

type ConsentRecorder func(email, ipAddress, userAgent string)

ConsentRecorder captures the DPDP Act 2023 consent-grant event that occurs when a user completes the Kite OAuth flow — at callback time we know the email (via Kite token exchange) plus the IP/UA/timestamp, which is the minimum record the Data Protection Board may request in an audit.

Implementations must be non-blocking and best-effort: the OAuth callback should not fail just because consent logging is unavailable. Implementations are expected to swallow or log their own errors — the callback ignores the return value.

type GoogleSSOConfig

type GoogleSSOConfig struct {
	ClientID     string
	ClientSecret string
	RedirectURL  string
	Endpoint     string // Optional: override token endpoint URL (for testing). Empty = Google default.
	UserInfoURL  string // Optional: override userinfo URL (for testing). Empty = Google default.
}

GoogleSSOConfig holds configuration for Google OAuth SSO.

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler implements all OAuth 2.1 HTTP endpoints.

Endpoint handlers are grouped by flow across sibling files:

  • handlers.go core wiring (Handler struct, NewHandler, setters, metadata, Register, helpers)
  • handlers_oauth.go OAuth 2.1 Authorize / email prompt / Token
  • handlers_callback.go Kite OAuth callback (HandleKiteOAuthCallback)
  • handlers_browser.go browser login flow (HandleBrowserLogin / HandleBrowserAuthCallback / HandleLoginChoice)
  • handlers_admin.go admin password login (HandleAdminLogin)

func NewHandler

func NewHandler(cfg *Config, signer Signer, exchanger KiteExchanger) *Handler

NewHandler creates a new OAuth handler. Config must be validated first.

func (*Handler) AuthServerMetadata

func (h *Handler) AuthServerMetadata(w http.ResponseWriter, r *http.Request)

AuthServerMetadata serves RFC 8414 OAuth Authorization Server Metadata.

func (*Handler) Authorize

func (h *Handler) Authorize(w http.ResponseWriter, r *http.Request)

Authorize handles GET /oauth/authorize — validates params and redirects to Kite login.

func (*Handler) Close

func (h *Handler) Close()

Close releases resources held by the handler (e.g., background goroutines).

func (*Handler) GenerateBrowserLoginURL

func (h *Handler) GenerateBrowserLoginURL(apiKey, email, redirect string) string

GenerateBrowserLoginURL generates a Kite login URL for browser-based auth. The email and redirect path are signed together and passed through as redirect_params, so the callback can look up per-user credentials for the token exchange.

func (*Handler) GoogleSSOEnabled

func (h *Handler) GoogleSSOEnabled() bool

GoogleSSOEnabled returns true if Google SSO is configured.

func (*Handler) HandleAdminLogin

func (h *Handler) HandleAdminLogin(w http.ResponseWriter, r *http.Request)

HandleAdminLogin serves and processes the admin password login form. GET: renders admin_login.html with CSRF token. POST: validates CSRF, checks admin role + active status + bcrypt password. On success: sets auth cookie and redirects to /admin/ops.

func (*Handler) HandleAdminMFAEnroll

func (h *Handler) HandleAdminMFAEnroll(w http.ResponseWriter, r *http.Request)

HandleAdminMFAEnroll handles enrollment.

GET — fresh flow: generate a new TOTP secret, render the enrollment form (manual-entry secret + otpauth:// link). POST — verify the supplied code matches the freshly-generated secret; if so, persist the secret on the user store and mint the MFA cookie.

The secret is round-tripped through a hidden form field rather than stored server-side at GET time. Only the SUCCESSFUL POST persists. This avoids retaining a half-enrolled secret if the user abandons the flow.

func (*Handler) HandleAdminMFAVerify

func (h *Handler) HandleAdminMFAVerify(w http.ResponseWriter, r *http.Request)

HandleAdminMFAVerify handles ongoing verification (after enrollment).

GET — render the 6-digit code form. If the user hasn't enrolled yet, redirect to /enroll. POST — verify the code against the stored secret. On success, mint the MFA cookie.

func (*Handler) HandleBrowserAuthCallback

func (h *Handler) HandleBrowserAuthCallback(w http.ResponseWriter, r *http.Request, requestToken string)

HandleBrowserAuthCallback handles the Kite callback for browser login flow. Called when flow=browser in the callback query params. Sets a JWT cookie and redirects to the target page (e.g. /admin/ops).

func (*Handler) HandleBrowserLogin

func (h *Handler) HandleBrowserLogin(w http.ResponseWriter, r *http.Request)

HandleBrowserLogin serves a login form or redirects to Kite login for browser-based auth. If an email query param is provided, looks up stored credentials and redirects to Kite. Otherwise, serves a login form where the user enters their email. CSRF protection: GET sets a random token as an HttpOnly cookie and hidden form field; POST verifies the cookie matches the form value.

func (*Handler) HandleEmailLookup

func (h *Handler) HandleEmailLookup(w http.ResponseWriter, r *http.Request)

HandleEmailLookup handles POST /oauth/email-lookup — looks up registry for user's email, then redirects to Kite login with the registered app's API key.

func (*Handler) HandleGoogleCallback

func (h *Handler) HandleGoogleCallback(w http.ResponseWriter, r *http.Request)

HandleGoogleCallback handles the OAuth callback from Google.

func (*Handler) HandleGoogleLogin

func (h *Handler) HandleGoogleLogin(w http.ResponseWriter, r *http.Request)

HandleGoogleLogin redirects the user to the Google consent screen.

func (*Handler) HandleKiteOAuthCallback

func (h *Handler) HandleKiteOAuthCallback(w http.ResponseWriter, r *http.Request, requestToken string)

HandleKiteOAuthCallback handles the Kite callback for MCP OAuth flow. Called when flow=oauth in the callback query params.

func (*Handler) HandleLoginChoice

func (h *Handler) HandleLoginChoice(w http.ResponseWriter, r *http.Request)

HandleLoginChoice serves a unified login page offering Kite login and admin login. If the user already has a valid dashboard cookie, redirects to the dashboard.

func (*Handler) JWTManager

func (h *Handler) JWTManager() *JWTManager

JWTManager returns the JWT manager for external use.

func (*Handler) LoadClientsFromDB

func (h *Handler) LoadClientsFromDB() error

LoadClientsFromDB loads persisted OAuth clients into the in-memory store.

func (*Handler) MintAdminMFAToken

func (h *Handler) MintAdminMFAToken(email string) (string, error)

MintAdminMFAToken issues a short-lived JWT marking the user's MFA gate as passed. Used by both enroll and verify success paths and by tests. Exposed (not unexported) because the dashboard middleware occasionally needs to mint via a different code path.

func (*Handler) Register

func (h *Handler) Register(w http.ResponseWriter, r *http.Request)

Register handles POST /oauth/register.

func (*Handler) RequireAdminMFA

func (h *Handler) RequireAdminMFA(next http.Handler) http.Handler

RequireAdminMFA is the middleware that gates admin paths. Preconditions: the upstream admin auth middleware has already set email-in-ctx (via ContextWithEmail). RequireAdminMFA checks:

  • email present (else 401 — defensive; should never trip in prod)
  • kite_admin_mfa cookie present and valid for THIS email

If the cookie is missing/invalid, the user is redirected to /auth/admin-mfa/enroll (un-enrolled) or /verify (enrolled). The original path is preserved as the redirect target so they land back where they were after the gate clears.

func (*Handler) RequireAuth

func (h *Handler) RequireAuth(next http.Handler) http.Handler

RequireAuth returns middleware that validates Bearer JWT tokens on requests. Unauthenticated requests get a 401 with WWW-Authenticate pointing to the resource metadata.

func (*Handler) RequireAuthBrowser

func (h *Handler) RequireAuthBrowser(next http.Handler) http.Handler

RequireAuthBrowser checks for a valid JWT cookie (audience=dashboard). Unlike RequireAuth, this does NOT check Kite token expiry — the browser auth flow has its own re-auth path via HandleBrowserAuthCallback.

RequireAuthBrowser returns middleware for browser-based auth. Tries Bearer token first, then falls back to a JWT cookie. If neither is valid, redirects to the Kite login flow.

func (*Handler) ResourceMetadata

func (h *Handler) ResourceMetadata(w http.ResponseWriter, r *http.Request)

ResourceMetadata serves RFC 9728 OAuth Protected Resource Metadata.

func (*Handler) SetAuthCookie

func (h *Handler) SetAuthCookie(w http.ResponseWriter, email string) error

SetAuthCookie sets a JWT cookie for browser-based dashboard auth.

func (*Handler) SetClientPersister

func (h *Handler) SetClientPersister(p ClientPersister, logger *slog.Logger)

SetClientPersister enables persistence for the OAuth client store.

func (*Handler) SetConsentRecorder

func (h *Handler) SetConsentRecorder(rec ConsentRecorder)

SetConsentRecorder registers a callback that persists a DPDP Act 2023 consent-grant event. The Handler invokes it once per successful OAuth callback, after the email is known. When nil (default), consent recording is disabled — appropriate for DevMode or clients that don't need the audit trail. Production deployments must wire this to a durable store.

func (*Handler) SetGoogleSSO

func (h *Handler) SetGoogleSSO(cfg *GoogleSSOConfig)

SetGoogleSSO enables Google SSO for admin login.

func (*Handler) SetHTTPClient

func (h *Handler) SetHTTPClient(c *http.Client)

SetHTTPClient sets a custom HTTP client for Google OAuth operations (token exchange + userinfo). When nil (default), the standard library clients are used.

func (*Handler) SetKiteTokenChecker

func (h *Handler) SetKiteTokenChecker(checker KiteTokenChecker)

SetKiteTokenChecker registers a callback that checks Kite token validity. When set, RequireAuth returns 401 if the Kite token has expired, forcing mcp-remote to re-authenticate (which includes a fresh Kite login).

func (*Handler) SetRegistry

func (h *Handler) SetRegistry(r KeyRegistry)

SetRegistry sets the key registry for zero-config onboarding. When set, generic OAuth clients can be matched to Kite apps by email.

func (*Handler) SetUserStore

func (h *Handler) SetUserStore(store AdminUserStore)

SetUserStore sets the user store for admin password-based login.

func (*Handler) Token

func (h *Handler) Token(w http.ResponseWriter, r *http.Request)

Token handles POST /oauth/token — exchanges auth code + PKCE verifier for JWT.

type JWTManager

type JWTManager struct {
	// contains filtered or unexported fields
}

JWTManager handles token generation and validation.

Dual-key rotation: secret signs new tokens; previousSecret is accepted during verification only. To rotate without invalidating live sessions:

  1. Set OAUTH_JWT_SECRET_PREVIOUS = current secret
  2. Set OAUTH_JWT_SECRET = freshly generated new secret
  3. Restart. Existing JWTs (signed with the now-previous key) verify via the fallback. New tokens sign with the new key.
  4. After all live JWTs have expired (default 24h MCP / 7d dashboard), unset OAUTH_JWT_SECRET_PREVIOUS. The rotation is complete.

previousSecret may be empty — in which case there is no fallback and rotation = invalidation, the pre-PR-DR behaviour.

func NewJWTManager

func NewJWTManager(secret string, expiry time.Duration) *JWTManager

NewJWTManager creates a new JWT manager. previousSecret is optional — pass empty string when no rotation is in progress.

func (*JWTManager) GenerateToken

func (j *JWTManager) GenerateToken(email, clientID string) (string, error)

GenerateToken creates a signed JWT for the given email and client.

func (*JWTManager) GenerateTokenWithExpiry

func (j *JWTManager) GenerateTokenWithExpiry(email, clientID string, expiry time.Duration) (string, error)

GenerateTokenWithExpiry creates a signed JWT with a custom expiry duration.

func (*JWTManager) SetPreviousSecret

func (j *JWTManager) SetPreviousSecret(previous string)

SetPreviousSecret installs a second-chance verify key for graceful rotation. Tokens signed with this key validate; new tokens still sign with the primary secret. Pass empty to clear the rotation slot once the migration window has elapsed.

func (*JWTManager) ValidateToken

func (j *JWTManager) ValidateToken(tokenString string, audiences ...string) (*Claims, error)

ValidateToken parses and validates the JWT, returning claims if valid. If audiences are provided, validates that the token's audience matches at least one.

Dual-key behaviour: tries the primary secret first. On signature failure, retries with previousSecret if set. Lets graceful rotations keep live sessions alive across a secret swap. Verifications that succeed via the previous key still pass — there is no warning header or claim flag, since reading already-issued tokens is precisely what the fallback exists for.

type KeyRegistry

type KeyRegistry interface {
	HasEntries() bool
	GetByEmail(email string) (*RegistryEntry, bool)
	GetSecretByAPIKey(apiKey string) (apiSecret string, ok bool)
}

KeyRegistry provides access to the pre-registered Kite app credentials. Implemented by registry.Store to avoid direct import.

type KiteExchanger

type KiteExchanger interface {
	ExchangeRequestToken(requestToken string) (email string, err error)
	ExchangeWithCredentials(requestToken, apiKey, apiSecret string) (email string, err error)
	GetCredentials(email string) (apiKey, apiSecret string, ok bool)
	GetSecretByAPIKey(apiKey string) (apiSecret string, ok bool)
}

KiteExchanger exchanges a Kite request_token for user identity and caches the access token.

type KiteTokenChecker

type KiteTokenChecker func(email string) bool

KiteTokenChecker checks whether the Kite trading token for a given email is still valid. Returns true if the token is valid (or no token cached yet), false if expired.

type RegistryEntry

type RegistryEntry struct {
	APIKey       string
	APISecret    string
	RegisteredBy string // admin email who registered the app
}

RegistryEntry is a thin projection of a pre-registered Kite app, used inside the oauth package to avoid importing the registry package directly.

type Signer

type Signer interface {
	Sign(data string) string
	Verify(signed string) (string, error)
}

Signer signs and verifies arbitrary strings (implemented by kc.SessionSigner via adapter).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL