mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-03 23:55:44 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			292 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			292 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package controller
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"net/http"
 | 
						|
	"strings"
 | 
						|
	"tinyauth/internal/config"
 | 
						|
	"tinyauth/internal/service"
 | 
						|
	"tinyauth/internal/utils"
 | 
						|
 | 
						|
	"github.com/gin-gonic/gin"
 | 
						|
	"github.com/google/go-querystring/query"
 | 
						|
	"github.com/rs/zerolog/log"
 | 
						|
)
 | 
						|
 | 
						|
type Proxy struct {
 | 
						|
	Proxy string `uri:"proxy" binding:"required"`
 | 
						|
}
 | 
						|
 | 
						|
type ProxyControllerConfig struct {
 | 
						|
	AppURL string
 | 
						|
}
 | 
						|
 | 
						|
type ProxyController struct {
 | 
						|
	config ProxyControllerConfig
 | 
						|
	router *gin.RouterGroup
 | 
						|
	docker *service.DockerService
 | 
						|
	auth   *service.AuthService
 | 
						|
}
 | 
						|
 | 
						|
func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, docker *service.DockerService, auth *service.AuthService) *ProxyController {
 | 
						|
	return &ProxyController{
 | 
						|
		config: config,
 | 
						|
		router: router,
 | 
						|
		docker: docker,
 | 
						|
		auth:   auth,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (controller *ProxyController) SetupRoutes() {
 | 
						|
	proxyGroup := controller.router.Group("/auth")
 | 
						|
	proxyGroup.GET("/:proxy", controller.proxyHandler)
 | 
						|
}
 | 
						|
 | 
						|
func (controller *ProxyController) proxyHandler(c *gin.Context) {
 | 
						|
	var req Proxy
 | 
						|
 | 
						|
	err := c.BindUri(&req)
 | 
						|
	if err != nil {
 | 
						|
		log.Error().Err(err).Msg("Failed to bind URI")
 | 
						|
		c.JSON(400, gin.H{
 | 
						|
			"status":  400,
 | 
						|
			"message": "Bad Request",
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if req.Proxy != "nginx" && req.Proxy != "traefik" && req.Proxy != "caddy" {
 | 
						|
		log.Warn().Str("proxy", req.Proxy).Msg("Invalid proxy")
 | 
						|
		c.JSON(400, gin.H{
 | 
						|
			"status":  400,
 | 
						|
			"message": "Bad Request",
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html")
 | 
						|
 | 
						|
	if isBrowser {
 | 
						|
		log.Debug().Msg("Request identified as (most likely) coming from a browser")
 | 
						|
	} else {
 | 
						|
		log.Debug().Msg("Request identified as (most likely) coming from a non-browser client")
 | 
						|
	}
 | 
						|
 | 
						|
	uri := c.Request.Header.Get("X-Forwarded-Uri")
 | 
						|
	proto := c.Request.Header.Get("X-Forwarded-Proto")
 | 
						|
	host := c.Request.Header.Get("X-Forwarded-Host")
 | 
						|
 | 
						|
	labels, err := controller.docker.GetLabels(host)
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		log.Error().Err(err).Msg("Failed to get labels from Docker")
 | 
						|
		controller.handleError(c, req, isBrowser)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	clientIP := c.ClientIP()
 | 
						|
 | 
						|
	if controller.auth.IsBypassedIP(labels.IP, clientIP) {
 | 
						|
		controller.setHeaders(c, labels)
 | 
						|
		c.JSON(200, gin.H{
 | 
						|
			"status":  200,
 | 
						|
			"message": "Authenticated",
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	authEnabled, err := controller.auth.IsAuthEnabled(uri, labels.Path)
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		log.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
 | 
						|
		controller.handleError(c, req, isBrowser)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if !authEnabled {
 | 
						|
		log.Debug().Msg("Authentication disabled for resource, allowing access")
 | 
						|
		controller.setHeaders(c, labels)
 | 
						|
		c.JSON(200, gin.H{
 | 
						|
			"status":  200,
 | 
						|
			"message": "Authenticated",
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if !controller.auth.CheckIP(labels.IP, clientIP) {
 | 
						|
		if req.Proxy == "nginx" || !isBrowser {
 | 
						|
			c.JSON(401, gin.H{
 | 
						|
				"status":  401,
 | 
						|
				"message": "Unauthorized",
 | 
						|
			})
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		queries, err := query.Values(config.UnauthorizedQuery{
 | 
						|
			Resource: strings.Split(host, ".")[0],
 | 
						|
			IP:       clientIP,
 | 
						|
		})
 | 
						|
 | 
						|
		if err != nil {
 | 
						|
			log.Error().Err(err).Msg("Failed to encode unauthorized query")
 | 
						|
			c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode()))
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	var userContext config.UserContext
 | 
						|
 | 
						|
	context, err := utils.GetContext(c)
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		log.Debug().Msg("No user context found in request, treating as not logged in")
 | 
						|
		userContext = config.UserContext{
 | 
						|
			IsLoggedIn: false,
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		userContext = context
 | 
						|
	}
 | 
						|
 | 
						|
	if userContext.Provider == "basic" && userContext.TotpEnabled {
 | 
						|
		log.Debug().Msg("User has TOTP enabled, denying basic auth access")
 | 
						|
		userContext.IsLoggedIn = false
 | 
						|
	}
 | 
						|
 | 
						|
	if userContext.IsLoggedIn {
 | 
						|
		appAllowed := controller.auth.IsResourceAllowed(c, userContext, labels)
 | 
						|
 | 
						|
		if !appAllowed {
 | 
						|
			log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
 | 
						|
 | 
						|
			if req.Proxy == "nginx" || !isBrowser {
 | 
						|
				c.JSON(403, gin.H{
 | 
						|
					"status":  403,
 | 
						|
					"message": "Forbidden",
 | 
						|
				})
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			queries, err := query.Values(config.UnauthorizedQuery{
 | 
						|
				Resource: strings.Split(host, ".")[0],
 | 
						|
			})
 | 
						|
 | 
						|
			if err != nil {
 | 
						|
				log.Error().Err(err).Msg("Failed to encode unauthorized query")
 | 
						|
				c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			if userContext.OAuth {
 | 
						|
				queries.Set("username", userContext.Email)
 | 
						|
			} else {
 | 
						|
				queries.Set("username", userContext.Username)
 | 
						|
			}
 | 
						|
 | 
						|
			c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode()))
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		if userContext.OAuth {
 | 
						|
			groupOK := controller.auth.IsInOAuthGroup(c, userContext, labels.OAuth.Groups)
 | 
						|
 | 
						|
			if !groupOK {
 | 
						|
				log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements")
 | 
						|
 | 
						|
				if req.Proxy == "nginx" || !isBrowser {
 | 
						|
					c.JSON(403, gin.H{
 | 
						|
						"status":  403,
 | 
						|
						"message": "Forbidden",
 | 
						|
					})
 | 
						|
					return
 | 
						|
				}
 | 
						|
 | 
						|
				queries, err := query.Values(config.UnauthorizedQuery{
 | 
						|
					Resource: strings.Split(host, ".")[0],
 | 
						|
					GroupErr: true,
 | 
						|
				})
 | 
						|
 | 
						|
				if err != nil {
 | 
						|
					log.Error().Err(err).Msg("Failed to encode unauthorized query")
 | 
						|
					c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
 | 
						|
					return
 | 
						|
				}
 | 
						|
 | 
						|
				if userContext.OAuth {
 | 
						|
					queries.Set("username", userContext.Email)
 | 
						|
				} else {
 | 
						|
					queries.Set("username", userContext.Username)
 | 
						|
				}
 | 
						|
 | 
						|
				c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode()))
 | 
						|
				return
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		c.Header("Remote-User", utils.SanitizeHeader(userContext.Username))
 | 
						|
		c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name))
 | 
						|
		c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email))
 | 
						|
		c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))
 | 
						|
 | 
						|
		controller.setHeaders(c, labels)
 | 
						|
 | 
						|
		c.JSON(200, gin.H{
 | 
						|
			"status":  200,
 | 
						|
			"message": "Authenticated",
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if req.Proxy == "nginx" || !isBrowser {
 | 
						|
		c.JSON(401, gin.H{
 | 
						|
			"status":  401,
 | 
						|
			"message": "Unauthorized",
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	queries, err := query.Values(config.RedirectQuery{
 | 
						|
		RedirectURI: fmt.Sprintf("%s://%s%s", proto, host, uri),
 | 
						|
	})
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		log.Error().Err(err).Msg("Failed to encode redirect URI query")
 | 
						|
		c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode()))
 | 
						|
}
 | 
						|
 | 
						|
func (controller *ProxyController) setHeaders(c *gin.Context, labels config.App) {
 | 
						|
	c.Header("Authorization", c.Request.Header.Get("Authorization"))
 | 
						|
 | 
						|
	headers := utils.ParseHeaders(labels.Response.Headers)
 | 
						|
 | 
						|
	for key, value := range headers {
 | 
						|
		log.Debug().Str("header", key).Msg("Setting header")
 | 
						|
		c.Header(key, value)
 | 
						|
	}
 | 
						|
 | 
						|
	basicPassword := utils.GetSecret(labels.Response.BasicAuth.Password, labels.Response.BasicAuth.PasswordFile)
 | 
						|
 | 
						|
	if labels.Response.BasicAuth.Username != "" && basicPassword != "" {
 | 
						|
		log.Debug().Str("username", labels.Response.BasicAuth.Username).Msg("Setting basic auth header")
 | 
						|
		c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Response.BasicAuth.Username, basicPassword)))
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (controller *ProxyController) handleError(c *gin.Context, req Proxy, isBrowser bool) {
 | 
						|
	if req.Proxy == "nginx" || !isBrowser {
 | 
						|
		c.JSON(500, gin.H{
 | 
						|
			"status":  500,
 | 
						|
			"message": "Internal Server Error",
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
 | 
						|
}
 |