oneauth

package module
v0.0.37 Latest Latest
Warning

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

Go to latest
Published: Mar 17, 2026 License: Apache-2.0 Imports: 30 Imported by: 3

README

OneAuth

A Go authentication library providing unified local and OAuth-based authentication with support for multiple authentication methods per user account.

Features

  • Unified authentication — Password and OAuth through a single interface
  • Multi-provider support — One account accessible via password, Google, GitHub, etc.
  • API authentication — JWT access tokens, refresh tokens, API keys
  • Multi-tenant JWT — KeyStore interface for per-client signing keys with algorithm confusion prevention
  • Federated auth — App registration, resource-scoped token minting, custom claims
  • Flexible storage — File-based, GORM (SQL), and GAE/Datastore implementations
  • Client SDK — Token management, auto-refresh, credential persistence for CLI tools
  • gRPC support — Auth context utilities and interceptors

Quick Start

go get github.com/panyam/oneauth
import (
    "github.com/panyam/oneauth"
    "github.com/panyam/oneauth/stores/fs"
)

// Initialize stores
storagePath := "/path/to/storage"
userStore := fs.NewFSUserStore(storagePath)
identityStore := fs.NewFSIdentityStore(storagePath)
channelStore := fs.NewFSChannelStore(storagePath)
tokenStore := fs.NewFSTokenStore(storagePath)

// Create authentication callbacks
createUser := oneauth.NewCreateUserFunc(userStore, identityStore, channelStore)
validateCreds := oneauth.NewCredentialsValidator(identityStore, channelStore, userStore)

// Configure local authentication
localAuth := &oneauth.LocalAuth{
    CreateUser:          createUser,
    ValidateCredentials: validateCreds,
    EmailSender:         &oneauth.ConsoleEmailSender{},
    TokenStore:          tokenStore,
    BaseURL:             "https://yourapp.com",
    HandleUser:          yourSessionHandler,
}

// Set up HTTP routes
mux := http.NewServeMux()
mux.Handle("/auth/login", localAuth)
mux.Handle("/auth/signup", http.HandlerFunc(localAuth.HandleSignup))

See Getting Started for the full setup guide.

Architecture

OneAuth separates authentication into three layers:

User: john@example.com
├── Identity: john@example.com (verified)
├── Channel: local    (password hash)
├── Channel: google   (OAuth tokens)
└── Channel: github   (OAuth tokens)
  • User — A unique account with profile information
  • Identity — An email or phone number with verification status (shared across channels)
  • Channel — An authentication mechanism storing provider-specific credentials

Verifying an email through any channel (e.g., Google OAuth) verifies it for all channels.

See Architecture for design decisions, data model diagrams, and token lifecycle.

API Authentication

Protect API endpoints with JWT middleware:

middleware := &oneauth.APIMiddleware{
    KeyStore:        keyStore,        // multi-tenant (per-client keys)
    TokenQueryParam: "token",         // ?token= fallback for WebSocket clients
}

mux.Handle("/api/data", middleware.ValidateToken(handler))
mux.Handle("/api/write", middleware.RequireScopes("write")(handler))
mux.Handle("/api/public", middleware.Optional(handler))

// Access claims in handlers
userID := oneauth.GetUserIDFromAPIContext(r.Context())
custom := oneauth.GetCustomClaimsFromContext(r.Context())

See API Authentication for JWT lifecycle, custom claims, multi-tenant validation, and middleware configuration.

Federated Auth (App Registration)

For systems where multiple applications register and mint scoped JWTs:

// Resource server validates tokens from any registered app
keyStore := gorm.NewGORMKeyStore(db)
middleware := &oneauth.APIMiddleware{KeyStore: keyStore}

// Apps register via admin API and mint tokens for their users
token, err := oneauth.MintResourceToken(clientID, clientSecret, userID, scopes, customClaims)

See Architecture — Federated Auth for the full flow.

Store Implementations

Implementation Package Use Case
File-based stores/fs Development, < 1000 users
GORM (SQL) stores/gorm PostgreSQL, MySQL, SQLite
GAE/Datastore stores/gae Google Cloud deployments

All stores implement the same interfaces: UserStore, IdentityStore, ChannelStore, TokenStore, RefreshTokenStore, APIKeyStore, KeyStore.

See Stores for interface definitions and usage examples.

Client SDK

For CLI tools and programmatic clients:

import (
    "github.com/panyam/oneauth/client"
    "github.com/panyam/oneauth/client/stores/fs"
)

store, _ := fs.NewFSCredentialStore("", "myapp")
authClient := client.NewAuthClient("https://api.example.com", store)
authClient.Login("user@example.com", "password", "read write")

// Auto-refresh on 401 or before expiry
resp, _ := authClient.HTTPClient().Get("https://api.example.com/resource")

Documentation

Guides
Guide Description
Getting Started Installation, store setup, first auth flow
API Authentication JWT middleware, custom claims, multi-tenant validation
Browser Authentication OAuth flows, channel linking, session management
gRPC Integration Context utilities, auth interceptors
Stores Store interfaces, implementations, KeyStore
Testing Test patterns, security best practices
Reference
Document Description
Architecture Design decisions, data model, token lifecycle, federated auth
Auth Flows Login/signup decision trees, user journeys
Developer Guide Index of all developer documentation
User Guide End-user documentation
Release Notes Version history and changelog
API Docs Generated godoc reference

Requirements

  • Go 1.21+
  • golang.org/x/crypto/bcrypt — password hashing
  • github.com/golang-jwt/jwt/v5 — JWT tokens
  • golang.org/x/oauth2 — OAuth providers (optional)
  • gorm.io/gorm — GORM stores (optional)
  • cloud.google.com/go/datastore — GAE stores (optional)

License

See LICENSE file for terms and conditions.

Documentation

Overview

Package oneauth provides a unified authentication framework for Go applications.

OneAuth separates authentication concerns into three layers: users, identities, and channels. This design enables multiple authentication methods per user while maintaining a single account.

Architecture

User: A unique account in your system. Users are identified by a user ID and contain profile information.

Identity: A contact method (email address or phone number) that belongs to a user. Identities have verification status and can be shared across multiple authentication channels.

Channel: An authentication mechanism (local password, Google OAuth, GitHub OAuth) connected to an identity. Channels store provider-specific credentials and profile data.

Basic Usage

Set up stores for users, identities, channels, and tokens:

import (
    "github.com/panyam/oneauth"
    "github.com/panyam/oneauth/stores"
)

storagePath := "/path/to/storage"
userStore := stores.NewFSUserStore(storagePath)
identityStore := stores.NewFSIdentityStore(storagePath)
channelStore := stores.NewFSChannelStore(storagePath)
tokenStore := stores.NewFSTokenStore(storagePath)

Create authentication callbacks:

createUser := oneauth.NewCreateUserFunc(userStore, identityStore, channelStore)
validateCreds := oneauth.NewCredentialsValidator(identityStore, channelStore, userStore)
verifyEmail := oneauth.NewVerifyEmailFunc(identityStore, tokenStore)
updatePassword := oneauth.NewUpdatePasswordFunc(identityStore, channelStore)

Configure local authentication:

localAuth := &oneauth.LocalAuth{
    CreateUser:          createUser,
    ValidateCredentials: validateCreds,
    EmailSender:         &oneauth.ConsoleEmailSender{},
    TokenStore:          tokenStore,
    BaseURL:             "https://yourapp.com",
    VerifyEmail:         verifyEmail,
    UpdatePassword:      updatePassword,
    HandleUser: func(authtype, provider string, token *oauth2.Token,
                    userInfo map[string]any, w http.ResponseWriter, r *http.Request) {
        // Create session and respond
    },
}

Set up HTTP handlers:

mux := http.NewServeMux()
mux.Handle("/auth/login", localAuth)
mux.Handle("/auth/signup", http.HandlerFunc(localAuth.HandleSignup))
mux.Handle("/auth/verify-email", http.HandlerFunc(localAuth.HandleVerifyEmail))
mux.Handle("/auth/forgot-password", http.HandlerFunc(localAuth.HandleForgotPassword))
mux.Handle("/auth/reset-password", http.HandlerFunc(localAuth.HandleResetPassword))

Store Implementations

OneAuth provides file-based store implementations in the stores package, suitable for development and small applications. For production use with larger user bases, implement the store interfaces backed by your database.

Security

Passwords are hashed using bcrypt with default cost. Verification and password reset tokens are cryptographically secure 32-byte values, hex-encoded to 64 characters. Tokens expire automatically (24 hours for verification, 1 hour for password reset) and are deleted after single use.

Testing

Authentication handlers can be tested without a running HTTP server using httptest.NewRequest and httptest.ResponseRecorder. Tests use temporary storage directories for complete isolation.

Index

Constants

View Source
const (
	ErrCodeEmailExists     = "email_exists"
	ErrCodeUsernameTaken   = "username_taken"
	ErrCodeWeakPassword    = "weak_password"
	ErrCodeInvalidUsername = "invalid_username"
	ErrCodeInvalidEmail    = "invalid_email"
	ErrCodeInvalidPhone    = "invalid_phone"
	ErrCodeMissingField    = "missing_field"
	ErrCodeInvalidCreds    = "invalid_credentials"
)

