126 lines
3.2 KiB
Go
126 lines
3.2 KiB
Go
|
|
package session
|
||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/hmac"
|
||
|
|
"crypto/rand"
|
||
|
|
"crypto/sha256"
|
||
|
|
"encoding/base64"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
const cookieName = "go_loose_session"
|
||
|
|
|
||
|
|
type Claims struct {
|
||
|
|
UserID string `json:"uid"`
|
||
|
|
Email string `json:"email"`
|
||
|
|
DisplayName string `json:"name"`
|
||
|
|
Management bool `json:"management"`
|
||
|
|
ExpiresAt int64 `json:"exp"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type Manager struct {
|
||
|
|
secret []byte
|
||
|
|
secure bool
|
||
|
|
}
|
||
|
|
|
||
|
|
func New(secret string, secure bool) *Manager {
|
||
|
|
return &Manager{secret: []byte(secret), secure: secure}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) Set(w http.ResponseWriter, claims Claims) error {
|
||
|
|
return m.set(w, claims, "")
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) SetShared(w http.ResponseWriter, claims Claims, domain string) error {
|
||
|
|
return m.set(w, claims, domain)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) set(w http.ResponseWriter, claims Claims, domain string) error {
|
||
|
|
claims.ExpiresAt = time.Now().Add(12 * time.Hour).Unix()
|
||
|
|
payload, err := json.Marshal(claims)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
encoded := base64.RawURLEncoding.EncodeToString(payload)
|
||
|
|
value := encoded + "." + m.sign(encoded)
|
||
|
|
http.SetCookie(w, &http.Cookie{
|
||
|
|
Name: cookieName, Value: value, Path: "/", HttpOnly: true, Secure: m.secure,
|
||
|
|
SameSite: http.SameSiteLaxMode, MaxAge: int((12 * time.Hour).Seconds()), Domain: domain,
|
||
|
|
})
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) Get(r *http.Request) (Claims, error) {
|
||
|
|
cookie, err := r.Cookie(cookieName)
|
||
|
|
if err != nil {
|
||
|
|
return Claims{}, err
|
||
|
|
}
|
||
|
|
parts := strings.Split(cookie.Value, ".")
|
||
|
|
if len(parts) != 2 || !hmac.Equal([]byte(parts[1]), []byte(m.sign(parts[0]))) {
|
||
|
|
return Claims{}, errors.New("invalid session signature")
|
||
|
|
}
|
||
|
|
payload, err := base64.RawURLEncoding.DecodeString(parts[0])
|
||
|
|
if err != nil {
|
||
|
|
return Claims{}, errors.New("invalid session encoding")
|
||
|
|
}
|
||
|
|
var claims Claims
|
||
|
|
if err := json.Unmarshal(payload, &claims); err != nil {
|
||
|
|
return Claims{}, errors.New("invalid session payload")
|
||
|
|
}
|
||
|
|
if claims.UserID == "" || time.Now().Unix() >= claims.ExpiresAt {
|
||
|
|
return Claims{}, errors.New("session expired")
|
||
|
|
}
|
||
|
|
return claims, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) Clear(w http.ResponseWriter) {
|
||
|
|
m.clear(w, "")
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) ClearShared(w http.ResponseWriter, domain string) {
|
||
|
|
m.clear(w, domain)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) clear(w http.ResponseWriter, domain string) {
|
||
|
|
http.SetCookie(w, &http.Cookie{
|
||
|
|
Name: cookieName, Value: "", Path: "/", HttpOnly: true, Secure: m.secure,
|
||
|
|
SameSite: http.SameSiteLaxMode, MaxAge: -1, Domain: domain,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) NewState() (string, error) {
|
||
|
|
value := make([]byte, 24)
|
||
|
|
if _, err := rand.Read(value); err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
return base64.RawURLEncoding.EncodeToString(value), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) SignedState(state string) string {
|
||
|
|
return state + "." + m.sign(state)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) VerifyState(value string) bool {
|
||
|
|
_, ok := m.VerifySigned(value)
|
||
|
|
return ok
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) VerifySigned(value string) (string, bool) {
|
||
|
|
index := strings.LastIndexByte(value, '.')
|
||
|
|
if index <= 0 || index == len(value)-1 {
|
||
|
|
return "", false
|
||
|
|
}
|
||
|
|
payload, signature := value[:index], value[index+1:]
|
||
|
|
return payload, hmac.Equal([]byte(signature), []byte(m.sign(payload)))
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) sign(value string) string {
|
||
|
|
mac := hmac.New(sha256.New, m.secret)
|
||
|
|
mac.Write([]byte(value))
|
||
|
|
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||
|
|
}
|