added cargo files
This commit is contained in:
850
PinePods-0.8.2/gpodder-api/internal/api/auth.go
Normal file
850
PinePods-0.8.2/gpodder-api/internal/api/auth.go
Normal file
@@ -0,0 +1,850 @@
|
||||
// Package api provides the API endpoints for the gpodder API
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"pinepods/gpodder-api/internal/db"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
"github.com/fernet/fernet-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Define the parameters we use for Argon2id
|
||||
type argon2Params struct {
|
||||
memory uint32
|
||||
iterations uint32
|
||||
parallelism uint8
|
||||
saltLength uint32
|
||||
keyLength uint32
|
||||
}
|
||||
|
||||
// AuthMiddleware creates a middleware for authentication
|
||||
func AuthMiddleware(db *db.PostgresDB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
// Get the username from the URL parameters
|
||||
username := c.Param("username")
|
||||
if username == "" {
|
||||
log.Printf("[ERROR] AuthMiddleware: Username parameter is missing in path")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is an internal API call via X-GPodder-Token
|
||||
gpodderTokenHeader := c.GetHeader("X-GPodder-Token")
|
||||
if gpodderTokenHeader != "" {
|
||||
|
||||
// Get user data
|
||||
var userID int
|
||||
var gpodderToken sql.NullString
|
||||
var podSyncType string
|
||||
|
||||
err := db.QueryRow(`
|
||||
SELECT UserID, GpodderToken, Pod_Sync_Type FROM "Users"
|
||||
WHERE LOWER(Username) = LOWER($1)
|
||||
`, username).Scan(&userID, &gpodderToken, &podSyncType)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] AuthMiddleware: Database error: %v", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if gpodder sync is enabled
|
||||
if podSyncType != "gpodder" && podSyncType != "both" && podSyncType != "external" {
|
||||
log.Printf("[ERROR] AuthMiddleware: Gpodder API not enabled for user: %s (sync type: %s)",
|
||||
username, podSyncType)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Gpodder API not enabled for this user"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// For internal calls with X-GPodder-Token header, validate token directly
|
||||
if gpodderToken.Valid && gpodderToken.String == gpodderTokenHeader {
|
||||
c.Set("userID", userID)
|
||||
c.Set("username", username)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// If token doesn't match, authentication failed
|
||||
log.Printf("[ERROR] AuthMiddleware: Invalid X-GPodder-Token for user: %s", username)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// If no token header found, proceed with standard authentication
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
log.Printf("[ERROR] AuthMiddleware: Authorization header is missing")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Extract credentials
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Basic" {
|
||||
log.Printf("[ERROR] AuthMiddleware: Invalid Authorization header format")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Decode credentials
|
||||
decoded, err := base64.StdEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] AuthMiddleware: Failed to decode base64 credentials: %v", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Extract username and password
|
||||
credentials := strings.SplitN(string(decoded), ":", 2)
|
||||
if len(credentials) != 2 {
|
||||
log.Printf("[ERROR] AuthMiddleware: Invalid credentials format")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials format"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
authUsername := credentials[0]
|
||||
password := credentials[1]
|
||||
|
||||
// Check username match
|
||||
if strings.ToLower(username) != strings.ToLower(authUsername) {
|
||||
log.Printf("[ERROR] AuthMiddleware: Username mismatch - URL: %s, Auth: %s",
|
||||
strings.ToLower(username), strings.ToLower(authUsername))
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Username mismatch"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Query user data
|
||||
var userID int
|
||||
var hashedPassword string
|
||||
var podSyncType string
|
||||
var gpodderToken sql.NullString
|
||||
|
||||
err = db.QueryRow(`
|
||||
SELECT UserID, Hashed_PW, Pod_Sync_Type, GpodderToken FROM "Users"
|
||||
WHERE LOWER(Username) = LOWER($1)
|
||||
`, username).Scan(&userID, &hashedPassword, &podSyncType, &gpodderToken)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
log.Printf("[ERROR] AuthMiddleware: User not found: %s", username)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
|
||||
} else {
|
||||
log.Printf("[ERROR] AuthMiddleware: Database error: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
}
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if gpodder sync is enabled
|
||||
if podSyncType != "gpodder" && podSyncType != "both" && podSyncType != "external" {
|
||||
log.Printf("[ERROR] AuthMiddleware: Gpodder API not enabled for user: %s (sync type: %s)",
|
||||
username, podSyncType)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Gpodder API not enabled for this user"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Flag to track authentication success
|
||||
authenticated := false
|
||||
|
||||
// Check if this is a gpodder token authentication
|
||||
// Check if this is a gpodder token authentication
|
||||
if gpodderToken.Valid && (gpodderToken.String == password || gpodderToken.String == gpodderTokenHeader) {
|
||||
authenticated = true
|
||||
}
|
||||
|
||||
// If token auth didn't succeed, try password authentication
|
||||
if !authenticated && verifyPassword(password, hashedPassword) {
|
||||
authenticated = true
|
||||
}
|
||||
|
||||
// If authentication was successful, set context and continue
|
||||
if authenticated {
|
||||
c.Set("userID", userID)
|
||||
c.Set("username", username)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Authentication failed
|
||||
log.Printf("[ERROR] AuthMiddleware: Invalid credentials for user: %s", username)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to decrypt token
|
||||
func decryptToken(encryptionKey []byte, encryptedToken string) (string, error) {
|
||||
// Ensure the encryptionKey is correctly formatted for fernet
|
||||
// Fernet requires a 32-byte key encoded in base64
|
||||
keyStr := base64.StdEncoding.EncodeToString(encryptionKey)
|
||||
|
||||
// Parse the key
|
||||
key, err := fernet.DecodeKey(keyStr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode key: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt the token
|
||||
token := []byte(encryptedToken)
|
||||
msg := fernet.VerifyAndDecrypt(token, 0, []*fernet.Key{key})
|
||||
if msg == nil {
|
||||
return "", fmt.Errorf("failed to decrypt token or token invalid")
|
||||
}
|
||||
|
||||
return string(msg), nil
|
||||
}
|
||||
|
||||
// generateSessionToken generates a random token for sessions
|
||||
func generateSessionToken() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// createSession creates a new session in the database
|
||||
func createSession(db *db.Database, userID int, userAgent, clientIP string) (string, time.Time, error) {
|
||||
// Generate a random session token
|
||||
token, err := generateSessionToken()
|
||||
if err != nil {
|
||||
return "", time.Time{}, fmt.Errorf("failed to generate session token: %w", err)
|
||||
}
|
||||
|
||||
// Set expiration time (30 days from now)
|
||||
expires := time.Now().Add(30 * 24 * time.Hour)
|
||||
|
||||
// Insert session into database
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO "GpodderSessions" (UserID, SessionToken, ExpiresAt, UserAgent, ClientIP)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`, userID, token, expires, userAgent, clientIP)
|
||||
|
||||
if err != nil {
|
||||
return "", time.Time{}, fmt.Errorf("failed to create session: %w", err)
|
||||
}
|
||||
|
||||
return token, expires, nil
|
||||
}
|
||||
|
||||
// validateSession validates a session token
|
||||
func validateSession(db *db.Database, token string) (int, bool, error) {
|
||||
var userID int
|
||||
var expires time.Time
|
||||
var query string
|
||||
|
||||
// Format query according to database type
|
||||
if db.IsPostgreSQLDB() {
|
||||
query = `SELECT UserID, ExpiresAt FROM "GpodderSessions" WHERE SessionToken = $1`
|
||||
} else {
|
||||
query = `SELECT UserID, ExpiresAt FROM GpodderSessions WHERE SessionToken = ?`
|
||||
}
|
||||
|
||||
err := db.QueryRow(query, token).Scan(&userID, &expires)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, false, nil // Session not found
|
||||
}
|
||||
return 0, false, fmt.Errorf("error validating session: %w", err)
|
||||
}
|
||||
|
||||
// Check if session has expired
|
||||
if time.Now().After(expires) {
|
||||
// Delete expired session
|
||||
if db.IsPostgreSQLDB() {
|
||||
query = `DELETE FROM "GpodderSessions" WHERE SessionToken = $1`
|
||||
} else {
|
||||
query = `DELETE FROM GpodderSessions WHERE SessionToken = ?`
|
||||
}
|
||||
|
||||
_, err = db.Exec(query, token)
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete expired session: %v", err)
|
||||
}
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
// Update last active time
|
||||
if db.IsPostgreSQLDB() {
|
||||
query = `UPDATE "GpodderSessions" SET LastActive = CURRENT_TIMESTAMP WHERE SessionToken = $1`
|
||||
} else {
|
||||
query = `UPDATE GpodderSessions SET LastActive = CURRENT_TIMESTAMP WHERE SessionToken = ?`
|
||||
}
|
||||
|
||||
_, err = db.Exec(query, token)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to update session last active time: %v", err)
|
||||
}
|
||||
|
||||
return userID, true, nil
|
||||
}
|
||||
|
||||
// deleteSession removes a session from the database
|
||||
func deleteSession(db *db.Database, token string) error {
|
||||
_, err := db.Exec(`DELETE FROM "GpodderSessions" WHERE SessionToken = $1`, token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete session: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteUserSessions removes all sessions for a user
|
||||
func deleteUserSessions(db *db.PostgresDB, userID int) error {
|
||||
_, err := db.Exec(`DELETE FROM "GpodderSessions" WHERE UserID = $1`, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete user sessions: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleLogin enhanced with session management
|
||||
func handleLogin(database *db.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Use the AuthMiddleware to authenticate the user
|
||||
username := c.Param("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get the Authorization header
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the Authorization header is in the correct format
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Basic" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"})
|
||||
return
|
||||
}
|
||||
|
||||
// Decode the base64-encoded credentials
|
||||
decoded, err := base64.StdEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header"})
|
||||
return
|
||||
}
|
||||
|
||||
// Extract username and password
|
||||
credentials := strings.SplitN(string(decoded), ":", 2)
|
||||
if len(credentials) != 2 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials format"})
|
||||
return
|
||||
}
|
||||
|
||||
authUsername := credentials[0]
|
||||
password := credentials[1]
|
||||
|
||||
// Verify that the username in the URL matches the one in the Authorization header
|
||||
if strings.ToLower(username) != strings.ToLower(authUsername) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Username mismatch"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user exists and the password is correct
|
||||
var userID int
|
||||
var hashedPassword string
|
||||
var podSyncType string
|
||||
|
||||
// Make sure to use case-insensitive username lookup
|
||||
err = database.QueryRow(`
|
||||
SELECT UserID, Hashed_PW, Pod_Sync_Type FROM "Users" WHERE LOWER(Username) = LOWER($1)
|
||||
`, username).Scan(&userID, &hashedPassword, &podSyncType)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
|
||||
} else {
|
||||
log.Printf("Database error during login: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if gpodder sync is enabled for this user
|
||||
if podSyncType != "gpodder" && podSyncType != "both" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Gpodder API not enabled for this user"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify password using Pinepods' Argon2 password method
|
||||
if !verifyPassword(password, hashedPassword) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new session
|
||||
userAgent := c.Request.UserAgent()
|
||||
clientIP := c.ClientIP()
|
||||
sessionToken, expiresAt, err := createSession(database, userID, userAgent, clientIP)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to create session: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create session"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] handleLogin: Login successful for user: %s, created session token (first 8 chars): %s...",
|
||||
username, sessionToken[:8])
|
||||
|
||||
// Set session cookie
|
||||
c.SetCookie(
|
||||
"sessionid", // name
|
||||
sessionToken, // value
|
||||
int(30*24*time.Hour.Seconds()), // max age in seconds (30 days)
|
||||
"/", // path
|
||||
"", // domain (empty = current domain)
|
||||
c.Request.TLS != nil, // secure (HTTPS only)
|
||||
true, // httpOnly (not accessible via JavaScript)
|
||||
)
|
||||
|
||||
log.Printf("[DEBUG] handleLogin: Sending response with session expiry: %s",
|
||||
expiresAt.Format(time.RFC3339))
|
||||
// Return success with info
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"userid": userID,
|
||||
"username": username,
|
||||
"session_expires": expiresAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// handleLogout enhanced with session management
|
||||
func handleLogout(database *db.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Get username from URL
|
||||
username := c.Param("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get the session cookie
|
||||
sessionToken, err := c.Cookie("sessionid")
|
||||
if err != nil || sessionToken == "" {
|
||||
// No session cookie, just return success (idempotent operation)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "logged out",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the session
|
||||
err = deleteSession(database, sessionToken)
|
||||
if err != nil {
|
||||
log.Printf("Error deleting session: %v", err)
|
||||
// Continue anyway - we still want to invalidate the cookie
|
||||
}
|
||||
|
||||
// Clear the session cookie
|
||||
c.SetCookie(
|
||||
"sessionid", // name
|
||||
"", // value (empty = delete)
|
||||
-1, // max age (negative = delete)
|
||||
"/", // path
|
||||
"", // domain
|
||||
c.Request.TLS != nil, // secure
|
||||
true, // httpOnly
|
||||
)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "logged out",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SessionMiddleware checks if a user is logged in via session
|
||||
func SessionMiddleware(database *db.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
log.Printf("[DEBUG] SessionMiddleware processing request: %s %s",
|
||||
c.Request.Method, c.Request.URL.Path)
|
||||
|
||||
// First, try to get user from Authorization header for direct API access
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader != "" {
|
||||
log.Printf("[DEBUG] SessionMiddleware: Authorization header found, passing to next middleware")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// No Authorization header, check for session cookie
|
||||
sessionToken, err := c.Cookie("sessionid")
|
||||
if err != nil || sessionToken == "" {
|
||||
log.Printf("[ERROR] SessionMiddleware: No session cookie found: %v", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not logged in"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] SessionMiddleware: Found session cookie, validating")
|
||||
|
||||
// Validate the session
|
||||
userID, valid, err := validateSession(database, sessionToken)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] SessionMiddleware: Error validating session: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Session error"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if !valid {
|
||||
log.Printf("[ERROR] SessionMiddleware: Invalid or expired session")
|
||||
// Clear the invalid cookie
|
||||
c.SetCookie("sessionid", "", -1, "/", "", c.Request.TLS != nil, true)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Session expired"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] SessionMiddleware: Session valid for userID: %d", userID)
|
||||
|
||||
// Get the username for the user ID
|
||||
var username string
|
||||
err = database.QueryRow(`SELECT Username FROM "Users" WHERE UserID = $1`, userID).Scan(&username)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] SessionMiddleware: Error getting username for userID %d: %v",
|
||||
userID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "User data error"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if gpodder sync is enabled for this user
|
||||
var podSyncType string
|
||||
err = database.QueryRow(`SELECT Pod_Sync_Type FROM "Users" WHERE UserID = $1`, userID).Scan(&podSyncType)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] SessionMiddleware: Error checking sync type for userID %d: %v",
|
||||
userID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "User data error"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if podSyncType != "gpodder" && podSyncType != "both" {
|
||||
log.Printf("[ERROR] SessionMiddleware: Gpodder API not enabled for user: %s (sync type: %s)",
|
||||
username, podSyncType)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Gpodder API not enabled for this user"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Set the user information in the context
|
||||
c.Set("userID", userID)
|
||||
c.Set("username", username)
|
||||
|
||||
// Check if the path username matches the session username
|
||||
pathUsername := c.Param("username")
|
||||
if pathUsername != "" && strings.ToLower(pathUsername) != strings.ToLower(username) {
|
||||
log.Printf("[ERROR] SessionMiddleware: Username mismatch - Path: %s, Session: %s",
|
||||
pathUsername, username)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Username mismatch"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] SessionMiddleware: Session authentication successful for user: %s", username)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// AuthenticationMiddleware with GPodder token handling
|
||||
func AuthenticationMiddleware(database *db.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
log.Printf("[DEBUG] AuthenticationMiddleware processing request: %s %s",
|
||||
c.Request.Method, c.Request.URL.Path)
|
||||
|
||||
// Handle GPodder API standard .json suffix patterns
|
||||
if strings.HasSuffix(c.Request.URL.Path, ".json") {
|
||||
parts := strings.Split(c.Request.URL.Path, "/")
|
||||
var username string
|
||||
|
||||
// Handle /episodes/username.json pattern
|
||||
if strings.Contains(c.Request.URL.Path, "/episodes/") && len(parts) >= 3 {
|
||||
usernameWithExt := parts[len(parts)-1]
|
||||
username = strings.TrimSuffix(usernameWithExt, ".json")
|
||||
log.Printf("[DEBUG] AuthenticationMiddleware: Extracted username '%s' from episode actions URL", username)
|
||||
}
|
||||
|
||||
// Handle /devices/username.json pattern
|
||||
if strings.Contains(c.Request.URL.Path, "/devices/") {
|
||||
for i, part := range parts {
|
||||
if part == "devices" && i+1 < len(parts) {
|
||||
usernameWithExt := parts[i+1]
|
||||
username = strings.TrimSuffix(usernameWithExt, ".json")
|
||||
log.Printf("[DEBUG] AuthenticationMiddleware: Extracted username '%s' from devices URL", username)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set username parameter if extracted
|
||||
if username != "" {
|
||||
c.Params = append(c.Params, gin.Param{Key: "username", Value: username})
|
||||
}
|
||||
}
|
||||
|
||||
// First try session auth
|
||||
sessionToken, err := c.Cookie("sessionid")
|
||||
if err == nil && sessionToken != "" {
|
||||
log.Printf("[DEBUG] AuthenticationMiddleware: Found session cookie, validating")
|
||||
|
||||
userID, valid, err := validateSession(database, sessionToken)
|
||||
if err == nil && valid {
|
||||
log.Printf("[DEBUG] AuthenticationMiddleware: Session valid for userID: %d", userID)
|
||||
|
||||
var username string
|
||||
var query string
|
||||
|
||||
// Format query according to database type
|
||||
if database.IsPostgreSQLDB() {
|
||||
query = `SELECT Username FROM "Users" WHERE UserID = $1`
|
||||
} else {
|
||||
query = `SELECT Username FROM Users WHERE UserID = ?`
|
||||
}
|
||||
|
||||
err = database.QueryRow(query, userID).Scan(&username)
|
||||
if err == nil {
|
||||
var podSyncType string
|
||||
|
||||
// Format query according to database type
|
||||
if database.IsPostgreSQLDB() {
|
||||
query = `SELECT Pod_Sync_Type FROM "Users" WHERE UserID = $1`
|
||||
} else {
|
||||
query = `SELECT Pod_Sync_Type FROM Users WHERE UserID = ?`
|
||||
}
|
||||
|
||||
err = database.QueryRow(query, userID).Scan(&podSyncType)
|
||||
|
||||
if err == nil && (podSyncType == "gpodder" || podSyncType == "both") {
|
||||
// Check if the path username matches the session username
|
||||
pathUsername := c.Param("username")
|
||||
if pathUsername == "" || strings.ToLower(pathUsername) == strings.ToLower(username) {
|
||||
log.Printf("[DEBUG] AuthenticationMiddleware: Session auth successful for user: %s",
|
||||
username)
|
||||
c.Set("userID", userID)
|
||||
c.Set("username", username)
|
||||
c.Next()
|
||||
return
|
||||
} else {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Session username mismatch - Path: %s, Session: %s",
|
||||
pathUsername, username)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Gpodder not enabled for user: %s", username)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Could not get username for userID %d: %v",
|
||||
userID, err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Invalid session: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[DEBUG] AuthenticationMiddleware: No session cookie, falling back to basic auth")
|
||||
}
|
||||
|
||||
// Try basic auth if session auth failed
|
||||
log.Printf("[DEBUG] AuthenticationMiddleware: Attempting basic auth")
|
||||
|
||||
username := c.Param("username")
|
||||
if username == "" {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Username parameter is missing in path")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is an internal API call via X-GPodder-Token
|
||||
gpodderTokenHeader := c.GetHeader("X-GPodder-Token")
|
||||
if gpodderTokenHeader != "" {
|
||||
log.Printf("[DEBUG] AuthenticationMiddleware: Found X-GPodder-Token header")
|
||||
|
||||
// Get user data
|
||||
var userID int
|
||||
var gpodderToken sql.NullString
|
||||
var podSyncType string
|
||||
var query string
|
||||
|
||||
// Format query according to database type
|
||||
if database.IsPostgreSQLDB() {
|
||||
query = `SELECT UserID, GpodderToken, Pod_Sync_Type FROM "Users"
|
||||
WHERE LOWER(Username) = LOWER($1)`
|
||||
} else {
|
||||
query = `SELECT UserID, GpodderToken, Pod_Sync_Type FROM Users
|
||||
WHERE LOWER(Username) = LOWER(?)`
|
||||
}
|
||||
|
||||
err := database.QueryRow(query, username).Scan(&userID, &gpodderToken, &podSyncType)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Database error: %v", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if gpodder sync is enabled
|
||||
if podSyncType != "gpodder" && podSyncType != "both" && podSyncType != "external" {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Gpodder API not enabled for user: %s (sync type: %s)",
|
||||
username, podSyncType)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Gpodder API not enabled for this user"})
|
||||
return
|
||||
}
|
||||
|
||||
// For internal calls with X-GPodder-Token header, validate token directly
|
||||
if gpodderToken.Valid && gpodderToken.String == gpodderTokenHeader {
|
||||
log.Printf("[DEBUG] AuthenticationMiddleware: X-GPodder-Token validated for user: %s", username)
|
||||
c.Set("userID", userID)
|
||||
c.Set("username", username)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// If token doesn't match, authentication failed
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Invalid X-GPodder-Token for user: %s", username)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Standard basic auth handling
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: No Authorization header found")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Basic" {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Invalid Authorization header format")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"})
|
||||
return
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Failed to decode base64 credentials: %v", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header"})
|
||||
return
|
||||
}
|
||||
|
||||
credentials := strings.SplitN(string(decoded), ":", 2)
|
||||
if len(credentials) != 2 {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Invalid credentials format")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials format"})
|
||||
return
|
||||
}
|
||||
|
||||
authUsername := credentials[0]
|
||||
password := credentials[1]
|
||||
|
||||
if strings.ToLower(username) != strings.ToLower(authUsername) {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Username mismatch - URL: %s, Auth: %s",
|
||||
strings.ToLower(username), strings.ToLower(authUsername))
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Username mismatch"})
|
||||
return
|
||||
}
|
||||
|
||||
var userID int
|
||||
var hashedPassword string
|
||||
var podSyncType string
|
||||
var gpodderToken sql.NullString
|
||||
var query string
|
||||
|
||||
// Format query according to database type
|
||||
if database.IsPostgreSQLDB() {
|
||||
query = `SELECT UserID, Hashed_PW, Pod_Sync_Type, GpodderToken FROM "Users"
|
||||
WHERE LOWER(Username) = LOWER($1)`
|
||||
} else {
|
||||
query = `SELECT UserID, Hashed_PW, Pod_Sync_Type, GpodderToken FROM Users
|
||||
WHERE LOWER(Username) = LOWER(?)`
|
||||
}
|
||||
|
||||
err = database.QueryRow(query, username).Scan(&userID, &hashedPassword, &podSyncType, &gpodderToken)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: User not found: %s", username)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
|
||||
} else {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Database error: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if podSyncType != "gpodder" && podSyncType != "both" && podSyncType != "external" {
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Gpodder API not enabled for user: %s (sync type: %s)",
|
||||
username, podSyncType)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Gpodder API not enabled for this user"})
|
||||
return
|
||||
}
|
||||
|
||||
// Flag to track authentication success
|
||||
authenticated := false
|
||||
|
||||
// Check if this is a gpodder token authentication
|
||||
if gpodderToken.Valid && gpodderToken.String == password {
|
||||
log.Printf("[DEBUG] AuthenticationMiddleware: User authenticated with gpodder token: %s", username)
|
||||
authenticated = true
|
||||
}
|
||||
|
||||
// If token auth didn't succeed, try password authentication
|
||||
if !authenticated && verifyPassword(password, hashedPassword) {
|
||||
log.Printf("[DEBUG] AuthenticationMiddleware: User authenticated with password: %s", username)
|
||||
authenticated = true
|
||||
}
|
||||
|
||||
// If authentication was successful, set context and continue
|
||||
if authenticated {
|
||||
c.Set("userID", userID)
|
||||
c.Set("username", username)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Authentication failed
|
||||
log.Printf("[ERROR] AuthenticationMiddleware: Invalid credentials for user: %s", username)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
|
||||
}
|
||||
}
|
||||
|
||||
// verifyPassword verifies a password against a hash using Argon2
|
||||
// This implementation matches the Pinepods authentication mechanism using alexedwards/argon2id
|
||||
func verifyPassword(password, hashedPassword string) bool {
|
||||
// Use the alexedwards/argon2id package to compare password and hash
|
||||
match, err := argon2id.ComparePasswordAndHash(password, hashedPassword)
|
||||
if err != nil {
|
||||
// Log the error in a production environment
|
||||
return false
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
Reference in New Issue
Block a user