Skip to content

nanoninja/bulma

Repository files navigation

Bulma

HTTP Basic Authentication middleware for Go — RFC 7617 compliant.

Go License Go Reference CI Go Report Card

Features

  • Standard func(http.Handler) http.Handler middleware — composes with any router
  • Constant-time credential comparison to prevent timing attacks
  • Context-aware validator interface — propagates request deadlines to I/O calls
  • Authenticated credentials available downstream via FromContext
  • Sanitized WWW-Authenticate header to prevent injection attacks
  • Zero dependencies

Installation

go get github.com/nanoninja/bulma/v2

Quick start

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/nanoninja/bulma/v2"
)

func dashboard(w http.ResponseWriter, r *http.Request) {
    c, _ := bulma.FromContext(r.Context())
    fmt.Fprintf(w, "Welcome, %s", c.Username)
}

func main() {
    auth := bulma.BasicAuth(bulma.User{
        "alice": "s3cr3t",
        "bob":   "p4ssw0rd",
    })

    http.Handle("/admin", auth(http.HandlerFunc(dashboard)))
    log.Fatal(http.ListenAndServe(":3000", nil))
}

Options

BasicAuth accepts functional options to customize its behavior:

auth := bulma.BasicAuth(validator,
    bulma.WithRealm("MyApp"),
    bulma.WithFailure(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        http.Error(w, "Access denied", http.StatusUnauthorized)
    })),
)
Option Description
WithRealm(string) Sets the protection space in the WWW-Authenticate header. Defaults to "Authorization Required".
WithFailure(http.Handler) Custom handler called on authentication failure. Defaults to a plain 401 response.
WithCharset(string) Adds a charset parameter to the WWW-Authenticate header (RFC 7617 §2.1). Use "UTF-8" to hint clients to encode credentials as UTF-8 before Base64.

Composing middleware

Since BasicAuth returns a func(http.Handler) http.Handler, it integrates naturally with any router or middleware chain:

auth := bulma.BasicAuth(validator)

http.Handle("/admin",   auth(adminHandler))
http.Handle("/api/v1",  auth(apiHandler))
http.Handle("/metrics", auth(metricsHandler))

Accessing credentials

After a successful authentication, the credentials are available in the request context via FromContext. This avoids re-parsing the Authorization header in downstream handlers.

func dashboard(w http.ResponseWriter, r *http.Request) {
    c, ok := bulma.FromContext(r.Context())
    if !ok {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }
    fmt.Fprintf(w, "Welcome, %s", c.Username)
}

Validators

Built-in

// Single user
auth := bulma.BasicAuth(bulma.Auth("alice", "s3cr3t"))

// Multiple users (plain text passwords — simple use cases only)
auth := bulma.BasicAuth(bulma.User{
    "alice": "s3cr3t",
    "bob":   "p4ssw0rd",
})

// Inline function
auth := bulma.BasicAuth(bulma.ValidateFunc(func(_ context.Context, c bulma.Credential) bool {
    return bulma.SecureCompare(c.Username, "alice") &&
        bulma.SecureCompare(c.Password, "s3cr3t")
}))

Combining validators

AnyOf accepts credentials if any of its validators returns true. Validators are tried in order and evaluation stops at the first match.

A typical use case is credential migration: during a transition between two authentication systems, run both validators in parallel so users from either system are accepted without changing handler code.

auth := bulma.BasicAuth(bulma.AnyOf{
    newDBValidator,  // new system — checked first
    oldDBValidator,  // legacy system — fallback during migration
})

Once the migration is complete, remove the legacy validator from the slice.

Custom validator

Implement the Validator interface to connect any credential source:

type Validator interface {
    Validate(context.Context, Credential) bool
}

The context.Context carries the request deadline and cancellation — pass it to every I/O call to avoid hanging on slow or unavailable backends.

Use bulma.SecureCompare for constant-time string comparison. For hashed passwords, prefer bcrypt.CompareHashAndPassword which is timing-safe by design.

import (
    "context"
    "database/sql"
    "log/slog"

    "github.com/nanoninja/bulma/v2"
    "golang.org/x/crypto/bcrypt"
)

type DBValidator struct {
    db     *sql.DB
    logger *slog.Logger
}

func (v DBValidator) Validate(ctx context.Context, c bulma.Credential) bool {
    var hash string

    // Note: placeholder syntax depends on your driver (? MySQL, $1 PostgreSQL)
    err := v.db.QueryRowContext(ctx,
        "SELECT password_hash FROM users WHERE username = ?",
        c.Username,
    ).Scan(&hash)
    if err != nil {
        // Return false for both "not found" and query errors to avoid
        // leaking information about existing usernames
        v.logger.Error("auth query failed", "error", err)
        return false
    }

    return bcrypt.CompareHashAndPassword([]byte(hash), []byte(c.Password)) == nil
}
auth := bulma.BasicAuth(DBValidator{
    db:     db,
    logger: slog.Default(),
})

Advanced configuration

For full control over the handler lifecycle, use New directly with a Config:

ba := bulma.New(bulma.Config{
    Realm:     "MyApp",
    Validator: bulma.Auth("alice", "s3cr3t"),
    Success:   dashboardHandler,
    Failure:   loginErrorHandler,
})

Config.Success is required — New will panic if it is nil.

Security notes

  • Credentials are compared in constant time to prevent timing attacks (SecureCompare).
  • The WWW-Authenticate realm is sanitized to prevent header injection.
  • Validators always receive the request context — use QueryRowContext, DialContext, etc. to ensure authentication cannot block a request beyond its deadline.
  • HTTP Basic Auth transmits credentials as Base64 — always serve over HTTPS in production.
  • Prefer hashed passwords (bcrypt, argon2) over plain text in any persistent store.

License

Code is licensed under a BSD license.

About

Implementation of Basic HTTP authentication middleware for Go language.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages