Files
new-api/common/session_cookie.go
T
Calcium-Ion 31d70fca39 refactor(auth): replace dashboard sessions with stateless tokens and session control (#6329)
* 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
2026-07-20 16:48:43 +08:00

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
}