package admin import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "errors" "fmt" "strconv" "strings" "time" ) // Sessions are stateless: base64("user:expiryUnix") + "." + HMAC-SHA256. // They survive restarts as long as ONE_SECRET stays the same. type Sessions struct { secret []byte ttl time.Duration } func NewSessions(secret string, ttl time.Duration) *Sessions { if ttl <= 0 { ttl = 7 * 24 * time.Hour } return &Sessions{secret: []byte(secret), ttl: ttl} } var ErrBadSession = errors.New("invalid session") func (s *Sessions) Issue(user string) (string, time.Time) { exp := time.Now().Add(s.ttl) payload := base64.RawURLEncoding.EncodeToString([]byte(user + ":" + strconv.FormatInt(exp.Unix(), 10))) return payload + "." + s.sign(payload), exp } func (s *Sessions) Verify(token string) (string, error) { parts := strings.Split(token, ".") if len(parts) != 2 { return "", ErrBadSession } if !hmac.Equal([]byte(s.sign(parts[0])), []byte(parts[1])) { return "", ErrBadSession } raw, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { return "", ErrBadSession } i := strings.LastIndex(string(raw), ":") if i <= 0 { return "", ErrBadSession } user := string(raw)[:i] expUnix, err := strconv.ParseInt(string(raw)[i+1:], 10, 64) if err != nil { return "", ErrBadSession } if time.Now().After(time.Unix(expUnix, 0)) { return "", ErrBadSession } return user, nil } func (s *Sessions) sign(payload string) string { mac := hmac.New(sha256.New, s.secret) mac.Write([]byte(payload)) return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) } func (s *Sessions) TTL() int { return int(s.ttl.Seconds()) } func (s *Sessions) String() string { return fmt.Sprintf("sessions(ttl=%s)", s.ttl) }