first commit
This commit is contained in:
124
internal/handler/auth.go
Normal file
124
internal/handler/auth.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const sessionCookie = "rs_session"
|
||||
const sessionDuration = 24 * time.Hour
|
||||
|
||||
// sessionSecret returns the HMAC signing key from env, with a fallback warning.
|
||||
func sessionSecret() []byte {
|
||||
s := os.Getenv("SESSION_SECRET")
|
||||
if s == "" {
|
||||
// Insecure fallback for local dev only — set SESSION_SECRET in production
|
||||
return []byte("change-me-in-production")
|
||||
}
|
||||
return []byte(s)
|
||||
}
|
||||
|
||||
// adminPasswordHash returns the bcrypt hash of the admin password from env.
|
||||
func adminPasswordHash() string {
|
||||
return os.Getenv("ADMIN_PASSWORD_HASH")
|
||||
}
|
||||
|
||||
// checkPassword verifies a plaintext password against the stored bcrypt hash.
|
||||
func checkPassword(password string) bool {
|
||||
hash := adminPasswordHash()
|
||||
if hash == "" {
|
||||
return false
|
||||
}
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
||||
}
|
||||
|
||||
// signValue creates an HMAC-signed value: base64(payload)|base64(sig).
|
||||
func signValue(payload string) string {
|
||||
mac := hmac.New(sha256.New, sessionSecret())
|
||||
mac.Write([]byte(payload))
|
||||
sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(payload)) + "|" + sig
|
||||
}
|
||||
|
||||
// verifyValue checks the signature and returns the original payload.
|
||||
func verifyValue(signed string) (string, bool) {
|
||||
parts := strings.SplitN(signed, "|", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", false
|
||||
}
|
||||
payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
payload := string(payloadBytes)
|
||||
expected := signValue(payload)
|
||||
if !hmac.Equal([]byte(signed), []byte(expected)) {
|
||||
return "", false
|
||||
}
|
||||
return payload, true
|
||||
}
|
||||
|
||||
// setSession writes a signed session cookie.
|
||||
func setSession(w http.ResponseWriter) {
|
||||
expiry := time.Now().Add(sessionDuration).Format(time.RFC3339)
|
||||
value := signValue("admin|" + expiry)
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookie,
|
||||
Value: value,
|
||||
Path: "/admin",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: int(sessionDuration.Seconds()),
|
||||
})
|
||||
}
|
||||
|
||||
// clearSession deletes the session cookie.
|
||||
func clearSession(w http.ResponseWriter) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookie,
|
||||
Value: "",
|
||||
Path: "/admin",
|
||||
MaxAge: -1,
|
||||
Expires: time.Unix(0, 0),
|
||||
})
|
||||
}
|
||||
|
||||
// isAuthenticated returns true if the request has a valid session cookie.
|
||||
func isAuthenticated(r *http.Request) bool {
|
||||
c, err := r.Cookie(sessionCookie)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
payload, ok := verifyValue(c.Value)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// payload = "admin|<RFC3339 expiry>"
|
||||
parts := strings.SplitN(payload, "|", 2)
|
||||
if len(parts) != 2 || parts[0] != "admin" {
|
||||
return false
|
||||
}
|
||||
expiry, err := time.Parse(time.RFC3339, parts[1])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return time.Now().Before(expiry)
|
||||
}
|
||||
|
||||
// requireAuth is middleware that redirects to /admin/login if not authenticated.
|
||||
func (h *Handler) requireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !isAuthenticated(r) {
|
||||
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user