Documentation
¶
Overview ¶
Package auth signs users in to a web app and guards its routes, live pages and live events.
The app writes one interface, Users; auth does the rest: password hashing, cookie sessions for pages, bearer tokens for APIs, login throttling, revocation, and gates that refuse every route nobody marked public:
app := web.New(auth.Module(auth.Config{Users: NewUsers}))
@web.Get("/orders")
@auth.Requires("orders.read")
func Orders(me *auth.Identity, store *Store) Result[[]Order] { … }
@web.Get("/login")
@auth.Public
func Login(r *http.Request) ui.Node { return auth.LoginPage(auth.LoginPageProps{Request: r}) }
See the README's Authentication section, and the docs' auth page.
Index ¶
- Constants
- Variables
- func Allowed(ctx context.Context, perm string, obj any) bool
- func Can(ctx context.Context, perm string) bool
- func Check(ctx context.Context, perm string, obj any) error
- func CheckPassword(encoded, password string, cost Argon2) (ok, stale bool, err error)
- func HashPassword(password string) (string, error)
- func IssueToken(ctx context.Context, uid, name string, ttl time.Duration) (string, error)
- func LoginError(r *http.Request) string
- func LoginForm(p LoginFormProps) ui.Node
- func LoginMessage(err error) string
- func LoginPage(p LoginPageProps) ui.Node
- func Module(cfg Config) web.Option
- func Next(r *http.Request, home string) string
- func Public() vuka.Bundle
- func Refresh(ctx context.Context, uid string) error
- func Required() vuka.Bundle
- func Requires(perms ...string) vuka.Bundle
- func RequiresAny(perms ...string) vuka.Bundle
- func Revoke(ctx context.Context, token string) error
- func RevokeUser(ctx context.Context, uid string) error
- func SafeNext(next string) bool
- func SetPassword(ctx context.Context, uid, password string) error
- func SignIn(ctx context.Context, uid string) error
- func SignOut(ctx context.Context) error
- func System(ctx context.Context) context.Context
- func WithIdentity(ctx context.Context, id *Identity) context.Context
- type Argon2
- type Auth
- func (a *Auth) Issue(ctx context.Context, uid, name string, ttl time.Duration) (string, Token, error)
- func (a *Auth) Refresh(uid string)
- func (a *Auth) Revoke(ctx context.Context, token string) error
- func (a *Auth) RevokeUser(ctx context.Context, uid string) error
- func (a *Auth) SetPassword(ctx context.Context, uid, password string) error
- func (a *Auth) Verify(ctx context.Context, r *http.Request, login, password string) (id *Identity, err error)
- type Config
- type Gate
- type Hasher
- type Identity
- type JWT
- type LoginFormProps
- type LoginPageProps
- type Me
- type MemorySessions
- type MemoryTokens
- type Policy
- type Session
- type SessionStore
- type Throttle
- type ThrottledError
- type Token
- type TokenResponse
- type TokenStore
- type Users
Constants ¶
const TokenPrefix = "vka_"
TokenPrefix starts every bearer token auth issues, so a leaked one is recognisable (secret scanners, logs).
Variables ¶
var Argon2Defaults = Argon2{Memory: 19 * 1024, Time: 2, Threads: 1}
Argon2Defaults are OWASP's recommended minimum for Argon2id: 19 MiB, two passes, one thread — some tens of milliseconds per hash.
var ErrInvalidLogin = &web.Error{Status: http.StatusUnauthorized, Message: "invalid login or password"}
ErrInvalidLogin is a login's error whether the login is unknown or the password wrong: the answer never says which.
var ThrottleDefaults = Throttle{PerLogin: 5, PerIP: 50, Window: 15 * time.Minute}
ThrottleDefaults: 5 failures per login and 50 per address in 15 minutes.
Functions ¶
func Check ¶
Check is nil when ctx's identity may do perm to obj: by the policy of obj's type (Config.Policies) — a policy for Order decides for an Order and a *Order alike, and one for *Order for both too — else by its perms. Otherwise it is web.Unauthorized for an anonymous caller and web.Forbidden for one signed in, which a handler returns as is.
A context marked auth.System passes. One without the app's auth (an auth.WithIdentity of a context from outside any request) can't ask the app's policies: Check refuses it any object of a type a policy covers, rather than decide by the perms alone.
func CheckPassword ¶
CheckPassword reports whether password is the one encoded, and whether the encoding is stale — another algorithm (bcrypt), or a cost lower than cost — so that a login can store a new hash. A malformed encoding is an error.
func HashPassword ¶
HashPassword encodes a password for storage with Argon2id and the default cost: "$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>".
func IssueToken ¶
IssueToken makes a bearer token for user uid, as Auth.Issue does, for the app of ctx.
func LoginError ¶
LoginError is the message for the login page's ?error= code, which POST /auth/login sends back with: "" when there is none.
func LoginForm ¶
func LoginForm(p LoginFormProps) ui.Node
LoginForm is a sign-in form posting login and password to the login endpoint (Config.Endpoints):
<auth.LoginForm next={r.URL.Query().Get("next")} error={auth.LoginError(r)} />
Replace it with your own markup when you like: the endpoint reads the fields login, password and next.
func LoginMessage ¶ added in v0.14.0
LoginMessage is the message a login page shows for Login's error, for an app's own login handler re-rendering its form: the same for an unknown login and a wrong password, the throttle's, a refusal's own message (a *web.Error CheckLogin returned), and a general one for anything else (the error itself is the app's to log). "" for nil.
func LoginPage ¶
func LoginPage(p LoginPageProps) ui.Node
LoginPage is a whole sign-in page around a LoginForm, for an app's login route:
@web.Get("/login")
@auth.Public
func Login(r *http.Request) ui.Node { return <auth.LoginPage request={r} /> }
func Next ¶
Next is where a sign-in from r goes: its next parameter (a form field, else the query's) when SafeNext, else home.
func Public ¶
Public opens a route, a live page or a controller's route to everyone, signed in or not: a login page, a landing page, a health check. On a route of a guarded controller it opens that route only.
@web.Get("/login")
@auth.Public
func Login(r *http.Request) ui.Node { … }
From Go: app.Get("/login", h, web.Attrs(auth.Public())).
func Refresh ¶ added in v0.18.0
Refresh forgets the cached identity of user uid, as Auth.Refresh does, for the app of ctx.
func Required ¶
Required needs a sign-in: what every route needs already unless Config.PublicByDefault, written to say so. On a live event method of a public page, the event is refused to anonymous visitors.
func Requires ¶
Requires needs a sign-in with every one of perms (wildcards in the identity's perms count: "orders.*" grants "orders.write"). A visitor signed in without them gets 403. On a live event method it refuses the event; on a controller or a live page's type, every route or event of it.
func RequiresAny ¶
RequiresAny needs a sign-in with at least one of perms.
func RevokeUser ¶
RevokeUser ends every sign-in of user id, as Auth.RevokeUser does, for the app of ctx.
func SafeNext ¶
SafeNext reports whether next may be where a sign-in sends the browser: a path of this site ("/orders?page=2"), never another site — no scheme or host, no "//" or "/\" a browser reads as one, no backslash or control character anywhere, before or after decoding.
func SetPassword ¶
SetPassword hashes password and stores it for user uid with Users.SetPassword. Sessions and tokens the user has stay; RevokeUser ends them.
func SignIn ¶
SignIn signs ctx's browser in as the identity whose id is uid: a new session, under a new cookie id — any session the browser had is ended first, so an id planted before the sign-in is worthless after it. Call it from a handler of an HTTP request (a form post), before writing the response. Login checks a password first; SignIn is for flows that proved who the user is some other way.
func SignOut ¶
SignOut ends ctx's sign-in: the session is deleted and its cookie expired, or the bearer token the request carries is revoked. The live pages signed in with it close and reload.
func System ¶ added in v0.13.0
System is ctx marked as the app's own work — a job, a migration, main; web's crons run with it — which gated methods given it, and auth.Check, let through, whoever ctx's identity is. A gated call is refused without an identity or this mark (unless Config.LaxGates). Never mark a context a request's work goes on with.
Types ¶
type Auth ¶
type Auth struct {
// contains filtered or unexported fields
}
Auth is an app's authentication: Module provides it, for code without a request's context (jobs, the CLI) — revoking a user, issuing a token.
func (*Auth) Issue ¶
func (a *Auth) Issue(ctx context.Context, uid, name string, ttl time.Duration) (string, Token, error)
Issue makes a bearer token for user uid, lasting ttl (Config.TokenTTL when 0). The token is returned once: the store keeps only its hash.
func (*Auth) Refresh ¶ added in v0.18.0
Refresh forgets the identity Config.UserCache keeps for user uid, so the next request loads it with Users.Load: call it after changing a user's perms, roles or record. It only reaches this process's cache.
func (*Auth) Revoke ¶
Revoke ends a bearer token: requests carrying it are refused from now on, and the live pages signed in with it close.
func (*Auth) RevokeUser ¶
RevokeUser ends every sign-in of user id: its sessions and bearer tokens are deleted, JWTs issued before now are refused by this process, and its live pages in this process close at once (and load again, signed out); another process's at their next web.LiveCheckEvery check.
func (*Auth) SetPassword ¶
SetPassword hashes password and stores it for user uid.
func (*Auth) Verify ¶
func (a *Auth) Verify(ctx context.Context, r *http.Request, login, password string) (id *Identity, err error)
Verify checks a login and password, as Login does, without signing in: the token endpoint's first step. r, when not nil, is the request the attempt came with, whose address throttling counts.
A failure takes as long as any other: an unknown login is checked against a hash like the ones logins found lately have, and no failure returns before Config.FailedLoginTime.
type Config ¶
type Config struct {
// Users is the app's Users: a value, or a constructor the app's
// container builds (func(deps…) Users). Nil: the one provided type
// implementing Users, such as a @web.Service.
Users any
// PublicByDefault makes routes without a gate public, as they are
// without auth: only those marked Required or Requires need a sign-in.
// By default every route, live page and live event needs one unless
// marked @auth.Public.
PublicByDefault bool
// LoginPath is where a page visit without a sign-in is sent, with
// ?next=<the page>; "/login" by default. The app serves it (LoginPage
// is ready to), and marks it @auth.Public.
LoginPath string
// Home is where a sign-in goes without a next; "/" by default.
Home string
// Endpoints mounts the login, logout, me and token endpoints under a
// prefix ("/auth": POST /auth/login, POST /auth/logout, GET /auth/me,
// POST /auth/token); none when empty.
Endpoints string
// Roles maps a role to its perms: Identity.Roles adds them.
Roles map[string][]string
// Sessions holds cookie sessions; Tokens, bearer tokens. Both are in
// memory by default: lost on restart, and not shared between replicas
// (see MemorySessions).
Sessions SessionStore
Tokens TokenStore
// SessionTTL is how long a session lasts from sign-in; 14 days by
// default. SessionIdle, when set, ends a session not used for that
// long. TokenTTL is how long a bearer token lasts; 30 days by default.
SessionTTL, SessionIdle, TokenTTL time.Duration
// RememberMe makes staying signed in the user's choice at the login
// endpoint: a sign-in whose form has the remember field checked (or
// whose JSON has "remember": true) keeps its session for SessionTTL in
// a cookie that outlives the browser; any other gets a cookie that
// ends with the browser session, and a session of a day at most.
// LoginForm's Remember shows the checkbox. Off, every sign-in is
// remembered.
RememberMe bool
// Cookie names the session cookie; "vk_session" by default.
Cookie string
// SecureCookie sets the cookie's Secure flag on every response, not
// only on HTTPS requests (r.TLS, or X-Forwarded-Proto: https): set it
// when TLS ends at a proxy that doesn't say so.
SecureCookie bool
// Passwords are the hashing parameters of new hashes; Argon2id's
// defaults (Argon2Defaults) when zero.
Passwords Argon2
// Hashers read password encodings auth doesn't (another system's,
// FindLogin's encoded password as it stored it): a login checks the
// first that knows the encoding. A match is stale, so the login
// stores a fresh hash when Users has SetPassword.
Hashers []Hasher
// Throttle limits failed logins; ThrottleDefaults when zero.
Throttle Throttle
// FailedLoginTime is the least time a failed login takes, so that an
// unknown login, a wrong password, a stored hash of another cost or
// encoding and a refused attempt look alike; 300ms by default,
// negative for none.
FailedLoginTime time.Duration
// ClientIP is the address throttling counts a request against;
// web.ClientIP by default: the connection's peer, or, behind the app's
// web.TrustedProxies, the address they pass on.
ClientIP func(r *http.Request) string
// JWT verifies bearer JWTs issued elsewhere; nil: none accepted.
JWT *JWT
// Policies decide Check for the types they name.
Policies []Policy
// TrustedOrigins may send cross-origin POSTs and the like (web.CSRF).
TrustedOrigins []string
// LaxGates lets a gated method's call that nothing attributes to an
// identity run, as the app's own work: no request's or live page's
// context, no live event running it, no auth.WithIdentity or
// auth.System. By default (StrictGates) such a call is refused: code
// outside requests (main, a job; crons are marked by themselves) marks
// its context with auth.System(ctx), and gated methods take a
// context.Context to be given one. Whatever the setting, a call on a
// goroutine serving a request is checked against the request's
// identity, never run as the app's own. It holds for the whole process
// once an app with it is built.
LaxGates bool
// StrictGates is the default; set it to keep an app strict whatever
// another app of the process says (LaxGates).
StrictGates bool
// LiveRecheck is how often a live page loads its identity again
// (Users.Load) to see that the user is still there with the same perms
// and roles; a page whose identity changed reloads, signed in afresh
// or sent to the login page. 60 seconds by default; negative for never.
// It is looked at when web.LiveCheckEvery checks the page.
//
// A live page's sign-in is checked when the page mounts: its join and
// each navigation run its route, which reads the session or token. Its
// events trust the identity it mounted with. A sign-in ended through
// auth — SignOut, Revoke, RevokeUser, a new SignIn replacing the
// browser's session, a session found expired — closes its pages at
// once (web's CloseLive); one ended elsewhere (a store row deleted by
// hand, another process) is caught by the check every
// web.LiveCheckEvery (30 seconds by default).
LiveRecheck time.Duration
// UserCache keeps the identity Users.Load returns for a user this long,
// so a request signed in with a session or token reads only the
// session or token: 0 (the default) loads it on every request. A
// cached identity is dropped when RevokeUser, SetPassword or SignOut
// touch its user, when Load finds the user gone, and when the app
// calls Refresh after changing a user's perms, roles or record; a sign-in
// and LiveRecheck always load afresh. The cache is the process's own:
// another replica keeps its copy until it expires.
UserCache time.Duration
}
Config configures Module. Every field is optional but Users, which may also come from the app's container.
type Gate ¶
type Gate struct {
// contains filtered or unexported fields
}
Gate is what a route, a live page or a live event needs of its caller: the typed attribute Public, Required, Requires and RequiresAny put on a declaration. Without one, a route needs a sign-in (unless Config.PublicByDefault).
func (Gate) RouteWrapper ¶
RouteWrapper makes a route carrying a gate fail Build when the app has no auth.Module.
type Hasher ¶ added in v0.14.0
type Hasher interface {
// Check reports whether password is the one encoded; known is false
// when encoded isn't in this hasher's encoding.
Check(encoded, password string) (ok, known bool)
}
A Hasher checks passwords stored in an encoding auth doesn't read: another system's, so its users sign in with the passwords they had there (Config.Hashers).
A Hasher may also have a Dummy() string method returning a value in its encoding, of the cost its stored values have: an unknown login is checked against it when the logins found last were in this encoding, so the two take the same time. Without one, Config.FailedLoginTime alone evens them out.
type Identity ¶
type Identity struct {
ID string // the user's id, as Users.Load takes it
// Perms are what the user may do: "orders.write", or a wildcard,
// "orders.*" (everything under orders) or "*" (everything).
Perms []string
// Roles name sets of perms (Config.Roles), added to Perms when the
// identity is loaded.
Roles []string
User any // the app's own user record, for handlers; never sent anywhere by auth
}
Identity is who a request comes from.
func Current ¶
Current is the identity of ctx's request — a handler's, a live page's Mount, a live event's — or nil for an anonymous one.
func Login ¶
Login checks a login and password and signs ctx's browser in (SignIn): the identity, or ErrInvalidLogin — the same error, after the same work, for an unknown login and a wrong password — or a *ThrottledError, or what Users.CheckLogin refused with. A password stored with a stale hash is stored again with a fresh one when Users has SetPassword.
type JWT ¶
type JWT struct {
Key []byte // HS256's secret, at least 32 bytes
PublicKey *rsa.PublicKey // RS256's key
Issuer string // the "iss" a token must carry, when set
Audience string // an "aud" a token must list, when set
Leeway time.Duration // clock skew allowed on exp and nbf; a minute when 0
}
JWT verifies bearer JSON Web Tokens another service issued (an identity provider, a gateway): HS256 with a shared Key, or RS256 with PublicKey — one of them, which also pins the algorithm a token must name. The token's subject (the "sub" claim) is the user id Users.Load takes; its "exp" is required. auth never issues JWTs: its own tokens are opaque (Issue).
type LoginFormProps ¶
type LoginFormProps struct {
Action string // where it posts; "/auth/login" by default
Next string // where to go once signed in (SafeNext), else Config.Home
Error string // a message to show above the fields
Login string // the login field's value: what was typed, on a re-render
// Remember adds a "Remember me" checkbox, the remember field
// Config.RememberMe reads.
Remember bool
Class string
}
LoginFormProps are a LoginForm's attributes.
type LoginPageProps ¶
type LoginPageProps struct {
// Request fills the form: on a GET its ?next= and ?error=; on a form
// post (an app's own login handler re-rendering the page) the posted
// login and next, so what was typed stays.
Request *http.Request
Title string // "Sign in" by default
Action string // LoginForm's
Error string // the message to show, over Request's ?error= (LoginMessage)
// Remember is LoginForm's: a "Remember me" checkbox.
Remember bool
}
LoginPageProps are a LoginPage's attributes.
type Me ¶
type Me struct {
ID string `json:"id"`
Roles []string `json:"roles,omitempty"`
Perms []string `json:"perms,omitempty"`
User any `json:"user,omitempty"`
}
Me is what GET /auth/me answers: the identity's id, roles and perms, and what Users.Public returns of the user, when Users has that method.
type MemorySessions ¶
type MemorySessions struct {
// contains filtered or unexported fields
}
MemorySessions is the default SessionStore: a map in the process. Its sessions end when the process does — every user signs in again after a restart or a deploy — and replicas don't share them, so behind a load balancer a user is signed in on one replica only. Use it for development, tests and single-process apps that accept that; otherwise implement SessionStore over the app's database or cache.
func NewMemorySessions ¶
func NewMemorySessions() *MemorySessions
NewMemorySessions is an empty MemorySessions.
func (*MemorySessions) Delete ¶
func (s *MemorySessions) Delete(_ context.Context, key string) error
func (*MemorySessions) DeleteUser ¶
func (s *MemorySessions) DeleteUser(_ context.Context, uid string) error
type MemoryTokens ¶
type MemoryTokens struct {
// contains filtered or unexported fields
}
MemoryTokens is the default TokenStore: a map in the process. Its tokens stop working when the process ends — after a restart or a deploy every client needs a new one — and replicas don't share them. Use it for development and tests; otherwise implement TokenStore over the app's database.
func NewMemoryTokens ¶
func NewMemoryTokens() *MemoryTokens
NewMemoryTokens is an empty MemoryTokens.
func (*MemoryTokens) DeleteUser ¶
func (s *MemoryTokens) DeleteUser(_ context.Context, uid string) error
type Policy ¶
type Policy struct {
// contains filtered or unexported fields
}
Policy decides Check for one type of object: may this identity do perm to this order?
func PolicyFor ¶
PolicyFor is the policy of objects of type T: rule decides alone, so it grants what the perms don't (an owner editing their own order) and refuses what they grant, reading id.Can itself where perms matter. id is never nil: anonymous callers are refused before rule runs.
auth.PolicyFor(func(ctx context.Context, id *auth.Identity, perm string, o *Order) bool {
return id.Can(perm) || o.Owner == id.ID
})
type Session ¶
type Session struct {
UserID string
Created time.Time
Seen time.Time // last used, within a minute
Expires time.Time
}
Session is a signed-in browser, as a SessionStore keeps it. The cookie holds a random id; the store holds the SHA-256 of it, so reading the store gives nobody a cookie that works.
type SessionStore ¶
type SessionStore interface {
Get(ctx context.Context, key string) (*Session, error)
Save(ctx context.Context, key string, s Session) error
Delete(ctx context.Context, key string) error
DeleteUser(ctx context.Context, uid string) error
}
SessionStore keeps sessions by key, the hash of their cookie's id. Get returns nil and no error for an unknown key. DeleteUser deletes every session of a user.
type Throttle ¶
Throttle limits failed logins: after PerLogin failures for one login, or for the account a login finds, or PerIP from one address, within Window, further attempts are refused (429) until the oldest failure is Window old. Counts are per process.
type ThrottledError ¶
ThrottledError is a login's error once too many attempts failed.
func (*ThrottledError) Error ¶
func (e *ThrottledError) Error() string
func (*ThrottledError) Unwrap ¶
func (e *ThrottledError) Unwrap() error
Unwrap makes it a 429 to web.
type Token ¶
type Token struct {
UserID string
Name string // what it is for: "cli", "ci"
Created time.Time
Expires time.Time
}
Token is a bearer token as a TokenStore keeps it: the SHA-256 of the token, never the token itself, which only its holder has.
type TokenResponse ¶
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
}
TokenResponse is what POST /auth/token answers, as OAuth 2's token endpoint does.
type TokenStore ¶
type TokenStore interface {
Get(ctx context.Context, key string) (*Token, error)
Save(ctx context.Context, key string, t Token) error
Delete(ctx context.Context, key string) error
DeleteUser(ctx context.Context, uid string) error
}
TokenStore keeps bearer tokens by key, the hash of the token. Get returns nil and no error for an unknown key. DeleteUser deletes every token of a user.
type Users ¶
type Users interface {
FindLogin(ctx context.Context, login string) (*Identity, string, error)
Load(ctx context.Context, id string) (*Identity, error)
}
Users is the app's side: where identities and their passwords live.
FindLogin returns the identity whose login (a username, an email) is login, with its encoded password (HashPassword's output); nil and no error when there is none. Load returns the identity of a user id, as a session or token names it; nil and no error when the user is gone, which signs the session out.
Two methods are optional: SetPassword(ctx, id, encoded string) error stores a password auth re-hashed (a login with a stale hash upgrades it) or SetPassword was given; CheckLogin(ctx, *Identity) error refuses a login whose password was right (an account disabled, unconfirmed): its error is shown as is when it is a *web.Error, else as the usual "invalid login or password". And Public(*Identity) any, when present, is what GET /auth/me shows of the user.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package authtest helps test apps using auth: requests and live sessions signed in as anyone, without a login, and Users kept in memory.
|
Package authtest helps test apps using auth: requests and live sessions signed in as anyone, without a login, and Users kept in memory. |