Documentation
¶
Overview ¶
Package guard implements a cookie-backed, stateful session auth guard modelled on Laravel's session guard ("web" guard). It sits on top of the session package: where session persists arbitrary per-request data, guard adds the notion of an *authenticated user* — login, logout, current-user resolution, and route protection middleware.
It complements (does not replace) the stateless JWT in package auth. Use the JWT guard for APIs and SPAs that carry a bearer token; use this session guard for server-rendered apps that ride on an HttpOnly cookie.
Design rules (secure-by-default):
- The session stores only the user's opaque id, never the user record. The current user is re-resolved through the UserProvider per request and cached on the request context, so a single request hits the provider at most once.
- On a successful Attempt/Login the session ID is regenerated before the authenticated id is written, defeating session fixation: a pre-login ID an attacker may have planted is discarded.
- Logout regenerates and flushes the session so no authenticated remnant survives, and (via session.Destroy semantics) the stale cookie is not re-issued.
- Password verification goes through a pluggable Hasher (default: bcrypt). The interface keeps app models free to store any hash format and keeps the package free of a hard bcrypt dependency at the call site.
UserProvider is the single integration seam: implement it over whatever store backs your users (SQL, an ORM, an in-memory map) and the guard works unchanged.
Example ¶
Example shows the session guard end to end: Attempt with correct credentials logs the user in (Check()==true, User() returns the model), and a subsequent Logout clears the session (Check()==false). Requests are driven through the session middleware so a real session cookie carries the login across calls.
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/devituz/lagodev/auth/guard"
"github.com/devituz/lagodev/session"
"golang.org/x/crypto/bcrypt"
)
// exampleUser is a minimal user model satisfying guard.User.
type exampleUser struct {
id string
hash string
}
func (u exampleUser) AuthID() string { return u.id }
func (u exampleUser) AuthPasswordHash() string { return u.hash }
// exampleProvider is a tiny in-memory guard.UserProvider.
type exampleProvider struct {
byID map[string]guard.User
byCred map[string]guard.User
}
func (p *exampleProvider) FindByID(_ context.Context, id string) (guard.User, bool, error) {
u, ok := p.byID[id]
return u, ok, nil
}
func (p *exampleProvider) FindByCredentials(_ context.Context, identifier string) (guard.User, bool, error) {
u, ok := p.byCred[identifier]
return u, ok, nil
}
// Example shows the session guard end to end: Attempt with correct credentials
// logs the user in (Check()==true, User() returns the model), and a subsequent
// Logout clears the session (Check()==false). Requests are driven through the
// session middleware so a real session cookie carries the login across calls.
func main() {
hash, _ := bcrypt.GenerateFromPassword([]byte("s3cret"), bcrypt.MinCost)
user := exampleUser{id: "u1", hash: string(hash)}
prov := &exampleProvider{
byID: map[string]guard.User{"u1": user},
byCred: map[string]guard.User{"alice@example.com": user},
}
store := session.NewMemoryStore(time.Hour)
defer store.Close()
mgr := session.NewManager(store, session.Options{Insecure: true})
g := guard.New(mgr, prov, guard.Options{})
// run executes fn inside a request that has a session attached (via the
// session middleware) and returns the session cookie set on the response.
run := func(in *http.Cookie, fn func(w http.ResponseWriter, r *http.Request)) *http.Cookie {
rec := httptest.NewRecorder()
h := mgr.Middleware()(http.HandlerFunc(fn))
req := httptest.NewRequest(http.MethodGet, "/", nil)
if in != nil {
req.AddCookie(in)
}
h.ServeHTTP(rec, req)
for _, c := range rec.Result().Cookies() {
if c.Name == "lagodev_session" {
return c
}
}
return in
}
// 1. Log in with correct credentials.
cookie := run(nil, func(w http.ResponseWriter, r *http.Request) {
if _, err := g.Attempt(r.Context(), w, r, "alice@example.com", "s3cret"); err != nil {
panic(err)
}
})
// 2. Next request (carrying the cookie): authenticated.
cookie = run(cookie, func(w http.ResponseWriter, r *http.Request) {
fmt.Println("check after login:", g.Check(r))
u, err := g.User(r.Context(), r)
if err != nil {
panic(err)
}
fmt.Println("user id:", u.AuthID())
})
// 3. Log out.
cookie = run(cookie, func(w http.ResponseWriter, r *http.Request) {
if err := g.Logout(r.Context(), w, r); err != nil {
panic(err)
}
})
// 4. Next request: no longer authenticated.
run(cookie, func(w http.ResponseWriter, r *http.Request) {
fmt.Println("check after logout:", g.Check(r))
})
}
Output: check after login: true user id: u1 check after logout: false
Index ¶
- Variables
- type BcryptHasher
- type Guard
- func (g *Guard) Attempt(ctx context.Context, w http.ResponseWriter, r *http.Request, ...) (User, error)
- func (g *Guard) Check(r *http.Request) bool
- func (g *Guard) Guest() func(http.Handler) http.Handler
- func (g *Guard) ID(r *http.Request) string
- func (g *Guard) Login(ctx context.Context, w http.ResponseWriter, r *http.Request, user User) error
- func (g *Guard) Logout(ctx context.Context, w http.ResponseWriter, r *http.Request) error
- func (g *Guard) Middleware() func(http.Handler) http.Handler
- func (g *Guard) Remember() RememberStore
- func (g *Guard) User(ctx context.Context, r *http.Request) (User, error)
- type Hasher
- type Options
- type RememberStore
- type User
- type UserProvider
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrUnauthenticated is returned by User when there is no authenticated // user on the request (no session, no stored id, or the id no longer // resolves to a user). ErrUnauthenticated = errors.New("guard: unauthenticated") // ErrInvalidCredentials is returned by Attempt when the identifier is // unknown or the password does not match. The two cases are deliberately // indistinguishable to callers to avoid user enumeration. ErrInvalidCredentials = errors.New("guard: invalid credentials") // ErrNoSession is returned when guard is used on a request that has no // session attached (the session middleware was not applied). Without a // session there is nowhere to record the login. ErrNoSession = errors.New("guard: no session on request") )
Errors returned by Attempt and related flows.
Functions ¶
This section is empty.
Types ¶
type BcryptHasher ¶
type BcryptHasher struct{}
BcryptHasher is the default Hasher, backed by golang.org/x/crypto/bcrypt (already a project dependency, used by package auth for hashing).
func (BcryptHasher) Verify ¶
func (BcryptHasher) Verify(hash, plain string) bool
Verify reports whether plain matches the bcrypt hash.
type Guard ¶
type Guard struct {
// contains filtered or unexported fields
}
Guard ties a session.Manager to a UserProvider, providing login/logout and current-user resolution. A single Guard is shared across requests; per-request state (the resolved user) lives on the request context, so Guard itself holds no mutable request state and is safe for concurrent use.
func New ¶
func New(mgr *session.Manager, provider UserProvider, opts Options) *Guard
New returns a Guard. mgr and provider are required; opts is optional.
func (*Guard) Attempt ¶
func (g *Guard) Attempt(ctx context.Context, w http.ResponseWriter, r *http.Request, identifier, password string) (User, error)
Attempt verifies the credentials and, on success, logs the user in: it regenerates the session (fixation defense) and records the user id. It returns the authenticated user.
Errors:
- ErrInvalidCredentials when the identifier is unknown OR the password is wrong (the two are indistinguishable on purpose).
- ErrNoSession when no session is attached to the request.
- a wrapped provider/store error on infrastructure failure.
func (*Guard) Check ¶
Check reports whether a user id is present in the session. It is the cheap "is anyone logged in" test and, like ID, never calls the provider.
func (*Guard) Guest ¶
Guest returns the inverse middleware: it admits only unauthenticated requests (login/register pages). Authenticated requests get a 302 to HomePath when configured, else a 403.
func (*Guard) ID ¶
ID returns the authenticated user's id from the session, or "" when no user is logged in. It does not touch the provider.
func (*Guard) Login ¶
Login marks user as authenticated on the request's session. It regenerates the session ID first (so any pre-login fixation token is discarded) and then stores the user id. The session is persisted by the session middleware on the response write, or by an explicit Session.Save.
func (*Guard) Logout ¶
Logout clears the authenticated id and invalidates the session. It flushes the session contents and regenerates the ID so no authenticated remnant survives; the stale cookie is handled by the session layer. Remember tokens, if any, are the caller's responsibility (call Remember().Forget).
func (*Guard) Middleware ¶
Middleware returns a net/http middleware that requires an authenticated user. Unauthenticated requests get a 302 to LoginPath when configured, else a 401. It also installs the per-request user cache, so downstream handlers calling User do not re-query the provider.
It assumes the session middleware ran earlier in the chain (so a session is attached). Order: session.Middleware -> guard.Middleware -> handler.
func (*Guard) Remember ¶
func (g *Guard) Remember() RememberStore
Remember exposes the configured RememberStore (nil if unset), so callers that opt into remember-me can drive Issue/Lookup/Forget without re-plumbing it.
func (*Guard) User ¶
User resolves and returns the authenticated user, hitting the provider at most once per request (the result is cached on the request context by WithCachedUser, installed by Middleware). It returns ErrUnauthenticated when no id is stored or the stored id no longer resolves to a user (e.g. the account was deleted).
type Hasher ¶
type Hasher interface {
// Verify reports whether plain matches the stored hash. It must run in
// time independent of where a mismatch occurs (bcrypt and friends already
// do this).
Verify(hash, plain string) bool
}
Hasher verifies a plaintext password against a stored hash. The default is BcryptHasher; supply a custom one (argon2, scrypt, a legacy format) via Options.Hasher.
type Options ¶
type Options struct {
// Hasher verifies passwords. Defaults to BcryptHasher when nil.
Hasher Hasher
// Remember is the optional remember-me backend. Nil disables the hook.
Remember RememberStore
// LoginPath, when non-empty, makes Middleware redirect unauthenticated
// requests there (302) instead of returning 401. Guest does the inverse
// with HomePath.
LoginPath string
// HomePath, when non-empty, makes Guest redirect already-authenticated
// requests there instead of returning 403.
HomePath string
}
Options configures a Guard. The zero value is usable once a Manager and a UserProvider are supplied: Hasher defaults to BcryptHasher and the redirect is empty (Middleware then answers 401 rather than redirecting).
type RememberStore ¶
type RememberStore interface {
// Issue persists a new remember token for userID and returns the opaque
// plaintext to set in a client cookie.
Issue(ctx context.Context, userID string) (token string, err error)
// Lookup resolves a remember token to a user id. ok=false on miss/expiry.
Lookup(ctx context.Context, token string) (userID string, ok bool, err error)
// Forget invalidates a remember token (called on logout).
Forget(ctx context.Context, token string) error
}
RememberStore is an optional hook for "remember me": long-lived tokens that re-authenticate a user after the session cookie expires. It is intentionally minimal — the guard does not wire it into the cookie lifecycle automatically; applications that want remember-me drive Issue/Lookup/Forget themselves and call Login on a successful token lookup. Leave Options.Remember nil to skip.
type User ¶
type User interface {
// AuthID returns the opaque, stable identifier persisted in the session.
AuthID() string
// AuthPasswordHash returns the stored password hash for credential checks.
// It may be "" for users that authenticate by other means; Attempt then
// fails with ErrInvalidCredentials.
AuthPasswordHash() string
}
User is the minimal contract an application's user model must satisfy. It is intentionally tiny so any model fits: implement two methods and the guard can authenticate it.
type UserProvider ¶
type UserProvider interface {
// FindByID resolves a user by the identifier returned from AuthID. It is
// called once per request to rehydrate the authenticated user.
FindByID(ctx context.Context, id string) (User, bool, error)
// FindByCredentials resolves a user by a login identifier (email,
// username, ...). The password is NOT checked here — the guard compares
// the hash via its Hasher — so implementations must not leak whether the
// password matched.
FindByCredentials(ctx context.Context, identifier string) (User, bool, error)
}
UserProvider resolves users for the guard. Implementations back onto the app's user store. Both methods return ok=false (not an error) for a clean "no such user" miss; a non-nil error signals an infrastructure failure (store unreachable, query error).