package auth import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "errors" "fmt" "net/http" "strconv" "strings" "time" "golang.org/x/crypto/bcrypt" ) const cookieName = "vetting_session" type Manager struct { PasswordHash string Secret []byte TTL time.Duration } func (m *Manager) VerifyPassword(password string) bool { if m.PasswordHash == "" { return false } return bcrypt.CompareHashAndPassword([]byte(m.PasswordHash), []byte(password)) == nil } // Issue writes a signed session cookie valid for m.TTL. func (m *Manager) Issue(w http.ResponseWriter, r *http.Request) { expiry := time.Now().Add(m.TTL).Unix() payload := strconv.FormatInt(expiry, 10) sig := m.sign(payload) value := payload + "." + sig http.SetCookie(w, &http.Cookie{ Name: cookieName, Value: value, Path: "/", HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteLaxMode, Expires: time.Unix(expiry, 0), }) } func (m *Manager) Clear(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ Name: cookieName, Value: "", Path: "/", HttpOnly: true, MaxAge: -1, }) } var errInvalidSession = errors.New("invalid session") // Validate returns nil if the request's cookie is present, signed, and not expired. func (m *Manager) Validate(r *http.Request) error { c, err := r.Cookie(cookieName) if err != nil { return errInvalidSession } parts := strings.SplitN(c.Value, ".", 2) if len(parts) != 2 { return errInvalidSession } payload, sig := parts[0], parts[1] expected := m.sign(payload) if !hmac.Equal([]byte(sig), []byte(expected)) { return errInvalidSession } expiry, err := strconv.ParseInt(payload, 10, 64) if err != nil { return errInvalidSession } if time.Now().Unix() >= expiry { return errInvalidSession } return nil } func (m *Manager) sign(payload string) string { mac := hmac.New(sha256.New, m.Secret) _, _ = mac.Write([]byte(payload)) return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) } // BcryptHash is a helper used by the gen-admin-password tool. func BcryptHash(password string) (string, error) { b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", fmt.Errorf("bcrypt: %w", err) } return string(b), nil }