mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-07-19 16:31:15 +00:00
fix: support for parent trusted domain, fixes #1021
This commit is contained in:
@@ -88,12 +88,32 @@ const getEffectivePort = (url: URL): string => {
|
|||||||
return "80";
|
return "80";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// https://www.geeksforgeeks.org/javascript/how-to-check-if-a-string-is-a-valid-ip-address-format-in-javascript
|
||||||
|
const isIP = (str: string): boolean => {
|
||||||
|
const ipv4 =
|
||||||
|
/^(\d{1,3}\.){3}\d{1,3}$/;
|
||||||
|
const ipv6 =
|
||||||
|
/^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
||||||
|
return ipv4.test(str) || ipv6.test(str) || str.startsWith("[");
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimPeriod = (str: string): string => {
|
||||||
|
if(str.lastIndexOf('.') === (str.length - 1)){
|
||||||
|
str = str.substring(0, str.length - 1);
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
export const isTrustedDomain = (
|
export const isTrustedDomain = (
|
||||||
url: URL,
|
url: URL,
|
||||||
appUrl: URL,
|
appUrl: URL,
|
||||||
cookieDomain: string,
|
cookieDomain: string,
|
||||||
subdomainsEnabled: boolean,
|
subdomainsEnabled: boolean,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
if (isIP(url.hostname)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (url.protocol != appUrl.protocol) {
|
if (url.protocol != appUrl.protocol) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -102,7 +122,7 @@ export const isTrustedDomain = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.hostname == appUrl.hostname) {
|
if (trimPeriod(url.hostname) == trimPeriod(appUrl.hostname)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,9 +130,6 @@ export const isTrustedDomain = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.hostname.endsWith("." + cookieDomain.toLowerCase())) {
|
return trimPeriod(url.hostname).endsWith("." + cookieDomain.toLowerCase())
|
||||||
return true;
|
|| trimPeriod(url.hostname) == cookieDomain.toLowerCase();
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,17 +88,13 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.log.App.Info().Msgf("Starting Tinyauth version: %s", model.Version)
|
app.log.App.Info().Msgf("Starting Tinyauth version: %s", model.Version)
|
||||||
|
|
||||||
// get app url
|
// get app url
|
||||||
if app.config.AppURL == "" {
|
appURL, err := utils.SafeParseAppURL(app.config.AppURL)
|
||||||
return errors.New("app url cannot be empty, perhaps config loading failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
appUrl, err := url.Parse(app.config.AppURL)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse app url: %w", err)
|
return fmt.Errorf("failed to parse app url: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.runtime.AppURL = strings.ToLower(appUrl.Scheme + "://" + appUrl.Host)
|
app.runtime.AppURL = appURL
|
||||||
|
|
||||||
// validate session config
|
// validate session config
|
||||||
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
||||||
@@ -172,7 +168,13 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.runtime.CookieDomain = cookieDomain
|
app.runtime.CookieDomain = cookieDomain
|
||||||
|
|
||||||
// cookie names
|
// cookie names
|
||||||
app.runtime.UUID = utils.GenerateUUID(appUrl.Hostname())
|
u, err := url.Parse(app.runtime.AppURL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse app url: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.runtime.UUID = utils.GenerateUUID(u.Hostname())
|
||||||
|
|
||||||
cookieId := strings.Split(app.runtime.UUID, "-")[0] // first 8 characters of the uuid should be good enough
|
cookieId := strings.Split(app.runtime.UUID, "-")[0] // first 8 characters of the uuid should be good enough
|
||||||
|
|
||||||
|
|||||||
@@ -351,7 +351,8 @@ func (controller *OAuthController) isRedirectSafe(redirectURI string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasSuffix(hostname, "."+strings.ToLower(controller.runtime.CookieDomain)) {
|
if strings.HasSuffix(hostname, "."+strings.ToLower(controller.runtime.CookieDomain)) ||
|
||||||
|
hostname == controller.runtime.CookieDomain {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,55 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/weppos/publicsuffix-go/publicsuffix"
|
"github.com/weppos/publicsuffix-go/publicsuffix"
|
||||||
|
"golang.org/x/net/idna"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetCookieDomain parses the app url and returns the domain value to use for cookies.
|
var (
|
||||||
|
ErrEmptyURL = fmt.Errorf("invalid url")
|
||||||
|
)
|
||||||
|
|
||||||
|
func SafeParseAppURL(str string) (string, error) {
|
||||||
|
if strings.TrimSpace(str) == "" {
|
||||||
|
return "", ErrEmptyURL
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(str)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid url: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Host == "" ||
|
||||||
|
u.Scheme != "http" &&
|
||||||
|
u.Scheme != "https" {
|
||||||
|
return "", fmt.Errorf("invalid url, must be in format https(s)://host")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname := strings.ToLower(u.Hostname())
|
||||||
|
hostname = strings.TrimSuffix(hostname, ".")
|
||||||
|
|
||||||
|
if netIP := net.ParseIP(hostname); netIP != nil {
|
||||||
|
return "", fmt.Errorf("ip addresses not allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname, err = idna.Lookup.ToASCII(hostname)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to convert hostname to ascii: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
appURL := fmt.Sprintf("%s://%s", u.Scheme, hostname)
|
||||||
|
|
||||||
|
if u.Port() != "" {
|
||||||
|
appURL += ":" + u.Port()
|
||||||
|
}
|
||||||
|
|
||||||
|
return appURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCookieDomain parses the app URL and returns the domain value to use for cookies.
|
||||||
// When auth for subdomains is enabled, it strips the leftmost label
|
// When auth for subdomains is enabled, it strips the leftmost label
|
||||||
|
// GetCookieDomain assumes the app URL is first parsed with SafeParseAppURL
|
||||||
// (e.g. sub1.sub2.domain.com -> sub2.domain.com), otherwise it returns the full hostname.
|
// (e.g. sub1.sub2.domain.com -> sub2.domain.com), otherwise it returns the full hostname.
|
||||||
func GetCookieDomain(appUrl string, subdomainsEnabled bool) (string, error) {
|
func GetCookieDomain(appUrl string, subdomainsEnabled bool) (string, error) {
|
||||||
u, err := url.Parse(appUrl)
|
u, err := url.Parse(appUrl)
|
||||||
@@ -21,10 +66,6 @@ func GetCookieDomain(appUrl string, subdomainsEnabled bool) (string, error) {
|
|||||||
|
|
||||||
hostname := strings.ToLower(u.Hostname())
|
hostname := strings.ToLower(u.Hostname())
|
||||||
|
|
||||||
if netIP := net.ParseIP(hostname); netIP != nil {
|
|
||||||
return "", fmt.Errorf("ip addresses not allowed")
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(hostname, ".")
|
parts := strings.Split(hostname, ".")
|
||||||
|
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
|
|||||||
@@ -7,7 +7,86 @@ import (
|
|||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetRootDomain(t *testing.T) {
|
func TestSafeParseAPPURL(t *testing.T) {
|
||||||
|
// Normal app url
|
||||||
|
appURL := "http://sub.tinyauth.app"
|
||||||
|
expected := "http://sub.tinyauth.app"
|
||||||
|
result, err := utils.SafeParseAppURL(appURL)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
|
// Strip path
|
||||||
|
appURL = "http://sub.tinyauth.app/path"
|
||||||
|
expected = "http://sub.tinyauth.app"
|
||||||
|
result, err = utils.SafeParseAppURL(appURL)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
|
// Preserve port
|
||||||
|
appURL = "http://sub.tinyauth.app:8080"
|
||||||
|
expected = "http://sub.tinyauth.app:8080"
|
||||||
|
result, err = utils.SafeParseAppURL(appURL)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
|
// Remove trailing dot
|
||||||
|
appURL = "http://sub.tinyauth.app."
|
||||||
|
expected = "http://sub.tinyauth.app"
|
||||||
|
result, err = utils.SafeParseAppURL(appURL)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
|
// Convert to ascii
|
||||||
|
appURL = "http://bücher.example.com"
|
||||||
|
expected = "http://xn--bcher-kva.example.com"
|
||||||
|
result, err = utils.SafeParseAppURL(appURL)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
|
// Lowercase
|
||||||
|
appURL = "HTTP://SUb.tinyAUth.aPP"
|
||||||
|
expected = "http://sub.tinyauth.app"
|
||||||
|
result, err = utils.SafeParseAppURL(appURL)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
|
// Empty string
|
||||||
|
appURL = ""
|
||||||
|
_, err = utils.SafeParseAppURL(appURL)
|
||||||
|
assert.ErrorIs(t, err, utils.ErrEmptyURL)
|
||||||
|
|
||||||
|
// Invalid URL
|
||||||
|
appURL = "invalidurl"
|
||||||
|
_, err = utils.SafeParseAppURL(appURL)
|
||||||
|
assert.ErrorContains(t, err, "invalid url")
|
||||||
|
|
||||||
|
// Non http or https URL
|
||||||
|
appURL = "ftp://sub.tinyauth.app"
|
||||||
|
_, err = utils.SafeParseAppURL(appURL)
|
||||||
|
assert.ErrorContains(t, err, "invalid url")
|
||||||
|
|
||||||
|
// Invalid punycode
|
||||||
|
appURL = "http://ab--cd.example.com"
|
||||||
|
_, err = utils.SafeParseAppURL(appURL)
|
||||||
|
assert.ErrorContains(t, err, "failed to convert hostname to ascii")
|
||||||
|
|
||||||
|
// IP address
|
||||||
|
appURL = "http://10.10.10.10"
|
||||||
|
_, err = utils.SafeParseAppURL(appURL)
|
||||||
|
assert.ErrorContains(t, err, "ip addresses not allowed")
|
||||||
|
|
||||||
|
// IPv6 address
|
||||||
|
appURL = "http://[::1]:8080"
|
||||||
|
_, err = utils.SafeParseAppURL(appURL)
|
||||||
|
assert.ErrorContains(t, err, "ip addresses not allowed")
|
||||||
|
|
||||||
|
// Invalid URL
|
||||||
|
appURL = "://"
|
||||||
|
_, err = utils.SafeParseAppURL(appURL)
|
||||||
|
assert.ErrorContains(t, err, "invalid url")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCookieDomain(t *testing.T) {
|
||||||
// Normal case
|
// Normal case
|
||||||
domain := "http://sub.tinyauth.app"
|
domain := "http://sub.tinyauth.app"
|
||||||
expected := "tinyauth.app"
|
expected := "tinyauth.app"
|
||||||
@@ -27,11 +106,6 @@ func TestGetRootDomain(t *testing.T) {
|
|||||||
_, err = utils.GetCookieDomain(domain, true)
|
_, err = utils.GetCookieDomain(domain, true)
|
||||||
assert.EqualError(t, err, "invalid app url, must be in format subdomain.domain.tld or domain.tld")
|
assert.EqualError(t, err, "invalid app url, must be in format subdomain.domain.tld or domain.tld")
|
||||||
|
|
||||||
// IP address
|
|
||||||
domain = "http://10.10.10.10"
|
|
||||||
_, err = utils.GetCookieDomain(domain, true)
|
|
||||||
assert.ErrorContains(t, err, "ip addresses not allowed")
|
|
||||||
|
|
||||||
// Invalid URL
|
// Invalid URL
|
||||||
domain = "http://[::1]:namedport"
|
domain = "http://[::1]:namedport"
|
||||||
_, err = utils.GetCookieDomain(domain, true)
|
_, err = utils.GetCookieDomain(domain, true)
|
||||||
@@ -56,6 +130,11 @@ func TestGetRootDomain(t *testing.T) {
|
|||||||
_, err = utils.GetCookieDomain(domain, true)
|
_, err = utils.GetCookieDomain(domain, true)
|
||||||
assert.ErrorContains(t, err, "domain in public suffix list, cannot set cookies")
|
assert.ErrorContains(t, err, "domain in public suffix list, cannot set cookies")
|
||||||
|
|
||||||
|
// Domain managed by ICANN without subdomain
|
||||||
|
domain = "http://co.uk"
|
||||||
|
_, err = utils.GetCookieDomain(domain, true)
|
||||||
|
assert.ErrorContains(t, err, "domain in public suffix list, cannot set cookies")
|
||||||
|
|
||||||
// Domain without subdomain
|
// Domain without subdomain
|
||||||
domain = "http://tinyauth.app"
|
domain = "http://tinyauth.app"
|
||||||
expected = "tinyauth.app"
|
expected = "tinyauth.app"
|
||||||
|
|||||||
Reference in New Issue
Block a user