Files
go-cart-actor/internal/customerauth/session.go
T
mats 3df95eac90
Build and Publish / BuildAndDeployAmd64 (push) Failing after 5s
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s
more customer
2026-06-26 19:49:54 +02:00

124 lines
3.5 KiB
Go

package customerauth
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
)
// SessionCookieName is the name of the HttpOnly cookie that carries the signed
// session token for a logged-in customer.
const SessionCookieName = "sid"
// DefaultSessionTTL is how long an issued session stays valid.
const DefaultSessionTTL = 30 * 24 * time.Hour
var (
// ErrInvalidToken is returned when a token is malformed or its signature
// does not verify.
ErrInvalidToken = errors.New("customerauth: invalid session token")
// ErrExpiredToken is returned when a token's signature is valid but it has
// passed its expiry.
ErrExpiredToken = errors.New("customerauth: expired session token")
)
// Signer issues and verifies session tokens using an HMAC-SHA256 key.
type Signer struct {
secret []byte
}
// NewSigner returns a Signer over the given secret key bytes.
func NewSigner(secret []byte) *Signer { return &Signer{secret: secret} }
// token format: base64url(<profileID>.<expUnix>) "." base64url(hmac)
// The payload is signed, not encrypted — it carries no secrets, only the
// profile id and expiry, and the HMAC makes it tamper-evident.
// Issue returns a signed token for profileID that expires after ttl.
func (s *Signer) Issue(profileID uint64, ttl time.Duration) string {
exp := time.Now().Add(ttl).Unix()
payload := fmt.Sprintf("%d.%d", profileID, exp)
b := base64.RawURLEncoding.EncodeToString([]byte(payload))
return b + "." + s.sign(b)
}
// Parse verifies a token and returns the profile id it carries. It returns
// ErrInvalidToken for a bad signature/format and ErrExpiredToken when expired.
func (s *Signer) Parse(token string) (uint64, error) {
b, sig, ok := strings.Cut(token, ".")
if !ok || b == "" || sig == "" {
return 0, ErrInvalidToken
}
if !hmac.Equal([]byte(sig), []byte(s.sign(b))) {
return 0, ErrInvalidToken
}
raw, err := base64.RawURLEncoding.DecodeString(b)
if err != nil {
return 0, ErrInvalidToken
}
idStr, expStr, ok := strings.Cut(string(raw), ".")
if !ok {
return 0, ErrInvalidToken
}
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return 0, ErrInvalidToken
}
exp, err := strconv.ParseInt(expStr, 10, 64)
if err != nil {
return 0, ErrInvalidToken
}
if time.Now().Unix() >= exp {
return 0, ErrExpiredToken
}
return id, nil
}
func (s *Signer) sign(b string) string {
mac := hmac.New(sha256.New, s.secret)
mac.Write([]byte(b))
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
}
// SetSession writes the session cookie for token onto w. The Secure flag is set
// when the request arrived over TLS (directly or via an https-terminating
// proxy) so the cookie still works on plain-http localhost in dev.
func SetSession(w http.ResponseWriter, r *http.Request, token string, ttl time.Duration) {
http.SetCookie(w, &http.Cookie{
Name: SessionCookieName,
Value: token,
Path: "/",
HttpOnly: true,
Secure: isSecure(r),
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(ttl),
MaxAge: int(ttl.Seconds()),
})
}
// ClearSession expires the session cookie on w.
func ClearSession(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: SessionCookieName,
Value: "",
Path: "/",
HttpOnly: true,
Secure: isSecure(r),
SameSite: http.SameSiteLaxMode,
MaxAge: -1,
})
}
func isSecure(r *http.Request) bool {
if r.TLS != nil {
return true
}
return strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https")
}