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
- Variables
- func AllBuiltinScopes() []string
- func ContainsAllScopes(granted, required []string) bool
- func ContainsScope(scopes []string, scope string) bool
- func DetectUsernameType(username string) string
- func GenerateAPIKeyID() (string, error)
- func GenerateAPIKeySecret() (string, error)
- func GenerateSecureToken() (string, error)
- func GetAuthTypeFromAPIContext(ctx context.Context) string
- func GetScopesFromAPIContext(ctx context.Context) []string
- func GetUserIDFromAPIContext(ctx context.Context) string
- func GetUserIDFromContext(ctx context.Context) string
- func IdentityKey(identityType, identityValue string) string
- func IntersectScopes(requested, allowed []string) []string
- func JoinScopes(scopes []string) string
- func LinkLocalCredentials(config EnsureAuthUserConfig, userID string, username, password, email string) error
- func NewEnsureAuthUserFunc(config EnsureAuthUserConfig) ...
- func ParseScopes(scopeString string) []string
- func ScopesEqual(a, b []string) bool
- func SetUserIDInContext(ctx context.Context, userID string) context.Context
- func ValidateRequestedScopes(requested, allowed []string) (valid, invalid []string)
- type APIAuth
- func (a *APIAuth) HandleAPIKeys(w http.ResponseWriter, r *http.Request)
- func (a *APIAuth) HandleListSessions(w http.ResponseWriter, r *http.Request)
- func (a *APIAuth) HandleLogout(w http.ResponseWriter, r *http.Request)
- func (a *APIAuth) HandleLogoutAll(w http.ResponseWriter, r *http.Request)
- func (a *APIAuth) HandleRevokeAPIKey(w http.ResponseWriter, r *http.Request)
- func (a *APIAuth) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (a *APIAuth) ValidateAccessToken(tokenString string) (userID string, scopes []string, err error)
- func (a *APIAuth) VerifyTokenFunc() func(tokenString string) (userID string, token any, err error)
- type APIKey
- type APIKeyStore
- type APIMiddleware
- type AuthError
- type AuthErrorHandler
- type AuthToken
- type AuthUserStore
- type BasicUser
- type Channel
- type ChannelStore
- type ConsoleEmailSender
- type CreateUserFunc
- type Credentials
- type CredentialsValidator
- type EnsureAuthUserConfig
- type GetLoggedInUserFunc
- type GetUserScopesFunc
- type HandleUserFunc
- type Identity
- type IdentityStore
- type LinkCredentialsConfig
- type LinkOAuthConfig
- type LocalAuth
- func (a *LocalAuth) HandleForgotPassword(w http.ResponseWriter, r *http.Request)
- func (a *LocalAuth) HandleForgotPasswordForm(w http.ResponseWriter, r *http.Request)
- func (a *LocalAuth) HandleLinkCredentials(config LinkCredentialsConfig, getUser GetLoggedInUserFunc) http.HandlerFunc
- func (a *LocalAuth) HandleResetPassword(w http.ResponseWriter, r *http.Request)
- func (a *LocalAuth) HandleResetPasswordForm(w http.ResponseWriter, r *http.Request)
- func (a *LocalAuth) HandleSignup(w http.ResponseWriter, r *http.Request)
- func (a *LocalAuth) HandleVerifyEmail(w http.ResponseWriter, r *http.Request)
- func (a *LocalAuth) ServeHTTP(w http.ResponseWriter, r *http.Request)
- type Middleware
- type OneAuth
- func (a *OneAuth) AddAuth(prefix string, handler http.Handler) *OneAuth
- func (a *OneAuth) EnsureDefaults() *OneAuth
- func (a *OneAuth) GetLinkingUserID(r *http.Request) string
- func (a *OneAuth) HandleLinkOAuthCallback(config LinkOAuthConfig, linkingUserID, provider string, ...)
- func (a *OneAuth) Handler() http.Handler
- func (a *OneAuth) SaveUserAndRedirect(authtype, provider string, token *oauth2.Token, userInfo map[string]any, ...)
- func (a *OneAuth) StartLinkOAuth(r *http.Request, userID string)
- type RateLimiter
- type RefreshToken
- type RefreshTokenStore
- type SendEmail
- type SignupPolicy
- type SignupValidator
- type TokenError
- type TokenPair
- type TokenRequest
- type TokenStore
- type TokenType
- type UpdatePasswordFunc
- type User
- type UserStore
- type UsernameStore
- type VerifyEmailFunc
Constants ¶
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
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
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
const DefaultUserParamName = "loggedInUserId"
DefaultUserParamName is the default context key for user ID
Variables ¶
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
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)
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
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 ContainsAllScopes ¶ added in v0.0.20
ContainsAllScopes checks if all required scopes are present in the granted scopes
func ContainsScope ¶ added in v0.0.20
ContainsScope checks if a scope is present in the list
func DetectUsernameType ¶ added in v0.0.13
DetectUsernameType attempts to detect what type of username was provided
func GenerateAPIKeyID ¶ added in v0.0.20
GenerateAPIKeyID generates a new API key ID with prefix
func GenerateAPIKeySecret ¶ added in v0.0.20
GenerateAPIKeySecret generates the secret portion of an API key
func GenerateSecureToken ¶ added in v0.0.13
GenerateSecureToken generates a cryptographically secure random token
func GetAuthTypeFromAPIContext ¶ added in v0.0.20
GetAuthTypeFromAPIContext retrieves the auth type ("jwt" or "api_key") from context
func GetScopesFromAPIContext ¶ added in v0.0.20
GetScopesFromAPIContext retrieves the granted scopes from the API middleware context
func GetUserIDFromAPIContext ¶ added in v0.0.20
GetUserIDFromAPIContext retrieves the user ID from the API middleware context
func GetUserIDFromContext ¶ added in v0.0.20
GetUserIDFromContext retrieves the user ID from the request context Uses the default key "loggedInUserId"
func IdentityKey ¶ added in v0.0.13
IdentityKey creates a consistent identity key from type and value
func IntersectScopes ¶ added in v0.0.20
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
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:
- User signed up via Google OAuth (has google channel, no local channel)
- User visits profile page, sees "Add password for email login"
- User submits password (and optionally username) form
- Your handler calls LinkLocalCredentials with the logged-in user's ID
- 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 ¶
- Verifies the email belongs to the given userID
- Checks that local channel doesn't already exist
- Creates local channel with hashed password
- Reserves username in UsernameStore (if configured and username provided)
- Updates user profile["channels"] to include "local"
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) ¶
- User clicks "Login with Google" → redirects to Google
- Google redirects back to /auth/google/callback with auth code
- OAuth handler exchanges code for token, fetches userInfo (email, name, picture)
- OAuth handler calls OneAuth.SaveUserAndRedirect(authtype="oauth", provider="google", token, userInfo)
- SaveUserAndRedirect calls UserStore.EnsureAuthUser → this function
- 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
- SaveUserAndRedirect creates JWT, sets cookies, redirects to app
Flow for Local Signup ¶
- User submits signup form with email/password
- LocalAuth.HandleSignup validates and calls CreateUser (from NewCreateUserFunc)
- CreateUser creates User, Identity (verified=false), Local Channel
- HandleSignup calls HandleUser → SaveUserAndRedirect → this function
- 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
ParseScopes parses a space-separated scope string into a slice
func ScopesEqual ¶ added in v0.0.20
ScopesEqual checks if two scope slices contain the same scopes (order-independent)
func SetUserIDInContext ¶ added in v0.0.20
SetUserIDInContext sets the user ID in the request context
func ValidateRequestedScopes ¶ added in v0.0.20
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
JWTIssuer string // Issuer claim (e.g., "myapp")
JWTAudience string // Audience claim (e.g., "api")
JWTSigningAlg string // Signing algorithm (defaults to HS256)
// 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
// Rate limiting (optional)
RateLimiter RateLimiter
}
APIAuth handles API token-based authentication
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) VerifyTokenFunc ¶ added in v0.0.23
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
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
// API key validation (optional)
APIKeyStore APIKeyStore
// Token header configuration
AuthHeader string // Defaults to "Authorization"
// 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
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 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
NewAuthError creates a new AuthError
type AuthErrorHandler ¶ added in v0.0.26
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
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
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"`
}
Channel represents an authentication mechanism/provider
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
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 ¶
- User enters "johndoe" and password on login form
- DetectUsernameType returns "username" (not email, not phone)
- This validator looks up "johndoe" in UsernameStore → gets userID
- Gets user's email identity from IdentityStore
- Gets local channel for that email identity
- Verifies password against channel's password_hash
- 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 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
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
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 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"`
}
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 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
}
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 ¶
- OAuth-only user visits profile page, sees "Add password" form
- User submits form with password (and optionally username)
- Handler validates input, creates local channel, reserves username
- 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
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 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 (*OneAuth) EnsureDefaults ¶
func (*OneAuth) GetLinkingUserID ¶ added in v0.0.26
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:
- Local-only user visits profile, clicks "Link Google Account"
- App stores user ID in session as "linkingUserID"
- App redirects to Google OAuth with special state
- Google redirects back to /auth/google/callback
- OAuth callback sees "linkingUserID" in session
- 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 ¶
- Verifies the OAuth email matches the user's existing email identity
- Creates OAuth channel for the provider
- Updates user profile["channels"] to include the new provider
- 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) 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
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
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
type UpdatePasswordFunc ¶ added in v0.0.13
func NewUpdatePasswordFunc ¶ added in v0.0.13
func NewUpdatePasswordFunc(identityStore IdentityStore, channelStore ChannelStore) UpdatePasswordFunc
NewUpdatePasswordFunc creates an UpdatePasswordFunc from stores
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
func NewVerifyEmailFunc ¶ added in v0.0.13
func NewVerifyEmailFunc(identityStore IdentityStore, tokenStore TokenStore) VerifyEmailFunc
NewVerifyEmailFunc creates a VerifyEmailFunc from stores
Source Files
¶
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
|
|
|
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. |
|
stores
|
|
|
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. |