more customer
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user