58 lines
2.3 KiB
Go
58 lines
2.3 KiB
Go
// Package auth is the session layer: ONE server-side session (D39.84), presented either as a
|
|
// __Host cookie by the browser or as a Bearer token by the desktop and CLI clients. No handler may
|
|
// look at either — the principal is created in middleware and read from the request context.
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// CookieName is the browser presentation. The __Host prefix is not decoration: it forbids a Domain
|
|
// attribute and requires Secure + Path=/, which is what stops a sibling subdomain from writing a
|
|
// session cookie for the app.
|
|
const CookieName = "__Host-tm_session"
|
|
|
|
// tokenBytes is 256 bits of entropy (STACK_DECISIONS §5). Opaque: it encodes nothing, so a stolen
|
|
// token cannot be read, and a JWT's "verify without the database" property is exactly what we do
|
|
// not want — revocation must be immediate.
|
|
const tokenBytes = 32
|
|
|
|
// ErrNoSession is returned when nothing live matches the presented token. Callers must not
|
|
// distinguish "unknown token" from "expired token" on the wire: the difference is an oracle.
|
|
var ErrNoSession = errors.New("auth: no live session")
|
|
|
|
// Session is what the store knows about a live session.
|
|
type Session struct {
|
|
UserID string
|
|
IdleExpiresAt time.Time
|
|
AbsoluteExpiresAt time.Time
|
|
}
|
|
|
|
// SessionStore is the persistence the middleware needs. Lookup MUST apply expiry and revocation
|
|
// itself, so a store that forgets a clause cannot be papered over here.
|
|
type SessionStore interface {
|
|
Lookup(ctx context.Context, digest []byte, now time.Time) (Session, error)
|
|
Touch(ctx context.Context, digest []byte, now time.Time, idleTTL time.Duration) error
|
|
}
|
|
|
|
// NewToken mints a credential. The plaintext exists only in this return value and in the client:
|
|
// what reaches the database is Digest(token). No error return — crypto/rand.Read never fails, it
|
|
// crashes the program instead.
|
|
func NewToken() string {
|
|
b := make([]byte, tokenBytes)
|
|
rand.Read(b)
|
|
return base64.RawURLEncoding.EncodeToString(b)
|
|
}
|
|
|
|
// Digest is the stored form. Plain SHA-256 with no salt or stretching is correct HERE and only
|
|
// here: the input is 256 uniform random bits, so there is no guessable space to slow an attacker
|
|
// down in — unlike a password.
|
|
func Digest(token string) []byte {
|
|
sum := sha256.Sum256([]byte(token))
|
|
return sum[:]
|
|
}
|