core

package
v0.13.0 Latest Latest
Warning

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

Go to latest
Published: May 22, 2026 License: MIT Imports: 25 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 (
	// CompressExtGzip is the file extension for gzip compressed files.
	CompressExtGzip = ".gz"
	// CompressExtBrotli is the file extension for brotli compressed files.
	CompressExtBrotli = ".br"
	// InternalDir is the name of the directory segment that is considered private.
	// Any path containing this segment will return 404.
	InternalDir = "internal/"
)
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"
	CodeOkPasswordResetOtpVerified = "ok_password_reset_otp_verified"
)
View Source
const (
	CodeOkAlreadyVerified             = "ok_already_verified"
	CodeOkEmailChange                 = "ok_email_change"              // Success code for completed email change
	CodeOkPasswordResetNotNeeded      = "ok_password_reset_not_needed" // Success code when password reset is not needed
	CodeOkPendingEmailVerificationOtp = "ok_pending_email_verification_otp"
	CodeOkPasswordReset               = "ok_password_reset"

	//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"
	CodeErrorEmailVerificationOtpAlreadyRequested = "err_email_verification_otp_already_requested"
	CodeErrorPasswordHashingFailed                = "err_password_hashing_failed"
	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"
	CodeErrorRequiredEmailVerificationOtp         = "err_required_email_verification_otp"
	CodeErrorEndpointsHashMismatch                = "err_endpoints_hash_mismatch"
)

Standard response codes

View Source
const HeaderEndpointsHash = "X-Restinpieces-Endpoints-Hash"
View Source
const StrictCSP = "default-src 'self'; frame-ancestors 'none'; form-action 'none'; base-uri 'none'"

StrictCSP defines a hardened Content-Security-Policy for HTML documents. - default-src 'self': Only allow resources from the same origin. - frame-ancestors 'none': Prevent the page from being embedded in frames/iframes (clickjacking). - form-action 'none': Prevent any form submissions from this document. - base-uri 'none': Prevent the use of <base> tags to redirect relative URLs.

Variables

View Source
var (
	ErrorEndpointsHashMismatch = PrecomputeBasicResponse(http.StatusConflict, CodeErrorEndpointsHashMismatch, "Client endpoints are outdated, please refetch endpoints")
)