Common error codes

View Source
const (
	ScopeRead    = "read"    // Read access to user data
	ScopeWrite   = "write"   // Write access to user data
	ScopeProfile = "profile" // Access to user profile information
	ScopeOffline = "offline" // Enable refresh tokens (long-lived sessions)
	ScopeAdmin   = "admin"   // Administrative access
)

Built-in scope constants

View Source
const (
	TokenExpiryEmailVerification = 24 * time.Hour     // 24 hours
	TokenExpiryPasswordReset     = 1 * time.Hour      // 1 hour
	TokenExpiryAccessToken       = 15 * time.Minute   // 15 minutes
	TokenExpiryRefreshToken      = 7 * 24 * time.Hour // 7 days
)

Default token expiry durations

View Source
const DefaultUserParamName = "loggedInUserId"

DefaultUserParamName is the default context key for user ID

Variables

View Source
var (
	ErrAdminUnauthorized = fmt.Errorf("admin authentication required")
	ErrAdminForbidden    = fmt.Errorf("admin access denied")
)

Common errors for admin auth

View Source
var (
	ErrKeyNotFound       = fmt.Errorf("signing key not found")
	ErrAlgorithmMismatch = fmt.Errorf("algorithm mismatch")
)

Common errors for key operations

View Source
var (
	ErrTokenNotFound  = fmt.Errorf("token not found")
	ErrTokenExpired   = fmt.Errorf("token expired")
	ErrTokenRevoked   = fmt.Errorf("token revoked")
	ErrTokenReused    = fmt.Errorf("token reuse detected")
	ErrInvalidGrant   = fmt.Errorf("invalid grant")
	ErrInvalidScope   = fmt.Errorf("invalid scope")
	ErrAPIKeyNotFound = fmt.Errorf("api key not found")
)

Common errors for token operations

View Source
var PolicyEmailOnly = SignupPolicy{
	RequireUsername:       false,
	RequireEmail:          true,
	RequirePhone:          false,
	RequirePassword:       true,
	EnforceUsernameUnique: true,
	EnforceEmailUnique:    true,
	MinPasswordLength:     8,
	UsernamePattern:       `^[a-zA-Z0-9_-]{3,20}$`,
}

PolicyEmailOnly requires only email and password for signup (username optional)

View Source
var PolicyFlexible = SignupPolicy{
	RequireUsername:       false,
	RequireEmail:          false,
	RequirePhone:          false,
	RequirePassword:       false,
	EnforceUsernameUnique: true,
	EnforceEmailUnique:    true,
	MinPasswordLength:     8,
	UsernamePattern:       `^[a-zA-Z0-9_-]{3,20}$`,
}

PolicyFlexible is OAuth-friendly - email/phone optional, username optional

View Source
var PolicyUsernameRequired = SignupPolicy{
	RequireUsername:       true,
	RequireEmail:          true,
	RequirePhone:          false,
	RequirePassword:       true,
	EnforceUsernameUnique: true,
	EnforceEmailUnique:    true,
	MinPasswordLength:     8,
	UsernamePattern:       `^[a-zA-Z0-9_-]{3,20}$`,
}

PolicyUsernameRequired requires username, email, and password for signup

Functions

func AllBuiltinScopes added in v0.0.20

func AllBuiltinScopes() []string

AllBuiltinScopes returns all built-in scope values

func CSRFTemplateField added in v0.0.37

func CSRFTemplateField(r *http.Request) template.HTML

CSRFTemplateField returns an HTML hidden input field containing the CSRF token. Use this in templates: {{.CSRFField}}

func CSRFToken added in v0.0.37

func CSRFToken(r *http.Request) string

CSRFToken extracts the CSRF token from the request context. Returns an empty string if the CSRF middleware is not active.

func ContainsAllScopes added in v0.0.20

func ContainsAllScopes(granted, required []string) bool

ContainsAllScopes checks if all required scopes are present in the granted scopes

func ContainsScope added in v0.0.20

func ContainsScope(scopes []string, scope string) bool

ContainsScope checks if a scope is present in the list

func DetectUsernameType added in v0.0.13

func DetectUsernameType(username string) string

DetectUsernameType attempts to detect what type of username was provided

func GenerateAPIKeyID added in v0.0.20

func GenerateAPIKeyID() (string, error)

GenerateAPIKeyID generates a new API key ID with prefix

func GenerateAPIKeySecret added in v0.0.20

func GenerateAPIKeySecret() (string, error)

GenerateAPIKeySecret generates the secret portion of an API key

func GenerateSecureToken added in v0.0.13

func GenerateSecureToken() (string, error)

GenerateSecureToken generates a cryptographically secure random token

func GetAuthTypeFromAPIContext added in v0.0.20

func GetAuthTypeFromAPIContext(ctx context.Context) string

GetAuthTypeFromAPIContext retrieves the auth type ("jwt" or "api_key") from context

func GetCustomClaimsFromContext added in v0.0.35

func GetCustomClaimsFromContext(ctx context.Context) map[string]any

GetCustomClaimsFromContext retrieves the custom (non-standard) JWT claims from context. Returns nil if no custom claims are present (e.g., API key auth or no token).

func GetScopesFromAPIContext added in v0.0.20

func GetScopesFromAPIContext(ctx context.Context) []string

GetScopesFromAPIContext retrieves the granted scopes from the API middleware context

func GetUserIDFromAPIContext added in v0.0.20

func GetUserIDFromAPIContext(ctx context.Context) string

GetUserIDFromAPIContext retrieves the user ID from the API middleware context

func GetUserIDFromContext added in v0.0.20

func GetUserIDFromContext(ctx context.Context) string

GetUserIDFromContext retrieves the user ID from the request context Uses the default key "loggedInUserId"

func IdentityKey added in v0.0.13

func IdentityKey(identityType, identityValue string) string

IdentityKey creates a consistent identity key from type and value

func IntersectScopes added in v0.0.20

func IntersectScopes(requested, allowed []string) []string

IntersectScopes returns the intersection of requested and allowed scopes The result contains only scopes that appear in both slices

func JoinScopes added in v0.0.20

func JoinScopes(scopes []string) string

JoinScopes joins a slice of scopes into a space-separated string

func LinkLocalCredentials added in v0.0.26

func LinkLocalCredentials(config EnsureAuthUserConfig, userID string, username, password, email string) error

LinkLocalCredentials adds local (password) authentication to an existing OAuth-only user. This enables "incremental auth" where users sign up via OAuth and later add a password.

Who Calls This

Your app calls this from a "Set Password" or "Complete Profile" page. Typically:

  1. User signed up via Google OAuth (has google channel, no local channel)
  2. User visits profile page, sees "Add password for email login"
  3. User submits password (and optionally username) form
  4. Your handler calls LinkLocalCredentials with the logged-in user's ID
  5. User can now login with email/password OR Google

Example Handler in Your App

func handleSetPassword(w http.ResponseWriter, r *http.Request) {
    userID := getLoggedInUserID(r) // from session/JWT
    user, _ := userStore.GetUserById(userID)
    email := user.Profile()["email"].(string)

    username := r.FormValue("username") // optional
    password := r.FormValue("password")

    err := oneauth.LinkLocalCredentials(config, userID, username, password, email)
    if err != nil {
        // handle error (e.g., username taken, password too weak)
    }
    // redirect to profile with success message
}

What It Does

  1. Verifies the email belongs to the given userID
  2. Checks that local channel doesn't already exist
  3. Creates local channel with hashed password
  4. Reserves username in UsernameStore (if configured and username provided)
  5. Updates user profile["channels"] to include "local"

func MintResourceToken added in v0.0.35

func MintResourceToken(userID, appClientID, appSecret string, quota AppQuota, scopes []string) (string, error)

MintResourceToken creates a resource-scoped JWT for a user on behalf of a registered App, signed with the app's shared secret (HS256). This is the backwards-compatible API.

func MintResourceTokenWithKey added in v0.0.35

func MintResourceTokenWithKey(userID, appClientID string, signingKey any, quota AppQuota, scopes []string) (string, error)

MintResourceTokenWithKey creates a resource-scoped JWT signed with the provided key. The signing algorithm is auto-detected from the key type:

  • []byte → HS256
  • *rsa.PrivateKey → RS256
  • *ecdsa.PrivateKey → ES256

func NewEnsureAuthUserFunc added in v0.0.26

func NewEnsureAuthUserFunc(config EnsureAuthUserConfig) func(authtype string, provider string, token any, userInfo map[string]any) (User, error)

NewEnsureAuthUserFunc creates a function that handles user creation/lookup for both OAuth and local authentication with channel linking support.

Who Calls This

This function is called by OneAuth.SaveUserAndRedirect after a successful OAuth callback or local login. The returned function implements the core logic for AuthUserStore.EnsureAuthUser.

