Initial commit
This commit is contained in:
commit
afed023152
8152 files changed
+2086461
No files matched your search
@@ -0,0 +1,137 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cookie names. The login-tx cookie is scoped to /api/auth so it is only
|
||||
// sent to the endpoints that need it.
|
||||
const (
|
||||
CookieSession = "fs_session"
|
||||
CookieLoginTx = "fs_auth_tx"
|
||||
)
|
||||
|
||||
// Session is the signed-in identity kept in the session cookie. It is a
|
||||
// self-contained, HMAC-protected claim set — not a bearer token: it carries
|
||||
// no issuer credentials and only opens this API.
|
||||
type Session struct {
|
||||
Sub string
|
||||
Name string
|
||||
Email string
|
||||
Roles []string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// Valid reports whether the session is still within its TTL.
|
||||
func (s Session) Valid() bool { return time.Now().Before(s.ExpiresAt) }
|
||||
|
||||
// ErrTampered is returned when a cookie value fails its HMAC check or is
|
||||
// malformed — treat it exactly like a missing cookie.
|
||||
var ErrTampered = errors.New("auth cookie failed integrity check")
|
||||
|
||||
// Signer seals values into "<base64url(json)>.<base64url(hmac-sha256)>"
|
||||
// cookie payloads and opens them back. Construction is cheap; keep one
|
||||
// per server and share it.
|
||||
type Signer struct{ key []byte }
|
||||
|
||||
// NewSigner returns a Signer keyed by secret.
|
||||
func NewSigner(secret string) *Signer { return &Signer{key: []byte(secret)} }
|
||||
|
||||
// Seal encodes v into a signed cookie value.
|
||||
func (s *Signer) Seal(v any) (string, error) {
|
||||
payload, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal cookie payload: %w", err)
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(payload) + "." + s.mac(payload), nil
|
||||
}
|
||||
|
||||
// Open verifies a sealed value and decodes it into v.
|
||||
func (s *Signer) Open(sealed string, v any) error {
|
||||
value, mac, ok := strings.Cut(sealed, ".")
|
||||
if !ok {
|
||||
return ErrTampered
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(value)
|
||||
if err != nil {
|
||||
return ErrTampered
|
||||
}
|
||||
sig, err := base64.RawURLEncoding.DecodeString(mac)
|
||||
if err != nil {
|
||||
return ErrTampered
|
||||
}
|
||||
if !hmac.Equal(sig, s.macBytes(payload)) {
|
||||
return ErrTampered
|
||||
}
|
||||
if err := json.Unmarshal(payload, v); err != nil {
|
||||
return ErrTampered
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Signer) mac(payload []byte) string {
|
||||
return base64.RawURLEncoding.EncodeToString(s.macBytes(payload))
|
||||
}
|
||||
|
||||
func (s *Signer) macBytes(payload []byte) []byte {
|
||||
h := hmac.New(sha256.New, s.key)
|
||||
h.Write(payload)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// NewSessionCookie returns the Set-Cookie for a session valid for ttl.
|
||||
func (s *Signer) NewSessionCookie(sess Session, ttl time.Duration, secure bool) (*http.Cookie, error) {
|
||||
sess.ExpiresAt = time.Now().Add(ttl)
|
||||
value, err := s.Seal(sess)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Cookie{
|
||||
Name: CookieSession,
|
||||
Value: value,
|
||||
Path: "/",
|
||||
MaxAge: int(ttl.Seconds()),
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewLoginTxCookie returns the short-lived Set-Cookie carrying tx between
|
||||
// the login redirect and the callback.
|
||||
func (s *Signer) NewLoginTxCookie(tx LoginTx, secure bool) (*http.Cookie, error) {
|
||||
value, err := s.Seal(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Cookie{
|
||||
Name: CookieLoginTx,
|
||||
Value: value,
|
||||
Path: "/api/auth",
|
||||
MaxAge: int(txTTL.Seconds()),
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Expired returns a Set-Cookie that deletes the named cookie.
|
||||
func Expired(name string, secure bool) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: name,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user