This commit is contained in:
143
internal/auth/auth.go
Normal file
143
internal/auth/auth.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"releasewatcher/internal/store"
|
||||
)
|
||||
|
||||
const CookieName = "rw_session"
|
||||
|
||||
func randomBytes(n int) []byte {
|
||||
b := make([]byte, n)
|
||||
_, _ = rand.Read(b)
|
||||
return b
|
||||
}
|
||||
|
||||
func HashPassword(password string) string {
|
||||
salt := randomBytes(16)
|
||||
iters := 210000
|
||||
dk := pbkdf2Key([]byte(password), salt, iters, 32, sha256.New)
|
||||
return fmt.Sprintf("pbkdf2-sha256$%d$%s$%s", iters, hex.EncodeToString(salt), hex.EncodeToString(dk))
|
||||
}
|
||||
|
||||
func VerifyPassword(password, encoded string) bool {
|
||||
parts := strings.Split(encoded, "$")
|
||||
if len(parts) != 4 || parts[0] != "pbkdf2-sha256" {
|
||||
return false
|
||||
}
|
||||
iters, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
salt, err := hex.DecodeString(parts[2])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
want, err := hex.DecodeString(parts[3])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
got := pbkdf2Key([]byte(password), salt, iters, len(want), sha256.New)
|
||||
return hmac.Equal(got, want)
|
||||
}
|
||||
|
||||
func pbkdf2Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte {
|
||||
prf := hmac.New(h, password)
|
||||
hLen := prf.Size()
|
||||
numBlocks := (keyLen + hLen - 1) / hLen
|
||||
var dk []byte
|
||||
for block := 1; block <= numBlocks; block++ {
|
||||
prf.Reset()
|
||||
prf.Write(salt)
|
||||
prf.Write([]byte{byte(block >> 24), byte(block >> 16), byte(block >> 8), byte(block)})
|
||||
u := prf.Sum(nil)
|
||||
t := append([]byte(nil), u...)
|
||||
for i := 1; i < iter; i++ {
|
||||
prf.Reset()
|
||||
prf.Write(u)
|
||||
u = prf.Sum(nil)
|
||||
for x := range t {
|
||||
t[x] ^= u[x]
|
||||
}
|
||||
}
|
||||
dk = append(dk, t...)
|
||||
}
|
||||
return dk[:keyLen]
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
Secret []byte
|
||||
Store *store.FileStore
|
||||
}
|
||||
|
||||
func New(secret string, st *store.FileStore) *Manager {
|
||||
if secret == "" {
|
||||
secret = base64.RawURLEncoding.EncodeToString(randomBytes(32))
|
||||
}
|
||||
return &Manager{Secret: []byte(secret), Store: st}
|
||||
}
|
||||
|
||||
func (m *Manager) Sign(userID string) string {
|
||||
exp := time.Now().Add(12 * time.Hour).Unix()
|
||||
payload := fmt.Sprintf("%s.%d.%s", userID, exp, base64.RawURLEncoding.EncodeToString(randomBytes(12)))
|
||||
mac := hmac.New(sha256.New, m.Secret)
|
||||
mac.Write([]byte(payload))
|
||||
sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(payload + "." + sig))
|
||||
}
|
||||
|
||||
func (m *Manager) Parse(token string) (store.User, error) {
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
return store.User{}, err
|
||||
}
|
||||
parts := strings.Split(string(decoded), ".")
|
||||
if len(parts) != 4 {
|
||||
return store.User{}, errors.New("invalid token")
|
||||
}
|
||||
payload := strings.Join(parts[:3], ".")
|
||||
mac := hmac.New(sha256.New, m.Secret)
|
||||
mac.Write([]byte(payload))
|
||||
want := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
if !hmac.Equal([]byte(want), []byte(parts[3])) {
|
||||
return store.User{}, errors.New("bad signature")
|
||||
}
|
||||
exp, err := strconv.ParseInt(parts[1], 10, 64)
|
||||
if err != nil || time.Now().Unix() > exp {
|
||||
return store.User{}, errors.New("expired")
|
||||
}
|
||||
u, ok := m.Store.FindUserByID(parts[0])
|
||||
if !ok {
|
||||
return store.User{}, errors.New("unknown user")
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (m *Manager) CurrentUser(r *http.Request) (store.User, bool) {
|
||||
c, err := r.Cookie(CookieName)
|
||||
if err != nil {
|
||||
return store.User{}, false
|
||||
}
|
||||
u, err := m.Parse(c.Value)
|
||||
return u, err == nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetCookie(w http.ResponseWriter, userID string) {
|
||||
http.SetCookie(w, &http.Cookie{Name: CookieName, Value: m.Sign(userID), Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: false, MaxAge: 12 * 60 * 60})
|
||||
}
|
||||
|
||||
func ClearCookie(w http.ResponseWriter) {
|
||||
http.SetCookie(w, &http.Cookie{Name: CookieName, Value: "", Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: -1})
|
||||
}
|
||||
Reference in New Issue
Block a user