72 lines
1.7 KiB
Go
72 lines
1.7 KiB
Go
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) }
|