* refactor(auth): replace dashboard sessions with stateless tokens * feat(auth): harden session issuance and distributed enforcement * fix(proxy): preserve trusted proxy compatibility defaults * refactor: address dashboard auth review feedback * refactor: remove classic frontend and flatten web app
85 lines
2.8 KiB
Go
85 lines
2.8 KiB
Go
package common
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// NormalizeOrigin validates and canonicalizes a browser origin. Only an exact
|
|
// scheme/host/effective-port match is meaningful; paths and wildcards are not
|
|
// accepted for authentication cookie endpoints.
|
|
func NormalizeOrigin(raw string) (string, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" || raw == "null" || strings.ContainsAny(raw, "\r\n") {
|
|
return "", fmt.Errorf("origin is empty or invalid")
|
|
}
|
|
parsedURL, err := url.Parse(raw)
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid origin: %w", err)
|
|
}
|
|
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
|
return "", fmt.Errorf("origin scheme must be http or https")
|
|
}
|
|
if parsedURL.Host == "" || parsedURL.User != nil || parsedURL.RawQuery != "" || parsedURL.Fragment != "" || (parsedURL.Path != "" && parsedURL.Path != "/") {
|
|
return "", fmt.Errorf("origin must contain only scheme and host")
|
|
}
|
|
hostname := strings.ToLower(parsedURL.Hostname())
|
|
if hostname == "" || strings.Contains(hostname, "*") {
|
|
return "", fmt.Errorf("origin host is empty")
|
|
}
|
|
port := parsedURL.Port()
|
|
normalizedHost := hostname
|
|
if strings.Contains(hostname, ":") {
|
|
normalizedHost = "[" + hostname + "]"
|
|
}
|
|
if port == "" || (parsedURL.Scheme == "http" && port == "80") || (parsedURL.Scheme == "https" && port == "443") {
|
|
return parsedURL.Scheme + "://" + normalizedHost, nil
|
|
}
|
|
return parsedURL.Scheme + "://" + net.JoinHostPort(hostname, port), nil
|
|
}
|
|
|
|
func InitSessionCookieSettings() error {
|
|
secureRaw := strings.TrimSpace(os.Getenv("SESSION_COOKIE_SECURE"))
|
|
trustedURLsRaw := strings.TrimSpace(os.Getenv("SESSION_COOKIE_TRUSTED_URL"))
|
|
|
|
SessionCookieSecure = false
|
|
SessionCookieTrustedURLs = nil
|
|
|
|
if secureRaw == "" || strings.EqualFold(secureRaw, "false") {
|
|
if trustedURLsRaw != "" {
|
|
return fmt.Errorf("SESSION_COOKIE_TRUSTED_URL requires SESSION_COOKIE_SECURE=true")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if !strings.EqualFold(secureRaw, "true") {
|
|
return fmt.Errorf("SESSION_COOKIE_SECURE must be true or false")
|
|
}
|
|
|
|
if trustedURLsRaw == "" {
|
|
return fmt.Errorf("SESSION_COOKIE_SECURE=true requires SESSION_COOKIE_TRUSTED_URL")
|
|
}
|
|
|
|
trustedURLs := strings.Split(trustedURLsRaw, ",")
|
|
for _, trustedURL := range trustedURLs {
|
|
trustedURL = strings.TrimSpace(trustedURL)
|
|
if trustedURL == "" {
|
|
return fmt.Errorf("SESSION_COOKIE_TRUSTED_URL contains an empty URL")
|
|
}
|
|
normalizedOrigin, err := NormalizeOrigin(trustedURL)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid SESSION_COOKIE_TRUSTED_URL: %w", err)
|
|
}
|
|
if !strings.HasPrefix(normalizedOrigin, "https://") {
|
|
return fmt.Errorf("SESSION_COOKIE_TRUSTED_URL must contain only https URLs with hosts")
|
|
}
|
|
SessionCookieTrustedURLs = append(SessionCookieTrustedURLs, normalizedOrigin)
|
|
}
|
|
|
|
SessionCookieSecure = true
|
|
return nil
|
|
}
|