Flow for OAuth (e.g., Google Login)

  1. User clicks "Login with Google" → redirects to Google
  2. Google redirects back to /auth/google/callback with auth code
  3. OAuth handler exchanges code for token, fetches userInfo (email, name, picture)
  4. OAuth handler calls OneAuth.SaveUserAndRedirect(authtype="oauth", provider="google", token, userInfo)
  5. SaveUserAndRedirect calls UserStore.EnsureAuthUser → this function
  6. This function checks if email identity exists: - EXISTS: Link Google channel to existing user, update profile["channels"] - NEW: Create User, Identity (verified=true), Google Channel
  7. SaveUserAndRedirect creates JWT, sets cookies, redirects to app

Flow for Local Signup

  1. User submits signup form with email/password
  2. LocalAuth.HandleSignup validates and calls CreateUser (from NewCreateUserFunc)
  3. CreateUser creates User, Identity (verified=false), Local Channel
  4. HandleSignup calls HandleUser → SaveUserAndRedirect → this function
  5. User is logged in (or email verification required)

Channel Linking Logic

Multiple channels (local, google, github) can point to the same user via shared email:

User (id: abc123)
├── Identity: email → user@example.com
├── Channel: local → email:user@example.com (password_hash)
├── Channel: google → email:user@example.com (oauth profile)
└── Channel: github → email:user@example.com (oauth profile)

User profile tracks linked providers: profile["channels"] = ["local", "google", "github"]

func ParseScopes added in v0.0.20

func ParseScopes(scopeString string) []string

ParseScopes parses a space-separated scope string into a slice

func ScopesEqual added in v0.0.20

func ScopesEqual(a, b []string) bool

ScopesEqual checks if two scope slices contain the same scopes (order-independent)

func SetUserIDInContext added in v0.0.20

func SetUserIDInContext(ctx context.Context, userID string) context.Context

SetUserIDInContext sets the user ID in the request context

func ValidateRequestedScopes added in v0.0.20

func ValidateRequestedScopes(requested, allowed []string) (valid, invalid []string)

ValidateRequestedScopes validates that all requested scopes are from the allowed set Returns the valid scopes and any invalid scopes found

Types

type APIAuth added in v0.0.20

type APIAuth struct {
	// Stores
	RefreshTokenStore RefreshTokenStore
	APIKeyStore       APIKeyStore

	// JWT configuration
	JWTSecretKey  string // Secret key for signing JWTs (HMAC)
	JWTIssuer     string // Issuer claim (e.g., "myapp")
	JWTAudience   string // Audience claim (e.g., "api")
	JWTSigningAlg string // Signing algorithm (defaults to HS256)

	// Asymmetric JWT keys (optional — when set, these take precedence over JWTSecretKey)
	JWTSigningKey any // crypto.PrivateKey (*rsa.PrivateKey or *ecdsa.PrivateKey) for signing
	JWTVerifyKey  any // crypto.PublicKey (*rsa.PublicKey or *ecdsa.PublicKey) for verification

	// Token configuration
	AccessTokenExpiry  time.Duration // Defaults to 15 minutes
	RefreshTokenExpiry time.Duration // Defaults to 7 days

	// Callbacks
	ValidateCredentials CredentialsValidator                              // Validates username/password
	GetUserScopes       GetUserScopesFunc                                 // Returns allowed scopes for a user
	OnLoginSuccess      func(userID string, r *http.Request)              // Optional: for logging/analytics
	OnLoginFailure      func(username string, r *http.Request, err error) // Optional: for logging/analytics

	// CustomClaimsFunc is called during token creation to inject additional claims
	// into the JWT (e.g., client_id, max_rooms for relay-scoped tokens).
	// Standard claims (sub, iss, aud, exp, iat, type, scopes) cannot be overridden.
	// If nil, no custom claims are added (backwards-compatible).
	CustomClaimsFunc func(userID string, scopes []string) (map[string]any, error)

	// Rate limiting (optional)
	RateLimiter RateLimiter
}

APIAuth handles API token-based authentication

func (*APIAuth) CreateAccessToken added in v0.0.32

func (a *APIAuth) CreateAccessToken(userID string, scopes []string) (string, int64, error)

CreateAccessToken creates a signed JWT access token. If CustomClaimsFunc is set, its returned claims are merged into the token (standard claims cannot be overridden).

func (*APIAuth) HandleAPIKeys added in v0.0.20

func (a *APIAuth) HandleAPIKeys(w http.ResponseWriter, r *http.Request)

HandleAPIKeys handles API key management (GET=list, POST=create) Requires authentication (userID must be in request context)

func (*APIAuth) HandleListSessions added in v0.0.20

func (a *APIAuth) HandleListSessions(w http.ResponseWriter, r *http.Request)

HandleListSessions handles GET /api/sessions - lists active sessions for the user Requires authentication (userID must be in request context)

func (*APIAuth) HandleLogout added in v0.0.20

func (a *APIAuth) HandleLogout(w http.ResponseWriter, r *http.Request)

HandleLogout handles POST /api/logout - revokes a refresh token

func (*APIAuth) HandleLogoutAll added in v0.0.20

func (a *APIAuth) HandleLogoutAll(w http.ResponseWriter, r *http.Request)

HandleLogoutAll handles POST /api/logout-all - revokes all refresh tokens for the user Requires authentication (userID must be in request context)

func (*APIAuth) HandleRevokeAPIKey added in v0.0.20

func (a *APIAuth) HandleRevokeAPIKey(w http.ResponseWriter, r *http.Request)

HandleRevokeAPIKey handles DELETE /api/keys/:id - revokes an API key Requires authentication (userID must be in request context)

func (*APIAuth) ServeHTTP added in v0.0.20

func (a *APIAuth) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP handles the /api/login endpoint

func (*APIAuth) ValidateAccessToken added in v0.0.20

func (a *APIAuth) ValidateAccessToken(tokenString string) (userID string, scopes []string, err error)

ValidateAccessToken validates a JWT access token and returns the claims

func (*APIAuth) ValidateAccessTokenFull added in v0.0.32

func (a *APIAuth) ValidateAccessTokenFull(tokenString string) (userID string, scopes []string, customClaims map[string]any, err error)

ValidateAccessTokenFull validates a JWT access token and returns the standard claims plus any custom claims (non-standard keys) as a separate map.

func (*APIAuth) VerifyTokenFunc added in v0.0.23

func (a *APIAuth) VerifyTokenFunc() func(tokenString string) (userID string, token any, err error)

VerifyTokenFunc returns a function that can be used as Middleware.VerifyToken. This allows the Middleware to validate Bearer tokens using the APIAuth's JWT configuration.

type APIKey added in v0.0.20

type APIKey struct {
	KeyID      string     `json:"key_id"`   // Public identifier (e.g., "oa_abc123...")
	KeyHash    string     `json:"key_hash"` // bcrypt hash of the secret portion
	UserID     string     `json:"user_id"`  // Owner of this key
	Name       string     `json:"name"`     // User-defined label
	Scopes     []string   `json:"scopes"`   // Allowed scopes
	CreatedAt  time.Time  `json:"created_at"`
	ExpiresAt  *time.Time `json:"expires_at,omitempty"` // Optional expiry
	LastUsedAt time.Time  `json:"last_used_at"`
	RevokedAt  *time.Time `json:"revoked_at,omitempty"`
	Revoked    bool       `json:"revoked"`
}

APIKey represents a long-lived API key for programmatic access

func (*APIKey) IsExpired added in v0.0.20

func (k *APIKey) IsExpired() bool

IsExpired checks if an API key has expired

func (*APIKey) IsValid added in v0.0.20

func (k *APIKey) IsValid() bool

IsValid checks if an API key is valid (not expired and not revoked)

type APIKeyAuth added in v0.0.35

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

APIKeyAuth authenticates requests using a shared API key passed in the X-Admin-Key header.

func NewAPIKeyAuth added in v0.0.35

func NewAPIKeyAuth(key string) *APIKeyAuth

NewAPIKeyAuth creates an AdminAuth that validates the X-Admin-Key header.

func (*APIKeyAuth) Authenticate added in v0.0.35

func (a *APIKeyAuth) Authenticate(r *http.Request) error

type APIKeyStore added in v0.0.20

type APIKeyStore interface {
	// CreateAPIKey creates a new API key and returns the full key (only shown once)
	// The key format is: keyID + "_" + secret
	CreateAPIKey(userID, name string, scopes []string, expiresAt *time.Time) (fullKey string, apiKey *APIKey, err error)

	// GetAPIKeyByID retrieves an API key by its public ID
	GetAPIKeyByID(keyID string) (*APIKey, error)

	// ValidateAPIKey validates a full API key and returns the key metadata if valid
	// The fullKey format is: keyID + "_" + secret
	ValidateAPIKey(fullKey string) (*APIKey, error)

	// RevokeAPIKey marks an API key as revoked
	RevokeAPIKey(keyID string) error

	// ListUserAPIKeys returns all API keys for a user (without secrets)
	ListUserAPIKeys(userID string) ([]*APIKey, error)

	// UpdateAPIKeyLastUsed updates the last used timestamp
	UpdateAPIKeyLastUsed(keyID string) error
}

APIKeyStore manages API keys for programmatic access

type APIMiddleware added in v0.0.20