Precomputed error and ok responses with status codes

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 HeadersStatic = map[string]string{

	"Cache-Control": "public, max-age=31536000, immutable",

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

HeadersStatic defines cache headers for immutable static assets (CSS, JS, images). Assumes filename-based versioning for cache busting.

View Source
var HeadersStaticHtml = map[string]string{

	"Cache-Control": "public, no-cache",

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

	"Content-Security-Policy": StrictCSP,

	"Referrer-Policy": "strict-origin-when-cross-origin",
}

HeadersStaticHtml defines cache headers for HTML entry point files.

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 FSHandler added in v0.12.0

func FSHandler(fsys fs.FS, compressionExt string, notFoundHandler http.Handler) http.HandlerFunc

FSHandler is a highly optimized, zero-fallback static file handler designed specifically for serving the compiled asset bundle of Multi-Page Applications (MPAs) and Single-Page Applications (SPAs).

It is intended to be registered as the catch-all route ("/") so that all requests not claimed by a more specific route fall through to it:

a.Router().Register(r.Chains{
    // Dynamic routes registered first ...
    "GET /books/{bookId}": r.NewChain(
        core.MPAShellHandler(a.FS(), "books/index.html"),
    ).WithMiddleware(a.StaticAssetHeadersMiddleware),

    "GET /books/{bookId}/sentences/{sentenceIndex}/tokens": r.NewChain(
        a.SentenceNlpSsrHandler("internal/sentences-nlp/index.html"),
    ).WithMiddleware(a.SSRHeadersMiddleware),

    // Catch-all: everything else is a static asset
    "/": r.NewChain(
        core.FSHandler(a.FS(), core.CompressExtGzip),
    ).WithMiddleware(a.StaticAssetHeadersMiddleware),
})

For dynamic routes that map to a fixed HTML shell (client-rendered MPA/SPA pages), use MPAShellHandler instead. It reads the file once at startup, holds it in memory, and serves it with zero filesystem activity per request.

For dynamic routes that require data injected into the HTML before sending (SSR, no blank-flash, better SEO), implement a custom handler using [ssr.Marshal].

PERFORMANCE GUARANTEE: This handler guarantees exactly ONE filesystem Open and ONE filesystem Stat per successful request. It relies on strict conventions and avoids all TOCTOU (Time-of-Check to Time-of-Use) issues and expensive filesystem guesswork.

WHY NOT http.FileServerFS?

  1. Performance: http.FileServerFS performs 2 to 4 filesystem operations (Open, Stat, Stat) to resolve directories to index.html and handle redirects.
  2. Compression: http.FileServerFS cannot serve pre-compressed assets (e.g. app.js.gz) while preserving the correct Content-Type (application/javascript). Without special handling, browsers receive a .gz file and treat it as a binary download.
  3. Private Resources: http.FileServerFS serves the entire tree. FSHandler enforces the "internal/" convention for files that the server must read but should never be HTTP-accessible. For example, placing a custom "internal/404.html" prevents users from requesting it directly at "/404.html" and receiving a 200 OK, which is semantically broken and bad for SEO.

PARAMETERS:

  • fsys (fs.FS): The underlying filesystem, typically an embed.FS or os.DirFS.

  • compressionExt (string): An optional extension generated by your bundler (e.g., CompressExtGzip or CompressExtBrotli). If provided, and the browser's Accept-Encoding supports it, the handler will append this to the physical path it opens.

URL RESOLUTION CONVENTIONS:

  1. Root Path ("/" or ""): Always resolves to "index.html".

  2. Private Paths: Any path containing an "internal/" directory segment (e.g., "internal/config.json", "components/internal/secrets.js") is considered private and returns HTTP 404.

  3. Direct Assets (Has Extension): URLs with a file extension (e.g., "/dist/app.js") map directly to that file.

  4. MPA Routes / Directories (No Extension): URLs without an extension are assumed to be directory paths. * Missing Trailing Slash: Returns HTTP 301 Redirect to append the slash, ensuring browser relative paths (like <img src="./logo.png">) work correctly. * Has Trailing Slash: Appends "index.html" strictly in-memory and serves it.

COMPRESSION & HEADERS RULES:

  • Zero Fallback: If compressionExt is set and the browser supports it, the handler will ONLY attempt to open the compressed file (e.g., "app.js.gz"). It will not fall back to "app.js" if the compressed file is missing.
  • Exclusions: HTML files (".html", ".htm") are never compressed by this handler.
  • Content-Type Safety: The Content-Type header is always derived from the logical file extension (e.g., "app.js" -> application/javascript), preventing browsers from downloading compressed assets as binary "octet-stream" attachments.
  • Content-Encoding: Automatically sets the proper Content-Encoding and Vary headers.

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 MPAShellHandler added in v0.13.0

func MPAShellHandler(fsys fs.FS, shellPath string) http.HandlerFunc

MPAShellHandler serves a pre-built HTML shell file for client-rendered routes in Multi-Page Applications (MPAs) and Single-Page Applications (SPAs).

What Is a Shell File

A shell is a minimal, static HTML file produced by your JavaScript bundler (Vite, Webpack, etc.). It contains no meaningful content on its own — just the <script> and <link> tags that bootstrap your JavaScript framework (React, Vue, Svelte, etc.). On load, the framework takes over, calls your JSON API, and renders the page entirely in the browser.

Example shell (dist/books/index.html produced by Vite):

<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="/dist/assets/main-DiwrgTda.css">
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/dist/assets/main-BgFVfQP3.js"></script>
  </body>
</html>

When to Use This Handler

Use MPAShellHandler when:

  • The route is dynamic (e.g. "/books/{id}/sentences/{idx}")
  • The response is always the same HTML file regardless of URL parameters
  • Data fetching and rendering happen entirely in the browser via your JSON API
  • You accept the initial render being empty until JavaScript executes (blank flash or loading spinner before content appears)

When NOT to Use This Handler

Do not use MPAShellHandler when the page must arrive with data already embedded. If you need to eliminate the blank-flash and have content immediately visible (better SEO, faster perceived load, no flicker), implement a custom handler that uses [ssr.Marshal] to inject data into the template before sending:

func (a *App) BookPageHandler(templatePath string) http.HandlerFunc {
    templateBytes, err := fs.ReadFile(a.fs, templatePath)
    if err != nil {
        panic("missing SSR template: " + templatePath)
    }
    return func(w http.ResponseWriter, r *http.Request) {
        bookId := a.Router().Param(r, "bookId")
        book, err := a.db.GetBook(bookId)
        // ... error handling ...
        output, err := ssr.Marshal(templateBytes, map[string]any{
            "book": book,
        }, nonce)
        // ... write response ...
    }
}

Parameters

  • fsys (fs.FS): The underlying filesystem. Typically an embed.FS or os.DirFS. With embed.FS (recommended), the file is compiled into the binary and fs.ReadFile is a pure memory read with no I/O cost.

  • shellPath (string): Clean, relative path to the shell HTML file within fsys. Must point to a file, not a directory (e.g. "books/index.html"). The path is validated at startup: if the file is missing, the handler panics immediately rather than returning 500s at request time.

Startup Validation and Caching

The file is read once when routing is wired and its bytes are held in memory. All subsequent requests are served directly from that in-memory buffer — zero filesystem activity at request time, regardless of whether fsys is an embed.FS or an os.DirFS. If shellPath is not found, the handler panics immediately: a missing shell means the deployment is broken and should fail at startup, not silently return 500s to users.

Usage

In your router, pair dynamic URL patterns with their corresponding shell:

a.Router().Register(r.Chains{
    // Client-rendered: JS fetches data from /api/books/{bookId} after load
    "GET /books/{bookId}": r.NewChain(
        core.MPAShellHandler(a.FS(), "books/index.html"),
    ).WithMiddleware(a.StaticAssetHeadersMiddleware),

    // Client-rendered: JS fetches from /api/books/{bookId}/sentences/{sentenceIndex}
    "GET /books/{bookId}/sentences/{sentenceIndex}": r.NewChain(
        core.MPAShellHandler(a.FS(), "sentences/index.html"),
    ).WithMiddleware(a.StaticAssetHeadersMiddleware),

    // SSR route: data injected server-side, no blank flash — custom handler instead
    "GET /books/{bookId}/sentences/{sentenceIndex}/tokens": r.NewChain(
        a.SentenceNlpSsrHandler("internal/sentences-nlp/index.html"),
    ).WithMiddleware(a.SSRHeadersMiddleware),

    // Catch-all: static assets (js, css, images) with gzip compression
    "/": r.NewChain(
        core.FSHandler(a.FS(), core.CompressExtGzip, nil),
    ).WithMiddleware(a.StaticAssetHeadersMiddleware),
})

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. The variadic merge is the abstraction: headers from later maps will overwrite headers from earlier maps if keys conflict, providing composability.

http.Header is map[string][]string with canonical title-cased keys (e.g. "Cache-Control", not "cache-control"). Direct map assignment works only if your keys are already canonical. All keys in this file happen to be, but this is a silent footgun for any future key added in the wrong case. The safe version costs nothing: The only reason to prefer the direct assignment would be micro-optimizing hot paths, which header-setting is not.

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 and first-time registration. Endpoint: POST /auth-with-oauth2 Authenticated: No Allowed Mimetype: application/json

Login vs Registration

OAuth2 collapses the login/register distinction because the provider is the source of truth: if the email is unknown we create the account on the spot. This is the only write this handler performs.

Account method separation

If the email already exists and was registered with a password, this handler rejects the request. Implicit account linking inside a login endpoint is a security risk — the user has no awareness it happened. Explicit linking is handled by the authenticated POST /link-oauth2 endpoint instead.

Race condition

A TOCTOU window exists between GetUserByEmail returning nil and CreateUserWithOauth2 inserting the row. Two simultaneous first-time signups for the same address will race. The loser receives ErrConstraintUnique from the DB and gets a generic error response — acceptable for this edge case.

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.

errorRequiredEmailVerificationOtp 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) ConfirmEmailChangeOtpHandler added in v0.11.0

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

