core

package
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Apr 24, 2026 License: MIT Imports: 24 Imported by: 0

README

Error response structure

{
  "status": 400,
  "code": "invalid_input", // Machine-readable 
  "message": "The request contains invalid data.", // Human-readable explanation
  "data?": [ // optional details
    {
      "code": "max_length",        // Machine-readable issue type
      "message": "Password exceeds maximum length of 20 characters", // Human-readable explanation
      "param?": "password",          // The param causing the issue (optional if not field-specific)
      "value?": "mypasswordiswaytoolong123", // Optional: the problematic input
    },
    {
      "code": "required",
      "message": "Username is required"
      "param?": "username",
    }
  ]
}

Data response structure

  • prefer flat. maybe data key. TODO = trying to be mostly equssl toi error

    { "status": 200, "code": "invalid_input", // Machine-readable "message": "The request contains invalid data.", // Human-readable explanation "data?": [ // always array { custom data structure

TODO
  • details>message is the UI message shown. Can be dynamicaly created by server.
  • SDK gets the message and description to show in the UI.
  • param can be used to locate position of the message ex in form validation

Documentation

Index

Constants

View Source
const (
	MimeTypeJSON           = "application/json"
	MimeTypeHTML           = "text/html"
	MimeTypeJavaScript     = "application/javascript"
	MimeTypeJavaScriptText = "text/javascript"
)
View Source
const (
	CodeOkEndpoints            = "ok_endpoints"
	CodeOkEndpointsWithoutAuth = "ok_endpoints_without_auth"
)
View Source
const (
	// oks for non precomputed, dynamic auth responses
	CodeOkAuthentication      = "ok_authentication"        // Standard success code for auth
	CodeOkOAuth2ProvidersList = "ok_oauth2_providers_list" // Success code for OAuth2 providers list
	CodeOkOtpTokenIssued      = "ok_otp_token_issued"
)
View Source
const (
	CodeOkAlreadyVerified             = "ok_already_verified"
	CodeOkEmailVerified               = "ok_email_verified"
	CodeOkVerificationRequested       = "ok_verification_requested"    // Success code for email verification request
	CodeOkPasswordResetRequested      = "ok_password_reset_requested"  // Success code for password reset request
	CodeOkEmailChange                 = "ok_email_change"              // Success code for completed email change
	CodeOkEmailChangeRequested        = "ok_email_change_requested"    // Success code for email change request
	CodeOkPasswordResetNotNeeded      = "ok_password_reset_not_needed" // Success code when password reset is not needed
	CodeOkPasswordNotRequired         = "ok_password_not_required"     // Success code when password is not required for auth
	CodeOkPendingEmailOtpVerification = "ok_pending_email_otp_verification"

	//errors
	CodeErrorTokenGeneration                   = "err_token_generation"
	CodeErrorClaimsNotFound                    = "err_claims_not_found"
	CodeErrorInvalidRequest                    = "err_invalid_input"
	CodeErrorInvalidCredentials                = "err_invalid_credentials"
	CodeErrorPasswordMismatch                  = "err_password_mismatch"
	CodeErrorMissingFields                     = "err_missing_fields"
	CodeErrorWeakPassword                      = "err_weak_password"
	CodeErrorEmailConflict                     = "err_email_conflict"
	CodeErrorNotFound                          = "err_not_found"
	CodeErrorEmailVerificationAlreadyRequested = "err_email_verification_already_requested"
	CodeErrorPasswordResetAlreadyRequested     = "err_password_reset_already_requested"
	CodeErrorEmailChangeAlreadyRequested       = "err_email_change_already_requested"
	CodeErrorPasswordResetFailed               = "err_password_reset_failed"
	CodeErrorPasswordHashingFailed             = "err_password_hashing_failed"
	CodeOkPasswordReset                        = "ok_password_reset"
	CodeErrorRegistrationFailed                = "err_registration_failed"
	CodeErrorTooManyRequests                   = "err_too_many_requests"
	CodeErrorServiceUnavailable                = "err_service_unavailable"
	CodeErrorNoAuthHeader                      = "err_no_auth_header"
	CodeErrorJwtInvalidSignMethod              = "err_invalid_sign_method"
	CodeErrorJwtTokenExpired                   = "err_token_expired"
	CodeErrorAlreadyVerified                   = "err_already_verified"
	CodeErrorJwtInvalidToken                   = "err_invalid_token"
	CodeErrorJwtInvalidVerificationToken       = "err_invalid_verification_token"
	CodeErrorInvalidOtp                        = "err_invalid_otp"
	CodeErrorOtpFailed                         = "err_otp_failed"
	CodeErrorInvalidOAuth2Provider             = "err_invalid_oauth2_provider"
	CodeErrorOAuth2TokenExchangeFailed         = "err_oauth2_token_exchange_failed"
	CodeErrorOAuth2UserInfoFailed              = "err_oauth2_user_info_failed"
	CodeErrorOAuth2UserInfoProcessingFailed    = "err_oauth2_user_info_processing_failed"
	CodeErrorOAuth2DatabaseError               = "err_oauth2_database_error"
	CodeErrorAuthDatabaseError                 = "err_auth_database_error"
	CodeErrorIpBlocked                         = "err_ip_blocked"
	CodeErrorInvalidContentType                = "err_invalid_content_type"
	CodeErrorUnverifiedEmail                   = "err_unverified_email"
	CodeErrorRequiredEmailOtpVerification      = "err_required_email_otp_verification"
)

Standard response codes

Variables

View Source
var HeadersFavicon = map[string]string{

	"Cache-Control": "public, max-age=86400",
}

HeadersFavicon defines cache headers for favicon.ico. Favicons are often requested frequently and don't change often.

View Source
var HeadersJson = map[string]string{

	"Content-Type": "application/json; charset=utf-8",

	"X-Content-Type-Options": "nosniff",

	"Cache-Control": "no-store, no-cache, must-revalidate",

	"X-Frame-Options": "DENY",

	"Content-Security-Policy": "default-src 'none'; frame-ancestors 'none'",
}

TODO consiten name

View Source
var HeadersMaintenancePage = map[string]string{

	"Cache-Control": "no-store",

	"Retry-After": "600",
}

HeadersMaintenancePage defines essential headers for a maintenance response (typically 503).

View Source
var HeadersTls = map[string]string{

	"Strict-Transport-Security": "max-age=63072000; includeSubDomains",
}

headersTls defines headers related to enforcing TLS usage. These should typically only be applied when the connection is actually over HTTPS.

Functions

func FaviconHandler

func FaviconHandler(w http.ResponseWriter, r *http.Request)

faviconHandler handles requests for /favicon.ico by returning a 204 No Content. This prevents 404 errors in logs for browsers that automatically request it. It avoids serving an actual icon file, keeping the API server focused. Caching headers are applied by the middleware or router configuration.

func GzipMiddleware

func GzipMiddleware(fsys fs.FS) func(http.Handler) http.Handler

TODO cache control headers for assets w.Header().Set("Cache-Control", "public, max-age=86400, immutable") // 1 day w.Header().Set("ETag", "") // Empty since we don't support If-None-Match For caching, we shoudl rely on: - Cache-Control header with long max-age (set elsewhere) - Content-based ETags (hash of file contents) - The immutable nature of embedded assets TODO versioning

  • Embedded assets are versioned with the application
  • No risk of serving stale content
  • Cache busting can be done through URL versioning

TODO no dependency on app.

func PrecomputeBasicResponse

func PrecomputeBasicResponse(status int, code, message string) jsonResponse

ResponseBasicFormat is used for short ok and error responses PrecomputeBasicResponse() will be executed during initialization (before main() runs), and the JSON body will be precomputed and stored in the response variables. the variables will contain the fully JSON as []byte already It avoids repeated JSON marshaling during request handling Any time we use writeJSONResponse(w, response) in the code, it simply writes the pre-computed bytes to the response writer

func SetHeaders

func SetHeaders(w http.ResponseWriter, headers ...map[string]string)

setHeaders applies one or more sets of headers to the response writer. Headers from later maps will overwrite headers from earlier maps if keys conflict.

func StaticHeadersMiddleware

func StaticHeadersMiddleware(next http.Handler) http.Handler

StaticHeadersMiddleware adds cache and security related HTTP headers suitable for static assets served from an embedded filesystem in a production environment (!dev build tag). It differentiates between HTML files (applying specific caching and security headers) and other assets like CSS, JS, images (applying long-term immutable caching).

func WriteJsonError

func WriteJsonError(w http.ResponseWriter, resp jsonResponse)

writeJsonError writes a precomputed JSON error response

func WriteJsonOk

func WriteJsonOk(w http.ResponseWriter, resp jsonResponse)

For successful precomputed responses

func WriteJsonWithData

func WriteJsonWithData(w http.ResponseWriter, resp JsonWithData)

WriteJsonWithData writes a structured JSON response with the provided data

Types

type App

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

app is a service with heavy objects for the handlers. and also a out the box coded endpoints handlers. (methods)

func (*App) Auth

func (a *App) Auth() Authenticator

func (*App) AuthWithOAuth2Handler

func (a *App) AuthWithOAuth2Handler(w http.ResponseWriter, r *http.Request)

AuthWithOAuth2Handler handles OAuth2 authentication Endpoint: POST /auth-with-oauth2 Authenticated: No Allowed Mimetype: application/json

func (*App) AuthWithPasswordHandler

func (a *App) AuthWithPasswordHandler(w http.ResponseWriter, r *http.Request)

AuthWithPasswordHandler handles password-based authentication (login). Endpoint: POST /auth-with-password Authenticated: No Allowed Mimetype: application/json

Security: Enumeration hardening

The handler returns exactly two states to the caller:

  • Success: credentials valid, account verified, session token issued.
  • Failure: errorInvalidCredentials, for every credential failure without exception.

Both "email not found" and "wrong password" collapse to errorInvalidCredentials. Distinguishing them would allow an attacker to confirm whether an email is registered by observing the error code alone.

Security: Timing attack

The dominant cost in this handler is crypto.CheckPassword (bcrypt, ~100ms). If the user lookup fails and we return immediately — skipping CheckPassword — the response time is orders of magnitude shorter than a failed password check. An attacker can exploit this difference to enumerate valid emails with high confidence without ever needing the correct password: fast response means the email does not exist, slow response means it does.

Mitigation: crypto.CheckPassword is always called, even when the user is not found. On the not-found path it runs against a static dummy hash and its result is discarded. This ensures both paths pay the same bcrypt cost and are indistinguishable by response time.

Security: Verified check ordering

The verified check runs after CheckPassword deliberately. Checking it before would re-introduce a timing leak: a fast rejection for unverified accounts (before bcrypt) vs a slow rejection for wrong passwords (after bcrypt) would again allow email enumeration for accounts that exist but are unverified. Paying the full bcrypt cost before checking verified status closes that gap.

errorRequiredEmailOtpVerification is the intentional UX escape for users who registered but did not complete email verification. It confirms the email exists and the password is correct — acceptable because this is functionally a login success gated on a verification step, not a credential failure.

func (*App) Cache

func (a *App) Cache() cache.Cache[string, interface{}]

func (*App) Config

func (a *App) Config() *config.Config

func (*App) ConfigStore

func (a *App) ConfigStore() config.SecureStore

func (*App) ConfirmEmailChangeHandler

func (a *App) ConfirmEmailChangeHandler(w http.ResponseWriter, r *http.Request)

func (*App) ConfirmEmailOtpVerificationHandler added in v0.10.0

func (a *App) ConfirmEmailOtpVerificationHandler(w http.ResponseWriter, r *http.Request)

ConfirmEmailOtpVerificationHandler handles email OTP verification code confirmation. Endpoint: POST /confirm-email-otp-verification Authenticated: No Allowed Mimetype: application/json

Security: Enumeration hardening

This handler returns exactly two states to the caller:

  • Success: account is now verified, session token issued via writeAuthResponse.
  • Failure: errorInvalidOtp, for every other case without exception.

Failure is intentionally opaque. The following distinct internal conditions all map to errorInvalidOtp:

  • OTP or verification token is cryptographically invalid or expired.
  • Email extracted from the token does not match any account (errorNotFound in the original — removed because RequestEmailOtpVerificationHandler deliberately issues valid tokens for non-existent emails on silent-failure paths; leaking the distinction here would undo that hardening).
  • Account is already verified (okAlreadyVerified in the original — same reasoning: a token issued on an already-verified silent-failure path must not reveal account state when presented here).

The only way to obtain a valid signed token is from our own request handler, so the enumeration surface here is narrower than at the request step. But since we now issue real tokens on all request paths regardless of account state, the confirm step must be equally opaque or the front-door hardening is bypassed.

func (*App) ConfirmEmailVerificationHandler

func (a *App) ConfirmEmailVerificationHandler(w http.ResponseWriter, r *http.Request)

func (*App) ConfirmPasswordResetHandler

func (a *App) ConfirmPasswordResetHandler(w http.ResponseWriter, r *http.Request)

ConfirmPasswordResetHandler handles password reset confirmation Endpoint: POST /confirm-password-reset Authenticated: No Allowed Mimetype: application/json

func (*App) DbAuth

func (a *App) DbAuth() db.DbAuth

func (*App) DbQueue

func (a *App) DbQueue() db.DbQueue

func (*App) GetClientIP

func (a *App) GetClientIP(r *http.Request) string

GetClientIP extracts the client IP address from the request, handling proxies via configured header

func (*App) ListEndpointsHandler

func (a *App) ListEndpointsHandler(w http.ResponseWriter, r *http.Request)

func (*App) ListOAuth2ProvidersHandler

func (a *App) ListOAuth2ProvidersHandler(w http.ResponseWriter, r *http.Request)

ListOAuth2ProvidersHandler returns available OAuth2 providers Authenticated: No Allowed Mimetype: application/json Example OAuth2 Providers List Response:

{
  "status": 200,
  "code": "ok_oauth2_providers_list",
  "message": "OAuth2 providers list",
  "data": {
    "providers": [
      {
        "name": "google",
        "displayName": "Google",
        "state": "random-state-string",
        "authURL": "https://..."
      }
    ]
  }
}

Endpoint: GET /list-oauth2-providers

func (*App) Logger

func (a *App) Logger() *slog.Logger

func (*App) MetricsHandler

func (a *App) MetricsHandler(w http.ResponseWriter, r *http.Request)

MetricsHandler serves Prometheus metrics in the standard format Endpoint: GET /metrics Authenticated: No Allowed Mimetype: text/plain

func (*App) Notifier

func (a *App) Notifier() notify.Notifier

func (*App) RefreshAuthHandler

func (a *App) RefreshAuthHandler(w http.ResponseWriter, r *http.Request)

RefreshAuthHandler handles explicit JWT token refresh requests Endpoint: POST /auth-refresh Authenticated: Yes Allowed Mimetype: application/json

func (*App) RegisterWithPasswordHandler

func (a *App) RegisterWithPasswordHandler(w http.ResponseWriter, r *http.Request)

RegisterWithPasswordHandler handles password-based user registration. Endpoint: POST /register-with-password Authenticated: No Allowed Mimetype: application/json

Security: Email Enumeration and Timing Attack Prevention

This handler always returns the same response (okPendingEmailOtpVerification) regardless of whether the email already exists in the database, and regardless of whether the existing account used password or OAuth2 signup.

This is intentional. Revealing different responses per case would allow an attacker to enumerate valid emails by observing response bodies.

Timing attacks are also mitigated: crypto.GenerateHash (bcrypt/argon2) is always executed before the DB write, so the response time is dominated by the hash cost in all code paths. An attacker cannot infer email existence from response latency.

Password Protection on Conflict

On email conflict, CreateUserWithPassword never updates the existing password. This prevents account takeover: an attacker who knows a valid email cannot overwrite the real user's password via this unauthenticated endpoint, regardless of whether the account was created with password or OAuth2. Changing a password requires authentication (dedicated settings endpoint).

Flow

1. Validate input. 2. Hash password (always, every code path). 3. Upsert user: insert on new email, no-op on conflict (password untouched). 4. Always return okPendingEmailOtpVerification.

The SDK then calls RequestEmailOtpVerification. Email ownership proof via OTP is the gate. Whatever happened in the DB is irrelevant to the response here.

func (*App) RequestEmailChangeHandler

func (a *App) RequestEmailChangeHandler(w http.ResponseWriter, r *http.Request)

RequestEmailChangeHandler handles email change requests Endpoint: POST /api/request-email-change Authenticated: Yes (requires valid auth token) Allowed Mimetype: application/json

func (*App) RequestEmailOtpVerificationHandler added in v0.10.0

func (a *App) RequestEmailOtpVerificationHandler(w http.ResponseWriter, r *http.Request)

RequestEmailOtpVerificationHandler handles email OTP verification code requests. Endpoint: POST /request-email-otp-verification Authenticated: No Allowed Mimetype: application/json

Security: Enumeration hardening

This handler is deliberately opaque about account state. All three silent failure cases — email not found, account already verified, and OTP already requested (cooldown still active) — return an identical 200 response with a real, well-formed verification_token. No status code or body field reveals whether the email exists or what its state is.

The confirm handler rejects tokens for non-existent / already-verified accounts, so a token issued on a silent-failure path is harmless.

Security: Timing

OTP generation (JWT signing) always runs before any account-state branch, so the dominant CPU cost is paid on every request regardless of outcome.

A small residual timing difference remains: InsertJob (a DB round-trip) only executes on the real path. This gap is narrow — crypto dominates the total latency — but a high-precision attacker under ideal network conditions could detect it. Acceptable for the current threat model; document here if that changes.

func (*App) RequestEmailVerificationHandler

func (a *App) RequestEmailVerificationHandler(w http.ResponseWriter, r *http.Request)

RequestVerificationHandler handles email verification requests Endpoint: POST /request-verification Authenticated: Yes Allowed Mimetype: application/json

func (*App) RequestPasswordResetHandler

func (a *App) RequestPasswordResetHandler(w http.ResponseWriter, r *http.Request)

RequestPasswordResetHandler handles password reset requests Endpoint: POST /request-password-reset Authenticated: No Allowed Mimetype: application/json

Important Security Notes: - Sending emails is an expensive operation and potential spam vector - Rate limiting is enforced via cooldown buckets - Email enumeration is prevented by uniform success responses - Email verification check prevents password reset on unverified accounts

func (*App) Router

func (a *App) Router() router.Router

Router returns the application's router instance

func (*App) SetAuthenticator

func (a *App) SetAuthenticator(auth Authenticator)

SetAuthenticator sets the authenticator implementation

func (*App) SetCache

func (a *App) SetCache(c cache.Cache[string, interface{}])

func (*App) SetConfigProvider

func (a *App) SetConfigProvider(provider *config.Provider)

func (*App) SetConfigStore

func (a *App) SetConfigStore(store config.SecureStore)

func (*App) SetDb

func (a *App) SetDb(dbApp db.DbApp)

SetDb sets the database interfaces for auth and queue

func (*App) SetLogger

func (a *App) SetLogger(l *slog.Logger)

func (*App) SetNotifier

func (a *App) SetNotifier(n notify.Notifier)

func (*App) SetRouter

func (a *App) SetRouter(r router.Router)

func (*App) SetValidator

func (a *App) SetValidator(v Validator)

SetValidator sets the validator implementation

func (*App) Validator

func (a *App) Validator() Validator

Validator returns the validator instance

type AuthData

type AuthData struct {
	TokenType   string     `json:"token_type"`
	AccessToken string     `json:"access_token"`
	Record      AuthRecord `json:"record"`
}

AuthData represents the authentication response structure

func NewAuthData

func NewAuthData(token string, user *db.User) *AuthData

NewAuthData creates a new AuthData instance

type AuthRecord

type AuthRecord struct {
	ID       string `json:"id"`
	Email    string `json:"email"`
	Name     string `json:"name"`
	Verified bool   `json:"verified"`
}

AuthRecord represents the user record in authentication responses

type Authenticator

type Authenticator interface {
	Authenticate(r *http.Request) (*db.User, jsonResponse, error)
}

Authenticator defines the interface for authentication operations

type DefaultAuthenticator

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

DefaultAuthenticator implements Authenticator using the standard authentication flow

func NewDefaultAuthenticator

func NewDefaultAuthenticator(dbAuth db.DbAuth, logger *slog.Logger, configProvider *config.Provider) *DefaultAuthenticator

NewDefaultAuthenticator creates a new DefaultAuthenticator instance

func (*DefaultAuthenticator) Authenticate

func (a *DefaultAuthenticator) Authenticate(r *http.Request) (*db.User, jsonResponse, error)

Authenticate implements the Authenticator interface

type DefaultValidator

type DefaultValidator struct{}

DefaultValidator implements the Validator interface

func (*DefaultValidator) ContentType

func (v *DefaultValidator) ContentType(r *http.Request, allowedType string) (jsonResponse, error)

ContentType checks if the request's Content-Type matches the allowed type. Returns: - error (always "Invalid content type" for security) - precomputed jsonResponse for error cases Uses http.StatusUnsupportedMediaType (415) for invalid content types as per HTTP spec.

func (*DefaultValidator) Email added in v0.10.0

func (v *DefaultValidator) Email(email string) error

Email checks if an email address is valid according to RFC 5321/5322. It rejects display-name formats ("Name <addr>"), enforces length limits, and ensures the domain has a valid structure.

func (*DefaultValidator) Password added in v0.10.0

func (v *DefaultValidator) Password(password string) error

Password checks a password against NIST SP 800-63B guidelines.

type JsonBasic

type JsonBasic struct {
	Status  int    `json:"status"`
	Code    string `json:"code"`
	Message string `json:"message"`
}

JsonBasic contains the basic response fields. All responses must have them

type JsonWithData

type JsonWithData struct {
	JsonBasic
	Data interface{} `json:"data,omitempty"`
}

JsonWithData is used for structured JSON responses with data

type OAuth2ProviderInfo

type OAuth2ProviderInfo struct {
	Name                string `json:"name"`
	DisplayName         string `json:"displayName"`
	State               string `json:"state"`
	AuthURL             string `json:"authURL"`
	RedirectURL         string `json:"redirectURL"`
	CodeVerifier        string `json:"codeVerifier,omitempty"`
	CodeChallenge       string `json:"codeChallenge,omitempty"`
	CodeChallengeMethod string `json:"codeChallengeMethod,omitempty"`
}

OAuth2ProviderInfo contains the provider details needed for client-side OAuth2 flow

type OAuth2ProviderListData

type OAuth2ProviderListData struct {
	Providers []OAuth2ProviderInfo `json:"providers"`
}

OAuth2ProviderListData wraps the list of providers for standardized response

type OtpData added in v0.10.0

type OtpData struct {
	VerificationToken string `json:"verification_token"`
}

type ResponseRecorder

type ResponseRecorder struct {
	http.ResponseWriter
	Status       int       // HTTP status code
	WroteHeader  bool      // Flag to track if headers were written
	BytesWritten int64     // Total bytes written to response
	StartTime    time.Time // When the request started
	RequestID    string    // Optional request ID for tracing
}

ResponseRecorder is a comprehensive recorder that captures various HTTP response metrics

func (*ResponseRecorder) Duration

func (r *ResponseRecorder) Duration() time.Duration

Duration returns the time elapsed since the request started

func (*ResponseRecorder) Write

func (r *ResponseRecorder) Write(b []byte) (int, error)

Write captures bytes written and ensures headers are written first

func (*ResponseRecorder) WriteHeader

func (r *ResponseRecorder) WriteHeader(status int)

WriteHeader captures the status code and marks headers as written

type Validator

type Validator interface {
	// ContentType checks if the request's Content-Type matches the allowed type
	ContentType(r *http.Request, allowedType string) (jsonResponse, error)

	// Email checks if an email address is valid according to RFC 5321/5322.
	// It rejects display-name formats ("Name <addr>"), enforces length limits,
	// and ensures the domain has a valid structure.
	Email(email string) error

	// Password checks a password against NIST SP 800-63B guidelines.
	// It enforces length bounds and rejects known-compromised passwords.
	// It deliberately avoids complexity rules (uppercase/number/symbol
	// requirements), which reduce entropy in practice.
	Password(password string) error
}

Validator defines an interface for request validation operations

func NewValidator

func NewValidator() Validator

NewValidator creates a new DefaultValidator instance

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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