type APIMiddleware struct {
	// JWT validation (uses same config as APIAuth)
	JWTSecretKey  string
	JWTIssuer     string
	JWTAudience   string
	JWTSigningAlg string

	// KeyStore for multi-tenant JWT validation. When set, the middleware extracts
	// client_id from unverified JWT claims and looks up the signing key per-client.
	// When nil, falls back to JWTSecretKey (single-tenant, backwards-compatible).
	KeyStore KeyStore

	// API key validation (optional)
	APIKeyStore APIKeyStore

	// Token header configuration
	AuthHeader string // Defaults to "Authorization"

	// TokenQueryParam is the query parameter name to check for a token as fallback
	// when the Authorization header is missing (e.g., "token" for ?token=...).
	// Empty string disables query param extraction (default).
	TokenQueryParam string

	// Error handling
	OnAuthError func(w http.ResponseWriter, r *http.Request, err error)
}

APIMiddleware provides middleware for validating API tokens

func (*APIMiddleware) Optional added in v0.0.20

func (m *APIMiddleware) Optional(next http.Handler) http.Handler

Optional middleware allows requests without auth but sets user info if present

func (*APIMiddleware) RequireScopes added in v0.0.20

func (m *APIMiddleware) RequireScopes(requiredScopes ...string) func(http.Handler) http.Handler

RequireScopes middleware ensures the authenticated user has all required scopes

func (*APIMiddleware) ValidateToken added in v0.0.20

func (m *APIMiddleware) ValidateToken(next http.Handler) http.Handler

ValidateToken middleware validates Bearer tokens (JWT or API key) and sets user info in context

type AdminAuth added in v0.0.35

type AdminAuth interface {
	// Authenticate checks whether the request is authorized.
	// Returns nil if authorized, or an error describing why not.
	Authenticate(r *http.Request) error
}

AdminAuth authenticates admin requests to protected endpoints (e.g., Host registration, key rotation).

type AppQuota added in v0.0.35

type AppQuota struct {
	MaxRooms   int     `json:"max_rooms,omitempty"`
	MaxMsgRate float64 `json:"max_msg_rate,omitempty"`
}

AppQuota contains per-app quota limits embedded as custom claims in resource-scoped JWTs.

type AppRegistrar added in v0.0.35

type AppRegistrar struct {
	KeyStore WritableKeyStore
	Auth     AdminAuth
	// contains filtered or unexported fields
}

AppRegistrar is an embeddable HTTP handler for App registration CRUD. Mount it on any admin service's mux to let apps register and obtain signing credentials.

func (*AppRegistrar) Handler added in v0.0.35

func (h *AppRegistrar) Handler() http.Handler

Handler returns an http.Handler for app registration endpoints.

func (*AppRegistrar) RLockApps added in v0.0.35

func (h *AppRegistrar) RLockApps(fn func(map[string]*AppRegistration))

RLockApps calls fn with a read-locked view of all registered apps.

type AppRegistration added in v0.0.35

type AppRegistration struct {
	ClientID     string    `json:"client_id"`
	ClientDomain string    `json:"client_domain"`
	SigningAlg   string    `json:"signing_alg"`
	MaxRooms     int       `json:"max_rooms,omitempty"`
	MaxMsgRate   float64   `json:"max_msg_rate,omitempty"`
	CreatedAt    time.Time `json:"created_at"`
	Revoked      bool      `json:"revoked"`
}

AppRegistration holds metadata about a registered App.

type AuthError added in v0.0.26

type AuthError struct {
	Code    string // "email_exists", "username_taken", "weak_password", "invalid_format", etc.
	Message string // Human-readable message
	Field   string // Which form field has the error (e.g., "email", "username", "password")
}

AuthError represents a structured authentication error

func NewAuthError added in v0.0.26

func NewAuthError(code, message, field string) *AuthError

NewAuthError creates a new AuthError

func (*AuthError) Error added in v0.0.26

func (e *AuthError) Error() string

type AuthErrorHandler added in v0.0.26

type AuthErrorHandler func(err *AuthError, w http.ResponseWriter, r *http.Request) bool

AuthErrorHandler is called when authentication errors occur. The handler receives the structured error and should write the response. Returns true if the error was handled (response written), false to use default JSON response.

Example implementations:

  • Redirect back to form with flash message (app uses their session library)
  • Redirect with error in query params: /signup?error=email_exists
  • Return JSON error response
  • Log and show generic error page

type AuthToken added in v0.0.13

type AuthToken struct {
	Token     string    `json:"token"`
	Type      TokenType `json:"type"`
	UserID    string    `json:"user_id"`
	Email     string    `json:"email"`
	CreatedAt time.Time `json:"created_at"`
	ExpiresAt time.Time `json:"expires_at"`
}

AuthToken represents a verification or reset token

func (*AuthToken) IsExpired added in v0.0.13

func (t *AuthToken) IsExpired() bool

IsExpired checks if a token has expired

func (*AuthToken) IsValid added in v0.0.13

func (t *AuthToken) IsValid(expectedType TokenType) bool

IsValid checks if a token is valid (not expired and matches type)

type AuthUserStore added in v0.0.13

type AuthUserStore interface {
	UserStore
	IdentityStore
	ChannelStore

	// EnsureAuthUser orchestrates user creation/lookup across stores
	// This is the main entry point for OAuth and local authentication
	EnsureAuthUser(authtype string, provider string, token *oauth2.Token, userInfo map[string]any) (User, error)
}

AuthUserStore combines the store interfaces needed for authentication

type BasicUser

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

BasicUser is a simple implementation of the User interface

func (*BasicUser) Id

func (b *BasicUser) Id() string

func (*BasicUser) Profile added in v0.0.9

func (b *BasicUser) Profile() map[string]any

type CSRFMiddleware added in v0.0.37

type CSRFMiddleware struct {
	// CookieName is the name of the CSRF cookie. Default: "csrf_token".
	CookieName string
	// FieldName is the form field name to check. Default: "csrf_token".
	FieldName string
	// HeaderName is the HTTP header to check. Default: "X-CSRF-Token".
	HeaderName string
	// MaxAge is the cookie lifetime in seconds. Default: 3600 (1 hour).
	MaxAge int
	// Secure sets the Secure flag on the cookie (for HTTPS).
	Secure bool
	// SameSite sets the SameSite attribute. Default: SameSiteStrictMode.
	SameSite http.SameSite
	// Path sets the cookie path. Default: "/".
	Path string
	// ErrorHandler is called when CSRF validation fails. Default: 403 JSON response.
	ErrorHandler http.HandlerFunc
	// ExemptFunc returns true if the request should skip CSRF validation.
	// Default: exempt requests with an Authorization: Bearer header.
	ExemptFunc func(*http.Request) bool
}

CSRFMiddleware implements the double-submit cookie pattern for CSRF protection. It generates a random token stored in a cookie and validates that state-changing requests include a matching token in a form field or header.

Bearer-token requests are exempt by default since they are not vulnerable to CSRF. The cookie is NOT HttpOnly so JavaScript can read it for AJAX header submission.

func (*CSRFMiddleware) Protect added in v0.0.37

func (m *CSRFMiddleware) Protect(next http.Handler) http.Handler

Protect returns middleware that enforces CSRF protection. Safe methods (GET, HEAD, OPTIONS) receive a CSRF cookie and have the token injected into the request context. Unsafe methods must include a matching token in either the form field or header.

type Channel added in v0.0.13

type Channel struct {
	Provider    string         `json:"provider"`     // "local", "google", "github"
	IdentityKey string         `json:"identity_key"` // "email:john@example.com"
	Credentials map[string]any `json:"credentials"`  // password_hash, access_token, etc.
	Profile     map[string]any `json:"profile"`      // optional data from provider
	CreatedAt   time.Time      `json:"created_at"`
	UpdatedAt   time.Time      `json:"updated_at"`
	ExpiresAt   time.Time      `json:"expires_at"` // when channel auth expires and needs re-auth
	Version     int            `json:"version"`    // optimistic locking version
}

Channel represents an authentication mechanism/provider

func (*Channel) IsExpired added in v0.0.28

func (c *Channel) IsExpired() bool

IsExpired returns true if the channel has an expiration time set and it has passed

type ChannelStore added in v0.0.13

type ChannelStore interface {
	// GetChannel gets or optionally creates a channel
	GetChannel(provider string, identityKey string, createIfMissing bool) (channel *Channel, newCreated bool, err error)

	// SaveChannel creates or updates a channel (upsert)
	SaveChannel(channel *Channel) error

	// GetChannelsByIdentity returns all channels for an identity
	GetChannelsByIdentity(identityKey string) ([]*Channel, error)
}

ChannelStore manages authentication channels/providers

type ConsoleEmailSender added in v0.0.13

type ConsoleEmailSender struct{}

ConsoleEmailSender is a development implementation that logs emails to console

func (*ConsoleEmailSender) SendPasswordResetEmail added in v0.0.13

func (c *ConsoleEmailSender) SendPasswordResetEmail(to string, resetLink string) error

func (*ConsoleEmailSender) SendVerificationEmail added in v0.0.13

