Use Seession cookie for resistent login
This commit is contained in:
+3
-2
@@ -14,8 +14,9 @@
|
|||||||
<header>
|
<header>
|
||||||
<nav class="breadcrumb">
|
<nav class="breadcrumb">
|
||||||
<a href="/"><svg class="logo" viewBox="0 0 26.052269 26.052269" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="currentColor" stroke-linejoin="miter" transform="matrix(0.05463483,8.1519706e-6,-8.1519706e-6,0.05463483,-64.560546,-24.6949)"><rect x="1188.537" y="457.92056" width="461.87488" height="462.15189" stroke-width="20.2288"/><path d="m1348.9955 456.59572.046 309.36839" stroke-width="19.6849"/><path d="m1200.3996 765.80237 441.8362-.0659" stroke-width="19.6849"/><path d="m1648.2897 620.244-299.2012.0446" stroke-width="20.5676"/><path d="m1491.6148 909.24806-.021-136.93117" stroke-width="19.6849"/><rect x="1191.6504" y="461.66092" width="457.09634" height="457.09634" stroke-width="19.6761"/></g></svg></a>
|
<a href="/"><svg class="logo" viewBox="0 0 26.052269 26.052269" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="currentColor" stroke-linejoin="miter" transform="matrix(0.05463483,8.1519706e-6,-8.1519706e-6,0.05463483,-64.560546,-24.6949)"><rect x="1188.537" y="457.92056" width="461.87488" height="462.15189" stroke-width="20.2288"/><path d="m1348.9955 456.59572.046 309.36839" stroke-width="19.6849"/><path d="m1200.3996 765.80237 441.8362-.0659" stroke-width="19.6849"/><path d="m1648.2897 620.244-299.2012.0446" stroke-width="20.5676"/><path d="m1491.6148 909.24806-.021-136.93117" stroke-width="19.6849"/><rect x="1191.6504" y="461.66092" width="457.09634" height="457.09634" stroke-width="19.6761"/></g></svg></a>
|
||||||
{{range .Crumbs}}<span class="sep">/</span
|
{{range .Crumbs}}
|
||||||
><a href="{{.URL}}">{{.Name}}</a>{{end}}
|
<span class="sep">/</span><a href="{{.URL}}">{{.Name}}</a>
|
||||||
|
{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
{{if .EditMode}}
|
{{if .EditMode}}
|
||||||
<a class="btn" href="{{.PostURL}}">CANCEL</a>
|
<a class="btn" href="{{.PostURL}}">CANCEL</a>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -51,7 +51,11 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
h := &handler{root: root, user: *user, pass: *pass}
|
authKey, err := loadOrCreateAuthKey(root)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
h := &handler{root: root, user: *user, pass: *pass, authKey: authKey}
|
||||||
|
|
||||||
staticFS, _ := fs.Sub(assets, "assets")
|
staticFS, _ := fs.Sub(assets, "assets")
|
||||||
static := http.StripPrefix("/_/", http.FileServer(http.FS(staticFS)))
|
static := http.StripPrefix("/_/", http.FileServer(http.FS(staticFS)))
|
||||||
@@ -61,6 +65,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
static.ServeHTTP(w, r)
|
static.ServeHTTP(w, r)
|
||||||
}))
|
}))
|
||||||
|
http.HandleFunc("/_logout", h.handleLogout)
|
||||||
http.Handle("/", h)
|
http.Handle("/", h)
|
||||||
|
|
||||||
log.Printf("datascape listening on %s, wiki at %s", *addr, root)
|
log.Printf("datascape listening on %s, wiki at %s", *addr, root)
|
||||||
@@ -69,17 +74,13 @@ func main() {
|
|||||||
|
|
||||||
type handler struct {
|
type handler struct {
|
||||||
root, user, pass string
|
root, user, pass string
|
||||||
|
authKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.user != "" {
|
if !h.checkAuth(w, r) {
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
urlPath := path.Clean("/" + r.URL.Path)
|
urlPath := path.Clean("/" + r.URL.Path)
|
||||||
fsPath := filepath.Join(h.root, filepath.FromSlash(urlPath))
|
fsPath := filepath.Join(h.root, filepath.FromSlash(urlPath))
|
||||||
|
|||||||
Reference in New Issue
Block a user