auth: implement cookie-based auth for HTML endpoints and Bearer token auth for API endpoints
Some checks failed
CI / lint (push) Failing after 15s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / docker (push) Has been skipped

Details:

  • HTML endpoints (/, /profile, /admin, /login, /register):

    - Authenticate via HTTP-only cookie named 'token'

    - Handlers in internal/templates/templates.go check cookie validity

    - /admin endpoint additionally checks for role='admin'

    - Unauthenticated users redirected to /login

    - Non-admin users accessing /admin get HTTP 403 Forbidden

  • API endpoints (/api/*):

    - Authenticate via Bearer token in Authorization header only

    - Handlers in internal/api/api.go use authenticateRequest() function

    - Function extracts token from 'Authorization: Bearer <token>' header

    - Validates token against yggdrasil_sessions table

    - No cookie checking for API endpoints (launcher compatibility)

  • Web login (/api/web/login):

    - Sets HTTP-only cookie 'token' for browser storage

    - Returns JSON with token, UUID, username for JS localStorage

    - Maintains backward compatibility with existing JavaScript

  • JavaScript in HTML pages:

    - Gets token from localStorage (set by login response)

    - Sets Authorization: Bearer <token> header for API fetch calls

    - Updated admin.html and profile.js to include token in headers

This separation ensures:

  • HTML endpoints work automatically with browser cookies

  • API endpoints work with browsers (via JS) and launchers (Bearer tokens)

  • Security sensitive actions require proper role validation

  • Clean separation of concerns between document and API interfaces
This commit is contained in:
2026-06-07 23:11:51 +03:00
parent 5bd8a549ca
commit 75ea7c70c2
3 changed files with 48 additions and 3 deletions

View File

@@ -11,6 +11,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/auth" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/auth"
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config"
@@ -246,6 +247,17 @@ func (h *Handler) webLogin(w http.ResponseWriter, r *http.Request) {
return return
} }
// Set authentication token in cookie for web frontend
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: token,
Path: "/",
Expires: time.Now().Add(7 * 24 * time.Hour),
HttpOnly: true,
Secure: r.TLS != nil, // Set secure flag if HTTPS
SameSite: http.SameSiteLaxMode,
})
utils.WriteJSON(w, http.StatusOK, webLoginResponse{ utils.WriteJSON(w, http.StatusOK, webLoginResponse{
Token: token, Token: token,
UUID: user.UUID, UUID: user.UUID,

View File

@@ -177,7 +177,7 @@ document.getElementById('modpack-form').addEventListener('submit', async (e) =>
try { try {
const response = await fetch('/api/admin/modpacks', { const response = await fetch('/api/admin/modpacks', {
method: 'POST', method: 'POST',
headers { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token 'Authorization': 'Bearer ' + token
}, },

View File

@@ -29,6 +29,15 @@ type Handler struct {
templates map[string]*template.Template templates map[string]*template.Template
} }
// getCookieToken extracts the authentication token from the request cookie.
func (h *Handler) getCookieToken(r *http.Request) string {
// Check cookie named "token"
if cookie, err := r.Cookie("token"); err == nil {
return cookie.Value
}
return ""
}
// NewHandler creates a new templates handler and parses embedded templates. // NewHandler creates a new templates handler and parses embedded templates.
func NewHandler(db *database.DB, cfg *config.Config) *Handler { func NewHandler(db *database.DB, cfg *config.Config) *Handler {
h := &Handler{db: db, cfg: cfg, templates: make(map[string]*template.Template)} h := &Handler{db: db, cfg: cfg, templates: make(map[string]*template.Template)}
@@ -64,12 +73,36 @@ func (h *Handler) registerPage(w http.ResponseWriter, r *http.Request) {
} }
func (h *Handler) profilePage(w http.ResponseWriter, r *http.Request) { func (h *Handler) profilePage(w http.ResponseWriter, r *http.Request) {
// Get token from cookie only (HTML endpoint)
token := h.getCookieToken(r)
if token == "" {
// No token provided, redirect to login
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Validate token (just check if valid, don't need role check for profile)
var userID int
err := h.db.Pool().QueryRow(r.Context(),
`SELECT user_id
FROM yggdrasil_sessions
WHERE access_token = $1 AND expires_at > NOW()`,
token,
).Scan(&userID)
if err != nil {
// Invalid or expired token, redirect to login
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// User is authenticated, render profile page
h.render(w, "profile.html", pageData{Title: "Профиль"}) h.render(w, "profile.html", pageData{Title: "Профиль"})
} }
func (h *Handler) adminPage(w http.ResponseWriter, r *http.Request) { func (h *Handler) adminPage(w http.ResponseWriter, r *http.Request) {
// Extract bearer token from Authorization header // Get token from cookie only (HTML endpoint)
token := auth.ExtractBearer(r.Header.Get("Authorization")) token := h.getCookieToken(r)
if token == "" { if token == "" {
// No token provided, redirect to login // No token provided, redirect to login
http.Redirect(w, r, "/login", http.StatusSeeOther) http.Redirect(w, r, "/login", http.StatusSeeOther)