func (c *ConsoleEmailSender) SendVerificationEmail(to string, verificationLink string) error

type CreateUserFunc added in v0.0.13

type CreateUserFunc func(creds *Credentials) (User, error)

CreateUserFunc creates a new user with the given credentials

func NewCreateUserFunc added in v0.0.13

func NewCreateUserFunc(userStore UserStore, identityStore IdentityStore, channelStore ChannelStore) CreateUserFunc

NewCreateUserFunc creates a CreateUserFunc from stores

type Credentials added in v0.0.13

type Credentials struct {
	Username string  // Required for signup, can be username/email/phone for login
	Email    *string // Optional for signup
	Phone    *string // Optional for signup
	Password string  // Required
}

Credentials represents user credentials for signup or login

type CredentialsValidator added in v0.0.13

type CredentialsValidator func(username, password, usernameType string) (User, error)

CredentialsValidator validates credentials during login and returns the user

func NewCredentialsValidator added in v0.0.13

func NewCredentialsValidator(identityStore IdentityStore, channelStore ChannelStore, userStore UserStore) CredentialsValidator

NewCredentialsValidator creates a CredentialsValidator from stores

func NewCredentialsValidatorWithUsername added in v0.0.26

func NewCredentialsValidatorWithUsername(identityStore IdentityStore, channelStore ChannelStore, userStore UserStore, usernameStore UsernameStore) CredentialsValidator

NewCredentialsValidatorWithUsername creates a CredentialsValidator that supports logging in with username (in addition to email/phone).

Who Calls This

Use this instead of NewCredentialsValidator when setting up LocalAuth if you want users to be able to login with their username:

localAuth := &oneauth.LocalAuth{
    ValidateCredentials: oneauth.NewCredentialsValidatorWithUsername(
        identityStore, channelStore, userStore, usernameStore,
    ),
    // ... other config
}

How Username Login Works

  1. User enters "johndoe" and password on login form
  2. DetectUsernameType returns "username" (not email, not phone)
  3. This validator looks up "johndoe" in UsernameStore → gets userID
  4. Gets user's email identity from IdentityStore
  5. Gets local channel for that email identity
  6. Verifies password against channel's password_hash
  7. Returns the user

Fallback Behavior

If user enters an email or phone number instead of username, it falls back to the standard email/phone lookup (same as NewCredentialsValidator).

type EncryptedKeyStore added in v0.0.35

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

EncryptedKeyStore is a WritableKeyStore decorator that transparently encrypts HMAC (HS256/HS384/HS512) client secrets at rest using AES-256-GCM envelope encryption. Asymmetric keys (RS256, ES256 public keys) are stored unencrypted since they are not sensitive.

The decorator pattern means a single implementation works with any backend (FS, GORM, GAE, InMemory) without requiring schema changes. Encryption is optional — if no master key is configured, the server runs without it (with a log warning).

Migration: if GCM decryption fails on read, the wrapper falls back to treating the stored bytes as plaintext. This allows transparent migration from unencrypted to encrypted storage without a data migration step.

func NewEncryptedKeyStore added in v0.0.35

func NewEncryptedKeyStore(inner WritableKeyStore, masterKeyHex string) (*EncryptedKeyStore, error)

NewEncryptedKeyStore creates an EncryptedKeyStore that wraps inner with AES-256-GCM encryption derived from masterKeyHex.

masterKeyHex must be exactly 64 hex characters representing a 32-byte key. Generate one with: openssl rand -hex 32

The raw master key is never used directly for encryption. Instead, HKDF-SHA256 derives an encryption-specific key using the info string "oneauth-keystore-encryption-v1", allowing future key versioning without changing the master key.

func (*EncryptedKeyStore) DeleteKey added in v0.0.35

func (e *EncryptedKeyStore) DeleteKey(clientID string) error

DeleteKey delegates directly to the inner store. No decryption needed.

func (*EncryptedKeyStore) GetExpectedAlg added in v0.0.35

func (e *EncryptedKeyStore) GetExpectedAlg(clientID string) (string, error)

GetExpectedAlg delegates directly to the inner store. Algorithm metadata is not encrypted.

func (*EncryptedKeyStore) GetSigningKey added in v0.0.35

func (e *EncryptedKeyStore) GetSigningKey(clientID string) (any, error)

GetSigningKey returns the signing key for the given client. Behaves identically to GetVerifyKey for HMAC algorithms (same shared secret), with the same decryption and plaintext-fallback logic.

func (*EncryptedKeyStore) GetVerifyKey added in v0.0.35

func (e *EncryptedKeyStore) GetVerifyKey(clientID string) (any, error)

GetVerifyKey returns the verification key for the given client. For HMAC algorithms, the stored ciphertext is decrypted back to the original shared secret. If decryption fails (e.g., the key was stored before encryption was enabled), the raw bytes are returned as-is for backward compatibility.

func (*EncryptedKeyStore) ListKeys added in v0.0.35

func (e *EncryptedKeyStore) ListKeys() ([]string, error)

ListKeys delegates directly to the inner store. Returns client IDs only, no key material involved.

func (*EncryptedKeyStore) RegisterKey added in v0.0.35

func (e *EncryptedKeyStore) RegisterKey(clientID string, key any, algorithm string) error

RegisterKey stores a signing key for the given client. For HMAC algorithms, the key (which must be []byte) is encrypted with AES-256-GCM before being passed to the inner store. A random 12-byte nonce is prepended to the ciphertext. Asymmetric keys (public key PEM bytes) pass through unmodified.

type EnsureAuthUserConfig added in v0.0.26

type EnsureAuthUserConfig struct {
	UserStore     UserStore
	IdentityStore IdentityStore
	ChannelStore  ChannelStore
	UsernameStore UsernameStore // Optional - for username uniqueness
}

EnsureAuthUserConfig holds configuration for NewEnsureAuthUserFunc.

Example setup in your app:

config := oneauth.EnsureAuthUserConfig{
    UserStore:     gaeStores.UserStore,
    IdentityStore: gaeStores.IdentityStore,
    ChannelStore:  gaeStores.ChannelStore,
    UsernameStore: gaeStores.UsernameStore, // optional
}
ensureUser := oneauth.NewEnsureAuthUserFunc(config)

// Then use with OneAuth:
authUserStore := &MyAuthUserStore{config: config, ensureUser: ensureUser}
oneAuth.UserStore = authUserStore

type GetLoggedInUserFunc added in v0.0.26

type GetLoggedInUserFunc func(r *http.Request) (userID string, err error)

GetLoggedInUserFunc returns the currently logged-in user ID from the request. Apps must implement this based on their session/JWT handling.

type GetUserScopesFunc added in v0.0.20

type GetUserScopesFunc func(userID string) ([]string, error)

GetUserScopesFunc is a callback that returns allowed scopes for a user. Applications implement this to determine what scopes a user is allowed to have. This can be based on user roles, profile data, groups, etc.

func DefaultGetUserScopes added in v0.0.20

func DefaultGetUserScopes() GetUserScopesFunc

DefaultGetUserScopes returns a default implementation that grants basic scopes to all users

type HandleUserFunc added in v0.0.9

type HandleUserFunc func(authtype string, provider string, token *oauth2.Token, userInfo map[string]any, w http.ResponseWriter, r *http.Request)

type Identity added in v0.0.13

type Identity struct {
	Type      string    `json:"type"`     // "email", "phone"
	Value     string    `json:"value"`    // "john@example.com", "+1-555-1234"
	UserID    string    `json:"user_id"`  // which user owns this identity
	Verified  bool      `json:"verified"` // has any channel verified this identity?
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
	Version   int       `json:"version"` // optimistic locking version
}

Identity represents a contact method (email, phone) that can be verified

type IdentityStore added in v0.0.13

type IdentityStore interface {
	// GetIdentity gets or optionally creates an identity
	GetIdentity(identityType, identityValue string, createIfMissing bool) (identity *Identity, newCreated bool, err error)

	// SaveIdentity creates or updates an identity (upsert)
	SaveIdentity(identity *Identity) error

	// SetUserForIdentity associates an identity with a user
	SetUserForIdentity(identityType, identityValue string, newUserId string) error

	// MarkIdentityVerified marks an identity as verified
	MarkIdentityVerified(identityType, identityValue string) error

	// GetUserIdentities returns all identities for a user
	GetUserIdentities(userId string) ([]*Identity, error)
}

IdentityStore manages contact identities (email, phone)

type InMemoryKeyStore added in v0.0.32

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

InMemoryKeyStore is a thread-safe in-memory KeyStore implementation. Suitable for testing and simple single-process deployments.

func NewInMemoryKeyStore added in v0.0.32

func NewInMemoryKeyStore() *InMemoryKeyStore

NewInMemoryKeyStore creates a new empty InMemoryKeyStore.

func (*InMemoryKeyStore) DeleteKey added in v0.0.32

func (s *InMemoryKeyStore) DeleteKey(clientID string) error

DeleteKey removes the signing key for the given client_id.

func (*InMemoryKeyStore) GetExpectedAlg added in v0.0.32

func (s *InMemoryKeyStore) GetExpectedAlg(clientID string) (string, error)

