localauth

package
v0.0.84 Latest Latest
Warning

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

Go to latest
Published: May 22, 2026 License: Apache-2.0 Imports: 11 Imported by: 1

Documentation

Overview

Package localauth provides HTTP handlers and store-backed callbacks for local username/password authentication — signup, login, email verification, password reset, and linking local credentials onto existing OAuth users.

<!-- design:start --> The package centers on LocalAuth, a configuration-as-struct type whose optional fields decide which features are live: a nil TokenStore disables password reset, a nil EmailSender disables verification emails, and so on, rather than failing at construction. Its methods are thin HTTP handlers (ServeHTTP for login, HandleSignup, HandleVerifyEmail, the forgot/reset-password pair, and HandleLinkCredentials) that parse forms (url-encoded, multipart, or JSON), enforce policy, and delegate persistence to callback functions. Those callbacks (VerifyEmailFunc, UpdatePasswordFunc, core.CredentialsValidator, core.CreateUserFunc) are built from the New*Func constructors, which close over the User/Identity/Channel stores so the handlers stay store-agnostic. The data model is channel-aware: a User has one Identity per email/phone, and multiple Channels (local, google, github) keyed by the same identity, with profile["channels"] tracking linked providers. Passwords are bcrypt-hashed; missing-user login runs bcrypt against a dummy hash to defeat a CWE-208 timing oracle; forgot-password always reports success to prevent account enumeration; reset forms embed tokens via html/template escaping to prevent XSS.

ENTITIES

LocalAuth — config-as-struct holding validators, stores, email sender, policy, and error handlers; drives all handler behavior via which fields are set.

LocalAuth.ServeHTTP — login handler; parses credentials, applies rate limiting and account lockout (both before validation, keyed by IP:username), validates, and fires HandleUser on success.

LocalAuth.HandleSignup — registration handler; validates against policy or legacy validator, enforces username uniqueness, creates the user, optionally emails a verification link, and auto-logs-in unless verification is both required and configured.

LocalAuth.HandleVerifyEmail — verifies an email-verification token from the ?token query param via the VerifyEmail callback.

LocalAuth.HandleForgotPasswordForm — GET handler; redirects to ForgotPasswordURL or renders a built-in HTML form.

LocalAuth.HandleForgotPassword — POST handler; mints a reset token and emails the link, always returning success to avoid revealing whether the email exists.

LocalAuth.HandleResetPasswordForm — GET handler; redirects to ResetPasswordURL or renders a built-in form with the token html/template-escaped (G705).

LocalAuth.HandleResetPassword — POST handler; validates token and type, enforces an inline 8-char minimum, updates the password, and deletes the one-time token.

LocalAuth.HandleLinkCredentials — returns a protected-route handler that adds a local password (and optional username) to a logged-in OAuth-only user; needs a caller-supplied GetLoggedInUserFunc and maps "already exist" to 409 Conflict.

VerifyEmailFunc — func(token) error callback for email verification; built by NewVerifyEmailFunc.

UpdatePasswordFunc — func(email, newPassword) error callback; built by NewUpdatePasswordFunc.

GetLoggedInUserFunc — app-supplied func(r) (userID, error) resolving the current session user; localauth has no session model of its own.

LinkCredentialsConfig — store bundle for HandleLinkCredentials, converted internally to EnsureAuthUserConfig.

EnsureAuthUserConfig — User/Identity/Channel (plus optional Username) store bundle shared by the ensure-user and linking helpers.

NewCreateUserFunc — builds core.CreateUserFunc creating User + unverified Identity + local Channel with a bcrypt hash; rejects if the identity already exists.

NewCredentialsValidator — builds an email/phone login validator; runs bcrypt against a dummy hash on missing user (CWE-208) and does not support username login.

NewCredentialsValidatorWithUsername — adds username login (resolved via UsernameStore to the email identity) with fallback to email/phone; lacks the dummy-hash defense.