ConfirmEmailChangeOtpHandler handles the final step of email change. Endpoint: POST /api/confirm-email-change-otp Authenticated: Yes (requires valid auth token) Allowed Mimetype: application/json

# Flow The user submits the 6-digit OTP alongside the `verification_token` from Step 1. The server verifies the JWT signature and the OTP hash, then updates the email in the database. After a successful update, a security alert is queued to the old email address.

# Security: Session invalidation as replay protection After UpdateEmail, the session signing key changes (derived from email + passwordHash + secret). The old session JWT becomes cryptographically invalid. This prevents replay of the confirm request and forces re-login.

# Security: Race condition defense If the new email was taken between request and confirm, UpdateEmail fails with a unique constraint violation. This is returned as a generic error.

# Security: Old email notification After a successful email update, a JobTypeEmailChangeAlert job is queued to inform the old email owner. This is best-effort — if the queue insert fails, the email change still succeeds (the DB update already committed).

func (*App) ConfirmEmailVerificationOtpHandler added in v0.11.0

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

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

Stateless OTP verification

The request handler HMAC'd the 6-digit OTP into a signed JWT (the verification token) and returned it to the SDK. This handler receives the user-entered OTP and the token, verifies the JWT signature, and checks that the OTP matches. The server never stored the OTP — the signed token is the only proof of what was issued.