GetExpectedAlg returns the expected signing algorithm for the given client_id.

func (*InMemoryKeyStore) GetSigningKey added in v0.0.32

func (s *InMemoryKeyStore) GetSigningKey(clientID string) (any, error)

GetSigningKey returns the signing key for the given client_id. For HMAC algorithms, this is the same shared secret used for verification.

func (*InMemoryKeyStore) GetVerifyKey added in v0.0.32

func (s *InMemoryKeyStore) GetVerifyKey(clientID string) (any, error)

GetVerifyKey returns the verification key for the given client_id. For HMAC algorithms, this is the same shared secret used for signing.

func (*InMemoryKeyStore) ListKeys added in v0.0.32

func (s *InMemoryKeyStore) ListKeys() ([]string, error)

ListKeys returns all registered client IDs.

func (*InMemoryKeyStore) RegisterKey added in v0.0.32

func (s *InMemoryKeyStore) RegisterKey(clientID string, key any, algorithm string) error

RegisterKey adds or overwrites a signing key for the given client_id.

type JWKSHandler added in v0.0.35

type JWKSHandler struct {
	KeyStore    WritableKeyStore // needs ListKeys()
	CacheMaxAge int              // Cache-Control max-age in seconds (default: 3600)
}

JWKSHandler serves a JWKS (JSON Web Key Set) endpoint at /.well-known/jwks.json. Only asymmetric keys (RS256/ES256) are included — HS256 secrets are never exposed.

func (*JWKSHandler) ServeHTTP added in v0.0.35

