auth: implement cookie-based auth for HTML endpoints and Bearer token auth for API endpoints
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:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user