The password is not required here and cannot replace the verification token: the password proves identity, but it does not contain the OTP. Identity was already proven at the request step; what remains is proving possession of the email inbox (by entering the correct OTP).

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.
  • Account is already verified.

The only way to obtain a valid signed token is from our own request handler, which only issues tokens after verifying the correct password. The enumeration surface here is therefore narrower than at the request step.

func (*App) ConfirmPasswordResetOtpHandler added in v0.11.0

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

ConfirmPasswordResetOtpHandler handles the final step of password reset. Endpoint: POST /confirm-password-reset-otp Authenticated: No Allowed Mimetype: application/json

Flow

The user submits their new password alongside the `password_reset_grant_token` from Step 2. The server verifies the token, hashes the new password, updates the database, and returns a session token (auto-login).

# Security: User ID Enumeration & Timing attack mitigation The grant token payload contains the `UserID`. If the UserID is not found in the database, we silently initialize a dummy user hash (`crypto.DummyPasswordHash`). We then proceed unconditionally to the HMAC verification. This guarantees the request always takes the same CPU time, and we uniformly return `errorJwtInvalidVerificationToken` regardless of whether the UserID was fake or the signature was just bad.

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 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 (okPendingEmailVerificationOtp) 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 okPendingEmailVerificationOtp.

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

func (*App) RequestEmailChangeOtpHandler added in v0.11.0

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

RequestEmailChangeOtpHandler handles email change OTP requests. Endpoint: POST /api/request-email-change-otp Authenticated: Yes (requires valid auth token + password re-auth) Allowed Mimetype: application/json

# Flow Authenticated users request an email change by providing their current password and the new email address. The password is required as step-up authentication to protect against session hijack and physical access attacks. The server queues an email containing a 6-digit OTP to the new address and immediately returns a stateless `verification_token` (JWT) to the client. This token encapsulates the hashed OTP and the new email.

# Security: Step-up authentication The current password is required to prevent account takeover via stolen sessions or physical access to an unlocked device. This follows the industry standard (Google, Apple, Microsoft, GitHub all require password re-entry for email changes).

# Security: Enumeration hardening If the new email already belongs to another account, the handler enters the silent path: it still generates the OTP and JWT unconditionally, inserts a `JobTypeDummy` into the queue, and returns the same 200 response. The existence of the new email is never revealed.

# Security: Timing attack mitigation OTP generation and queue insertion are unconditional to guarantee constant time regardless of the new email's state.