func (h *JWKSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

type JWKSKeyStore added in v0.0.35

type JWKSKeyStore struct {
	JWKSURL         string
	HTTPClient      *http.Client
	RefreshInterval time.Duration // default: 1 hour
	MinRefreshGap   time.Duration // default: 5 seconds
	// contains filtered or unexported fields
}

JWKSKeyStore implements KeyStore (read-only) by fetching public keys from a remote JWKS endpoint. It caches keys locally and refreshes them periodically.

func NewJWKSKeyStore added in v0.0.35

func NewJWKSKeyStore(jwksURL string, opts ...JWKSOption) *JWKSKeyStore

NewJWKSKeyStore creates a new JWKSKeyStore. Call Start() to begin fetching keys.

func (*JWKSKeyStore) GetExpectedAlg added in v0.0.35

func (s *JWKSKeyStore) GetExpectedAlg(clientID string) (string, error)

GetExpectedAlg returns the algorithm for the given client ID.

func (*JWKSKeyStore) GetSigningKey added in v0.0.35

func (s *JWKSKeyStore) GetSigningKey(clientID string) (any, error)

GetSigningKey always returns an error — JWKS only exposes public keys.

func (*JWKSKeyStore) GetVerifyKey added in v0.0.35

func (s *JWKSKeyStore) GetVerifyKey(clientID string) (any, error)

GetVerifyKey returns the public key for the given client ID. If the key is not cached, triggers a refresh before returning ErrKeyNotFound.

func (*JWKSKeyStore) Start added in v0.0.35

func (s *JWKSKeyStore) Start() error

Start performs the initial JWKS fetch and starts background refresh.

func (*JWKSKeyStore) Stop added in v0.0.35

func (s *JWKSKeyStore) Stop()

Stop stops the background refresh goroutine.

type JWKSOption added in v0.0.35

type JWKSOption func(*JWKSKeyStore)

JWKSOption configures a JWKSKeyStore.

func WithHTTPClient added in v0.0.35

func WithHTTPClient(c *http.Client) JWKSOption

WithHTTPClient sets the HTTP client for JWKS fetching.

func WithMinRefreshGap added in v0.0.35

func WithMinRefreshGap(d time.Duration) JWKSOption

WithMinRefreshGap sets the minimum time between refreshes (prevents stampede).

func WithRefreshInterval added in v0.0.35

func WithRefreshInterval(d time.Duration) JWKSOption

WithRefreshInterval sets how often keys are refreshed in the background.

type KeyStore added in v0.0.32

type KeyStore interface {
	// GetVerifyKey returns the key material for verifying a JWT from the given client.
	GetVerifyKey(clientID string) (any, error)

	// GetSigningKey returns the key material for signing a JWT on behalf of the given client.
	GetSigningKey(clientID string) (any, error)

	// GetExpectedAlg returns the expected signing algorithm for the given client.
	// Used to prevent algorithm confusion attacks.
	GetExpectedAlg(clientID string) (string, error)
}

KeyStore provides multi-tenant signing key lookup for JWT verification and minting. For HS256 keys, GetVerifyKey and GetSigningKey return []byte (shared secret). For RS256/ES256 (future), GetVerifyKey returns crypto.PublicKey and GetSigningKey returns crypto.PrivateKey.

type LinkCredentialsConfig added in v0.0.26

type LinkCredentialsConfig struct {
	UserStore     UserStore
	IdentityStore IdentityStore
	ChannelStore  ChannelStore
	UsernameStore UsernameStore // Optional
}

LinkCredentialsConfig holds configuration for HandleLinkCredentials

type LinkOAuthConfig added in v0.0.26

type LinkOAuthConfig struct {
	UserStore     UserStore
	IdentityStore IdentityStore
	ChannelStore  ChannelStore
}

LinkOAuthConfig holds configuration for OAuth account linking

type LocalAuth added in v0.0.9

type LocalAuth struct {
	// Validates credentials during login
	ValidateCredentials CredentialsValidator

	// Validates credentials during signup (deprecated: use SignupPolicy instead)
	ValidateSignup SignupValidator

	// Creates a new user (for signup)
	CreateUser CreateUserFunc

	// Optional email sender for verification emails
	EmailSender SendEmail

	// Optional token store for email verification and password reset
	TokenStore TokenStore

	// Base URL for generating verification/reset links
	BaseURL string

	// Whether email verification is required before login
	RequireEmailVerification bool

	// Provider name (defaults to "local")
	Provider string

	// Form field names
	UsernameField string
	PasswordField string
	EmailField    string
	PhoneField    string

	// Handler called after successful authentication
	HandleUser HandleUserFunc

	// Callback to verify email by token
	VerifyEmail VerifyEmailFunc

	// Callback to update password
	UpdatePassword UpdatePasswordFunc

	// SignupPolicy defines what is required for signup (overrides ValidateSignup if set)
	SignupPolicy *SignupPolicy

	// OnSignupError is called when signup fails. If nil, returns JSON error.
	OnSignupError AuthErrorHandler

	// OnLoginError is called when login fails. If nil, returns JSON error.
	OnLoginError AuthErrorHandler

	// SignupURL is used for redirects on error (if OnSignupError uses redirects)
	SignupURL string

	// LoginURL is used for redirects on error (if OnLoginError uses redirects)
	LoginURL string

	// Optional UsernameStore for enforcing username uniqueness
	UsernameStore UsernameStore

	// ForgotPasswordURL: if set, GET /forgot-password redirects here and
	// POST /forgot-password redirects here with ?sent=true on success.
	// If empty, GET renders a basic HTML form and POST returns JSON.
	ForgotPasswordURL string

	// ResetPasswordURL: if set, GET /reset-password redirects here (with ?token=...)
	// and POST redirects here with ?success=true on success or ?error=... on failure.
	// If empty, GET renders a basic HTML form and POST returns JSON.
	ResetPasswordURL string
}

Allows local username/password based authentication

func (*LocalAuth) HandleForgotPassword added in v0.0.13

func (a *LocalAuth) HandleForgotPassword(w http.ResponseWriter, r *http.Request)

HandleForgotPassword handles forgot password requests (POST)

func (*LocalAuth) HandleForgotPasswordForm added in v0.0.13

func (a *LocalAuth) HandleForgotPasswordForm(w http.ResponseWriter, r *http.Request)

HandleForgotPasswordForm shows the forgot password form (GET)

func (*LocalAuth) HandleLinkCredentials added in v0.0.26

func (a *LocalAuth) HandleLinkCredentials(config LinkCredentialsConfig, getUser GetLoggedInUserFunc) http.HandlerFunc

HandleLinkCredentials returns an HTTP handler that adds local (password) auth to an existing OAuth-only user.

Who Calls This

Mount this handler at a protected route (requires login) like POST /auth/link-credentials:

localAuth := &oneauth.LocalAuth{...}
linkConfig := oneauth.LinkCredentialsConfig{
    UserStore:     stores.UserStore,
    IdentityStore: stores.IdentityStore,
    ChannelStore:  stores.ChannelStore,
    UsernameStore: stores.UsernameStore, // optional
}
getUser := func(r *http.Request) (string, error) {
    return getLoggedInUserIDFromSession(r), nil
}
mux.Handle("POST /auth/link-credentials", localAuth.HandleLinkCredentials(linkConfig, getUser))

Flow

  1. OAuth-only user visits profile page, sees "Add password" form
  2. User submits form with password (and optionally username)
  3. Handler validates input, creates local channel, reserves username
  4. User can now login with email/password OR their OAuth provider

Form Fields

  • password (required): The new password
  • username (optional): Username for login (if UsernameStore configured)

Responses

  • 200 OK: {"success": true, "message": "..."}
  • 400 Bad Request: {"error": "...", "code": "...", "field": "..."}
  • 401 Unauthorized: User not logged in
  • 409 Conflict: Local credentials already exist

func (*LocalAuth) HandleResetPassword added in v0.0.13

func (a *LocalAuth) HandleResetPassword(w http.ResponseWriter, r *http.Request)

HandleResetPassword handles password reset submissions (POST)

func (*LocalAuth) HandleResetPasswordForm added in v0.0.13

func (a *LocalAuth) HandleResetPasswordForm(w http.ResponseWriter, r *http.Request)

HandleResetPasswordForm shows the reset password form (GET)

func (*LocalAuth) HandleSignup added in v0.0.13

func (a *LocalAuth) HandleSignup(w http.ResponseWriter, r *http.Request)

HandleSignup processes user registration

func (*LocalAuth) HandleVerifyEmail added in v0.0.13

func (a *LocalAuth) HandleVerifyEmail(w http.ResponseWriter, r *http.Request)

HandleVerifyEmail handles email verification via token

func (*LocalAuth) ServeHTTP added in v0.0.9

func (a *LocalAuth) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP handles login requests

type Middleware

type Middleware struct {
	AuthTokenHeaderName string
	AuthTokenCookieName string
	UserParamName       string
	CallbackURLParam    string
	SessionGetter       func(r *http.Request, param string) any
	GetRedirURL         func(r *http.Request) string
	DefaultRedirectURL  string
	VerifyToken         func(tokenString string) (loggedInUserId string, token any, err error)
}

func (*Middleware) EnsureReasonableDefaults

func (a *Middleware) EnsureReasonableDefaults()

*

  • Ensures that config values have reasonable defaults.

func (*Middleware) EnsureUser

func (a *Middleware) EnsureUser(next http.Handler) http.Handler

func (*Middleware) ExtractUser

func (a *Middleware) ExtractUser(next http.Handler) http.Handler

*

  • Fetches the user from the request and loads the UserId and User variables
  • available for other handlers. *
  • Note this does not perform any redirects if a valid user does not exist.
  • To also enforce a user exists, use the EnsureUser handler which both
  • calls ExgractUser and ensures that user is logged in.

func (*Middleware) GetLoggedInUserId

func (a *Middleware) GetLoggedInUserId(r *http.Request) string

Get the ID of the logged in user from the current request

type NoAuth added in v0.0.35

type NoAuth struct{}

NoAuth allows all requests. For development/testing only.

func NewNoAuth added in v0.0.35

func NewNoAuth() *NoAuth

func (*NoAuth) Authenticate added in v0.0.35

func (a *NoAuth) Authenticate(r *http.Request) error

type OneAuth

type OneAuth struct {
	Session    *scs.SessionManager
	Middleware Middleware

	// Optional name that can be used as a prefix for all required vars
	AppName string

	// Name of the session variable where the auth token is stored
	AuthTokenSessionVar string

	// Must be passed in
	UserStore AuthUserStore

	// All the domains where the auth token cookies will be set on a login success or logout
	CookieDomains []string

	// JWT related fields
	JwtIssuer    string
	JWTSecretKey string

	// How long is a session cookie valid for.  Defaults to 1 day
	SessionTimeoutInSeconds int
	// contains filtered or unexported fields
}

func New

func New(appName string) *OneAuth

func (*OneAuth) AddAuth added in v0.0.2

func (a *OneAuth) AddAuth(prefix string, handler http.Handler) *OneAuth

func (*OneAuth) EnsureDefaults

func (a *OneAuth) EnsureDefaults() *OneAuth

func (*OneAuth) GetLinkingUserID added in v0.0.26

func (a *OneAuth) GetLinkingUserID(r *http.Request) string

GetLinkingUserID retrieves and clears the linking user ID from session. Call this in your OAuth callback to detect linking mode.

Example Usage

func googleCallback(w http.ResponseWriter, r *http.Request) {
    linkingUserID := oneAuth.GetLinkingUserID(r)
    if linkingUserID != "" {
        // Linking flow
        oneAuth.HandleLinkOAuthCallback(config, linkingUserID, "google", userInfo, w, r)
        return
    }
    // Normal login flow
    oneAuth.SaveUserAndRedirect(...)
}

func (*OneAuth) HandleLinkOAuthCallback added in v0.0.26

func (a *OneAuth) HandleLinkOAuthCallback(config LinkOAuthConfig, linkingUserID, provider string, userInfo map[string]any, w http.ResponseWriter, r *http.Request)

HandleLinkOAuthCallback returns an HTTP handler for linking an OAuth provider to an existing local-only user.

Who Calls This

This is called by OAuth providers after the user authorizes linking. The flow is:

  1. Local-only user visits profile, clicks "Link Google Account"
  2. App stores user ID in session as "linkingUserID"
  3. App redirects to Google OAuth with special state
  4. Google redirects back to /auth/google/callback
  5. OAuth callback sees "linkingUserID" in session
  6. Instead of normal login, calls this handler to link the account

How to Set Up

Modify your OAuth callback to detect linking mode:

func googleCallback(w http.ResponseWriter, r *http.Request) {
    // ... exchange code for token, get userInfo ...

    // Check if this is a linking flow
    linkingUserID := session.Get("linkingUserID")
    if linkingUserID != "" {
        session.Delete("linkingUserID")
        linkConfig := oneauth.LinkOAuthConfig{
            UserStore:     stores.UserStore,
            IdentityStore: stores.IdentityStore,
            ChannelStore:  stores.ChannelStore,
        }
        oneAuth.HandleLinkOAuthCallback(linkConfig, linkingUserID, "google", userInfo, w, r)
        return
    }

    // Normal login flow
    oneAuth.SaveUserAndRedirect("oauth", "google", token, userInfo, w, r)
}

What It Does

  1. Verifies the OAuth email matches the user's existing email identity
  2. Creates OAuth channel for the provider
  3. Updates user profile["channels"] to include the new provider
  4. Redirects to callback URL (or returns JSON success)

Security

The OAuth email MUST match the user's existing email to prevent account hijacking. Users cannot link to a different email address.

func (*OneAuth) Handler added in v0.0.2

func (a *OneAuth) Handler() http.Handler

func (*OneAuth) SaveUserAndRedirect

func (a *OneAuth) SaveUserAndRedirect(authtype, provider string, token *oauth2.Token, userInfo map[string]any, w http.ResponseWriter, r *http.Request)

*

  • Called by the oauth callback handler with auth token and user info after
  • a successful auth flow and redirect. *
  • Here is our opportunity to:
  • 1. Create a userId that is unique to our system based on userInfo
  • 2. Set the right session cookies from this.

func (*OneAuth) StartLinkOAuth added in v0.0.26

func (a *OneAuth) StartLinkOAuth(r *http.Request, userID string)

StartLinkOAuth initiates OAuth account linking by storing the user ID in session. Call this from your "Link [Provider] Account" button handler.

Example Usage

func handleLinkGoogle(w http.ResponseWriter, r *http.Request) {
    userID := getLoggedInUserID(r)
    oneAuth.StartLinkOAuth(r, userID)
    // Redirect to Google OAuth
    http.Redirect(w, r, "/auth/google/", http.StatusFound)
}

type RateLimiter added in v0.0.20

type RateLimiter interface {
	Allow(key string) bool
}

RateLimiter interface for rate limiting login attempts

type RefreshToken added in v0.0.20

type RefreshToken struct {
	Token      string         `json:"token"`       // 64-char hex token value
	TokenHash  string         `json:"token_hash"`  // SHA256 hash for storage (optional)
	UserID     string         `json:"user_id"`     // Associated user
	ClientID   string         `json:"client_id"`   // Optional client/app identifier
	DeviceInfo map[string]any `json:"device_info"` // User agent, IP, etc.
	Family     string         `json:"family"`      // Token family for rotation tracking
	Generation int            `json:"generation"`  // Increments on rotation
	Scopes     []string       `json:"scopes"`      // Granted scopes
	CreatedAt  time.Time      `json:"created_at"`
	ExpiresAt  time.Time      `json:"expires_at"`
	LastUsedAt time.Time      `json:"last_used_at"`
	RevokedAt  *time.Time     `json:"revoked_at,omitempty"`
	Revoked    bool           `json:"revoked"`
}

RefreshToken represents a long-lived refresh token for API access

func (*RefreshToken) IsExpired added in v0.0.20

func (t *RefreshToken) IsExpired() bool

IsExpired checks if a refresh token has expired

func (*RefreshToken) IsValid added in v0.0.20

func (t *RefreshToken) IsValid() bool

IsValid checks if a refresh token is valid (not expired and not revoked)

type RefreshTokenStore added in v0.0.20

type RefreshTokenStore interface {
	// CreateRefreshToken creates a new refresh token for a user
	CreateRefreshToken(userID, clientID string, deviceInfo map[string]any, scopes []string) (*RefreshToken, error)

	// GetRefreshToken retrieves a refresh token by its value
	GetRefreshToken(token string) (*RefreshToken, error)

	// RotateRefreshToken invalidates old token and creates new one in same family
	// Returns ErrTokenReused if the old token was already revoked (theft detection)
	RotateRefreshToken(oldToken string) (*RefreshToken, error)

	// RevokeRefreshToken marks a token as revoked
	RevokeRefreshToken(token string) error

	// RevokeUserTokens revokes all refresh tokens for a user
	RevokeUserTokens(userID string) error

	// RevokeTokenFamily revokes all tokens in a family (theft detection)
	RevokeTokenFamily(family string) error

	// GetUserTokens lists all active (non-revoked, non-expired) refresh tokens for a user
	GetUserTokens(userID string) ([]*RefreshToken, error)

	// CleanupExpiredTokens removes expired tokens (for maintenance)
	CleanupExpiredTokens() error
}

RefreshTokenStore manages refresh tokens for API access

type SendEmail added in v0.0.13

type SendEmail interface {
	SendVerificationEmail(to string, verificationLink string) error
	SendPasswordResetEmail(to string, resetLink string) error
}

SendEmail interface allows applications to provide their own email sending implementation

type SignupPolicy added in v0.0.26

type SignupPolicy struct {
	RequireUsername       bool   // Is username required? (default: false)
	RequireEmail          bool   // Is email required? (default: true)
	RequirePhone          bool   // Is phone required? (default: false)
	RequirePassword       bool   // Is password required for local? (default: true)
	EnforceUsernameUnique bool   // Check UsernameStore? (default: true if username required)
	EnforceEmailUnique    bool   // Check IdentityStore? (default: true)
	MinPasswordLength     int    // Minimum password (default: 8)
	UsernamePattern       string // Regex for username (default: ^[a-zA-Z0-9_-]{3,20}$)
}

SignupPolicy defines what is required for signup

func DefaultSignupPolicy added in v0.0.26

func DefaultSignupPolicy() SignupPolicy

DefaultSignupPolicy returns a sensible default signup policy

func (SignupPolicy) GetMinPasswordLength added in v0.0.26

func (p SignupPolicy) GetMinPasswordLength() int

GetMinPasswordLength returns the minimum password length

func (SignupPolicy) GetUsernamePattern added in v0.0.26

func (p SignupPolicy) GetUsernamePattern() *regexp.Regexp

GetUsernamePattern returns the compiled username regex pattern

type SignupValidator added in v0.0.13

type SignupValidator func(creds *Credentials) error

SignupValidator validates credentials during signup

var DefaultSignupValidator SignupValidator = func(creds *Credentials) error {

	if len(creds.Username) < 3 || len(creds.Username) > 20 {
		return fmt.Errorf("username must be 3-20 characters")
	}
	usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
	if !usernameRegex.MatchString(creds.Username) {
		return fmt.Errorf("username can only contain letters, numbers, underscores, and hyphens")
	}

	if creds.Email == nil && creds.Phone == nil {
		return fmt.Errorf("email or phone required")
	}

	if creds.Email != nil && *creds.Email != "" {
		emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
		if !emailRegex.MatchString(*creds.Email) {
			return fmt.Errorf("invalid email format")
		}
	}

	if creds.Phone != nil && *creds.Phone != "" {
		cleaned := strings.ReplaceAll(*creds.Phone, "-", "")
		cleaned = strings.ReplaceAll(cleaned, " ", "")
		cleaned = strings.ReplaceAll(cleaned, "(", "")
		cleaned = strings.ReplaceAll(cleaned, ")", "")
		if len(cleaned) < 10 {
			return fmt.Errorf("invalid phone number")
		}
	}

	if len(creds.Password) < 8 {
		return fmt.Errorf("password must be at least 8 characters")
	}

	return nil
}

DefaultSignupValidator provides sensible default validation for signup

type TokenError added in v0.0.20

type TokenError struct {
	Error            string `json:"error"`
	ErrorDescription string `json:"error_description,omitempty"`
}

TokenError represents an OAuth 2.0 compliant error response

type TokenPair added in v0.0.20

type TokenPair struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"` // "Bearer"
	ExpiresIn    int64  `json:"expires_in"` // Seconds until access token expires
	RefreshToken string `json:"refresh_token,omitempty"`
	Scope        string `json:"scope,omitempty"`
}

