package auth import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/base64" "fmt" "net/http" "strconv" "strings" "time" "gitea.tbdevent.eu/TBD/reforger_crawler_main/initializers" "github.com/gin-gonic/gin" ) // CookieName is the admin session cookie. It is scoped to /admin so it is never // sent to the public API. const CookieName = "rc_admin" var b64 = base64.RawURLEncoding // The signing key is derived from the admin secret, so rotating ADMIN_SECRET // invalidates every issued session. func sessionKey() []byte { m := hmac.New(sha256.New, []byte(initializers.AdminSecret)) m.Write([]byte("rc-admin-session/v1")) return m.Sum(nil) } func sign(payload string) string { m := hmac.New(sha256.New, sessionKey()) m.Write([]byte(payload)) return b64.EncodeToString(m.Sum(nil)) } // IssueToken creates a stateless session token: v1... func IssueToken(now time.Time) (string, time.Time) { exp := now.Add(initializers.AdminSessionTTL) nonce := make([]byte, 16) _, _ = rand.Read(nonce) payload := fmt.Sprintf("v1.%d.%s", exp.Unix(), b64.EncodeToString(nonce)) return payload + "." + sign(payload), exp } // VerifyToken checks the signature and expiry and returns the expiry time. func VerifyToken(token string, now time.Time) (time.Time, bool) { if initializers.AdminSecret == "" { return time.Time{}, false } idx := strings.LastIndexByte(token, '.') if idx <= 0 { return time.Time{}, false } payload, mac := token[:idx], token[idx+1:] if !hmac.Equal([]byte(mac), []byte(sign(payload))) { return time.Time{}, false } parts := strings.Split(payload, ".") if len(parts) != 3 || parts[0] != "v1" { return time.Time{}, false } expUnix, err := strconv.ParseInt(parts[1], 10, 64) if err != nil { return time.Time{}, false } exp := time.Unix(expUnix, 0) if !now.Before(exp) { return time.Time{}, false } return exp, true } func cookieSecure(c *gin.Context) bool { switch initializers.AdminCookieSecure { case "true", "1", "yes": return true case "false", "0", "no": return false } return c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") } func SetSessionCookie(c *gin.Context, token string, exp time.Time) { http.SetCookie(c.Writer, &http.Cookie{ Name: CookieName, Value: token, Path: "/admin", Expires: exp, MaxAge: int(time.Until(exp).Seconds()), HttpOnly: true, Secure: cookieSecure(c), SameSite: http.SameSiteStrictMode, }) } func ClearSessionCookie(c *gin.Context) { http.SetCookie(c.Writer, &http.Cookie{ Name: CookieName, Value: "", Path: "/admin", MaxAge: -1, HttpOnly: true, Secure: cookieSecure(c), SameSite: http.SameSiteStrictMode, }) }