Use Seession cookie for resistent login
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
authCookieName = "datascape_auth"
|
||||
authCookieMaxAge = 10 * 365 * 24 * 3600
|
||||
)
|
||||
|
||||
// loadOrCreateAuthKey returns a stable 32-byte HMAC key persisted in the wiki
|
||||
// root as `.auth-key`. A stable key means sessions survive restarts.
|
||||
func loadOrCreateAuthKey(wikiDir string) ([]byte, error) {
|
||||
p := filepath.Join(wikiDir, ".auth-key")
|
||||
if data, err := os.ReadFile(p); err == nil && len(data) >= 32 {
|
||||
return data, nil
|
||||
}
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(p, key, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func signAuth(key []byte) string {
|
||||
payload := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write([]byte(payload))
|
||||
sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
return payload + "." + sig
|
||||
}
|
||||
|
||||
func verifyAuth(key []byte, value string) bool {
|
||||
i := strings.IndexByte(value, '.')
|
||||
if i <= 0 {
|
||||
return false
|
||||
}
|
||||
payload, sig := value[:i], value[i+1:]
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write([]byte(payload))
|
||||
expected := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
return hmac.Equal([]byte(sig), []byte(expected))
|
||||
}
|
||||
|
||||
// checkAuth returns true if the request is authenticated. It accepts either a
|
||||
// valid signed cookie or HTTP Basic credentials; on successful basic auth it
|
||||
// issues a long-lived cookie so the browser stops re-prompting.
|
||||
func (h *handler) checkAuth(w http.ResponseWriter, r *http.Request) bool {
|
||||
if h.user == "" {
|
||||
return true
|
||||
}
|
||||
if c, err := r.Cookie(authCookieName); err == nil && verifyAuth(h.authKey, c.Value) {
|
||||
return true
|
||||
}
|
||||
u, p, ok := r.BasicAuth()
|
||||
if !ok || u != h.user || p != h.pass {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="datascape"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return false
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: authCookieName,
|
||||
Value: signAuth(h.authKey),
|
||||
Path: "/",
|
||||
MaxAge: authCookieMaxAge,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// handleLogout clears the session cookie and forces a fresh basic-auth prompt.
|
||||
func (h *handler) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: authCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="datascape"`)
|
||||
http.Error(w, "Logged out", http.StatusUnauthorized)
|
||||
}
|
||||
Reference in New Issue
Block a user