NewVerifyEmailFunc — builds a VerifyEmailFunc that checks token type, marks the email identity verified, and deletes the token (deletion failure is non-fatal).

NewUpdatePasswordFunc — builds an UpdatePasswordFunc that rehashes and stores the new password, auto-creating a local channel for OAuth-only users.

NewEnsureAuthUserFunc — builds the AuthUserStore.EnsureAuthUser logic; links a new channel to an email-matched existing user or creates user/identity/channel afresh (OAuth identities verified, local unverified).

LinkLocalCredentials — adds a local password channel to an existing user, reserves the username, and appends "local" to profile["channels"]; verifies email ownership and rejects if a local channel already exists.

FLOWS

See diagrams.md for the signup (with optional email verification), login (with rate-limit/lockout), password-reset, and OAuth/local credential-linking sequences. <!-- design:end -->

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func LinkLocalCredentials

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 NewCreateUserFunc

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

NewCreateUserFunc creates a CreateUserFunc from stores

func NewCredentialsValidator

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

NewCredentialsValidator creates a CredentialsValidator from stores

func NewCredentialsValidatorWithUsername

func NewCredentialsValidatorWithUsername(identityStore core.IdentityStore, channelStore core.ChannelStore, userStore core.UserStore, usernameStore core.UsernameStore) core.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).

func NewEnsureAuthUserFunc

func NewEnsureAuthUserFunc(config EnsureAuthUserConfig) func(authtype string, provider string, token any, userInfo map[string]any) (core.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"]

Types

type EnsureAuthUserConfig

type EnsureAuthUserConfig struct {
	UserStore     core.UserStore
	IdentityStore core.IdentityStore
	ChannelStore  core.ChannelStore
	UsernameStore core.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

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 LinkCredentialsConfig

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

LinkCredentialsConfig holds configuration for HandleLinkCredentials

type LocalAuth

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

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

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

	// Optional email sender for verification emails
	EmailSender core.SendEmail

	// Optional token store for email verification and password reset
	TokenStore core.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 core.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 *core.SignupPolicy

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

	// OnLoginError is called when login fails. If nil, returns JSON error.
	OnLoginError core.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 core.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

	// RateLimiter limits login attempts per key (IP:username). Optional.
	// When set, rate-limited requests receive 429 Too Many Requests.
	RateLimiter core.RateLimiter

	// Lockout locks accounts after consecutive failed login attempts. Optional.
	// When set, locked accounts receive 429 Too Many Requests until the lockout expires.
	Lockout *core.AccountLockout
}

Allows local username/password based authentication

func (*LocalAuth) HandleForgotPassword

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

HandleForgotPassword handles forgot password requests (POST)

func (*LocalAuth) HandleForgotPasswordForm

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

HandleForgotPasswordForm shows the forgot password form (GET)

func (*LocalAuth) HandleLinkCredentials

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

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

HandleResetPassword handles password reset submissions (POST)

func (*LocalAuth) HandleResetPasswordForm

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

HandleResetPasswordForm shows the reset password form (GET)

func (*LocalAuth) HandleSignup

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

HandleSignup processes user registration

func (*LocalAuth) HandleVerifyEmail

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

HandleVerifyEmail handles email verification via token

func (*LocalAuth) ServeHTTP

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

ServeHTTP handles login requests

type UpdatePasswordFunc

type UpdatePasswordFunc func(email, newPassword string) error

func NewUpdatePasswordFunc

func NewUpdatePasswordFunc(identityStore core.IdentityStore, channelStore core.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 VerifyEmailFunc

type VerifyEmailFunc func(token string) error

func NewVerifyEmailFunc

func NewVerifyEmailFunc(identityStore core.IdentityStore, tokenStore core.TokenStore) VerifyEmailFunc

NewVerifyEmailFunc creates a VerifyEmailFunc from stores

Jump to

Keyboard shortcuts

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