HTTP Basic Authentication middleware for Go — RFC 7617 compliant.
- Standard
func(http.Handler) http.Handlermiddleware — 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-Authenticateheader to prevent injection attacks - Zero dependencies
go get github.com/nanoninja/bulma/v2package 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))
}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. |
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))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)
}// 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")
}))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.
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(),
})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.Successis required —Newwill panic if it is nil.
- Credentials are compared in constant time to prevent timing attacks (
SecureCompare). - The
WWW-Authenticaterealm 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.
Code is licensed under a BSD license.