func (*App) RequestEmailVerificationOtpHandler added in v0.11.0

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

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

Flow

Called by the SDK immediately after Register or Login Handler. The caller must supply the same password used during registration. This is the only gate that matters: a valid password proves the caller went to registration and is the account owner.

Stateless OTP via verification token

The 6-digit OTP is HMAC'd into a signed JWT (the verification token). The server stores nothing — no DB column, no cache, no session. On success the token is returned to the SDK, which holds it in memory and sends it back alongside the user-entered OTP at the confirm step.

The password cannot replace this role: it proves identity but does not contain the OTP. Without the signed token there is no stateless way to verify which 6-digit code was issued. The alternative would require a server-side otp_hash column, an expiration column, and a cleanup job — trading the stateless design for statefulness with no security gain.

Security: Persistent harassment

Requiring the correct password closes the harassment vector entirely. An attacker who knows a target email but not the password receives an error and no email is ever sent. The legitimate user is the only one who can trigger mail delivery — the password is proof of prior registration.

Unlike the register endpoint, CreateUserWithPassword never overwrites an existing password on conflict, so the real user's secret is always intact and cannot be poisoned by an attacker registering the same email.

Security: Enumeration & timing attacks

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: 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: errorWeakPassword response

The password validator returns a distinct errorWeakPassword before the DB lookup. This does not leak email existence — it only reveals that the input violates the password policy. The policy itself is not secret (the register handler exposes identical validation), so no information is gained.

Security: okAlreadyVerified response

The already-verified check returns a distinct okAlreadyVerified only after the password gate. An attacker who can reach this branch already has the correct password and could simply log in. No information is gained that the attacker does not already possess.

func (*App) RequestPasswordResetOtpHandler added in v0.11.0

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

RequestPasswordResetOtpHandler handles password reset OTP requests. Endpoint: POST /request-password-reset-otp Authenticated: No Allowed Mimetype: application/json

# Flow Unauthenticated users can request a password reset OTP. The server queues an email containing a 6-digit OTP and immediately returns a stateless `verification_token` (JWT) to the client. This token encapsulates the hashed OTP.

# Security: Harassment and Rate Limiting Because this endpoint sits completely unauthenticated and does not require the user's password, we cannot prevent harassment entirely. Our defense is strictly bounded by the queue Cooldown Bucket (e.g., 1 email every 5 minutes). Successive requests within the cooldown window hit a unique constraint and are silently ignored.

# Security: Enumeration hardening If we return errors for invalid states (e.g., email not found, unverified), an attacker can enumerate valid accounts. Therefore, this handler employs a "silent state machine": it sets a boolean flag and never returns early.

# Security: Timing attack mitigation If the email is fake, skipping the OTP cryptographic generation and the database INSERT would make the response extremely fast (~10ms) compared to a real user (~150ms). Mitigation 1: Unconditionally generate the OTP and its JWT. Mitigation 2: Unconditionally insert a job into the queue (`JobTypeDummy` for invalid states) to guarantee constant time disk/network execution.

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

func (*App) VerifyPasswordResetOtpHandler added in v0.11.0

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

VerifyPasswordResetOtpHandler handles the second step of password reset. Endpoint: POST /verify-password-reset-otp Authenticated: No Allowed Mimetype: application/json

# Flow The user submits the 6-digit OTP along with the `verification_token` from Step 1. If valid, the server returns a temporary `password_reset_grant_token` which authorizes the final password change.

# Security: Enumeration hardening This handler uniformly returns `errorInvalidOtp` if anything fails: bad token signature, expired token, incorrect OTP, or user not found.

# Security: The Password Hash Binding The generated `password_reset_grant_token` is signed using a composite key derived from the global `PasswordResetSecret` AND the user's *current password hash*. This guarantees that the exact millisecond the user successfully changes their password in Step 3, the hash changes, and the grant token instantly becomes cryptographically invalid, preventing any replay attacks.

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 PasswordResetOtpVerifiedData added in v0.11.0

type PasswordResetOtpVerifiedData struct {
	Token string `json:"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