TokenPair represents the response from a successful authentication

type TokenRequest added in v0.0.20

type TokenRequest struct {
	GrantType    string `json:"grant_type"`              // "password", "refresh_token"
	Username     string `json:"username,omitempty"`      // For password grant
	Password     string `json:"password,omitempty"`      // For password grant
	RefreshToken string `json:"refresh_token,omitempty"` // For refresh_token grant
	Scope        string `json:"scope,omitempty"`         // Requested scopes
	ClientID     string `json:"client_id,omitempty"`     // Optional client identifier
}

TokenRequest represents a request to the token endpoint

type TokenStore added in v0.0.13

type TokenStore interface {
	CreateToken(userID, email string, tokenType TokenType, expiryDuration time.Duration) (*AuthToken, error)
	GetToken(token string) (*AuthToken, error)
	DeleteToken(token string) error
	DeleteUserTokens(userID string, tokenType TokenType) error
}

TokenStore interface for managing auth tokens

type TokenType added in v0.0.13

type TokenType string

TokenType represents different types of auth tokens

const (
	TokenTypeEmailVerification TokenType = "email_verification"
	TokenTypePasswordReset     TokenType = "password_reset"
	TokenTypeRefresh           TokenType = "refresh"
)

type UpdatePasswordFunc added in v0.0.13

type UpdatePasswordFunc func(email, newPassword string) error

func NewUpdatePasswordFunc added in v0.0.13

func NewUpdatePasswordFunc(identityStore IdentityStore, channelStore ChannelStore) UpdatePasswordFunc

NewUpdatePasswordFunc creates an UpdatePasswordFunc from stores. If the user has no local channel (e.g. OAuth-only user), one is created automatically.

type User

type User interface {
	Id() string
	Profile() map[string]any
}

User represents a unified user account

type UserStore

type UserStore interface {
	// CreateUser creates a new user with the given ID and profile
	CreateUser(userId string, isActive bool, profile map[string]any) (User, error)

	// GetUserById retrieves a user by their ID
	GetUserById(userId string) (User, error)

	// SaveUser creates or updates a user (upsert)
	SaveUser(user User) error
}

UserStore manages unified user accounts

type UsernameStore added in v0.0.26

type UsernameStore interface {
	// ReserveUsername reserves a username for a user (creates username -> userID mapping)
	// Returns error if username is already taken
	ReserveUsername(username string, userID string) error

	// GetUserByUsername looks up a userID by username
	// Returns error if username not found
	GetUserByUsername(username string) (userID string, err error)

	// ReleaseUsername removes a username reservation
	ReleaseUsername(username string) error

	// ChangeUsername atomically changes a username (release old, reserve new)
	// Returns error if new username is already taken
	ChangeUsername(oldUsername, newUsername, userID string) error
}

UsernameStore manages username uniqueness (optional - for apps that need username-based login) This is separate from IdentityStore because: - Username is a display handle, not a contact method - Different validation rules than email/phone - Username changes are more common - Enables O(1) username lookup for username-based login

type VerifyEmailFunc added in v0.0.13

type VerifyEmailFunc func(token string) error

func NewVerifyEmailFunc added in v0.0.13

func NewVerifyEmailFunc(identityStore IdentityStore, tokenStore TokenStore) VerifyEmailFunc

NewVerifyEmailFunc creates a VerifyEmailFunc from stores

type WritableKeyStore added in v0.0.32

type WritableKeyStore interface {
	KeyStore

	// RegisterKey adds or overwrites a signing key for the given client_id.
	RegisterKey(clientID string, key any, algorithm string) error

	// DeleteKey removes the signing key for the given client_id.
	DeleteKey(clientID string) error

	// ListKeys returns all registered client IDs.
	ListKeys() ([]string, error)
}

WritableKeyStore extends KeyStore with write operations for key registration and management. All persistent KeyStore implementations (GORM, FS, GAE) implement this interface. InMemoryKeyStore also implements it for testing.

Directories

Path Synopsis
Package client provides client-side authentication utilities for oneauth.
Package client provides client-side authentication utilities for oneauth.
stores/fs
Package fs provides a file system-based credential store for oneauth client.
Package fs provides a file system-based credential store for oneauth client.
cmd
demo-hostapp command
oneauth-server command
oneauth module
Package grpc provides authentication context utilities for passing user information between HTTP handlers and gRPC services via metadata.
Package grpc provides authentication context utilities for passing user information between HTTP handlers and gRPC services via metadata.
Package keystoretest provides shared test suites for all KeyStore implementations.
Package keystoretest provides shared test suites for all KeyStore implementations.
stores
fs
gae
Package gae provides Google Cloud Datastore implementations of oneauth store interfaces.
Package gae provides Google Cloud Datastore implementations of oneauth store interfaces.
gorm
Package gorm provides GORM-based implementations of oneauth store interfaces.
Package gorm provides GORM-based implementations of oneauth store interfaces.

Jump to

Keyboard shortcuts

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