Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 601089d62e chore(deps): bump modernc.org/sqlite
Bumps the minor-patch group with 1 update in the / directory: [modernc.org/sqlite](https://gitlab.com/cznic/sqlite).


Updates `modernc.org/sqlite` from 1.52.0 to 1.53.0
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.52.0...v1.53.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 08:13:35 +00:00
28 changed files with 449 additions and 507 deletions
+2 -8
View File
@@ -32,6 +32,8 @@ TINYAUTH_SERVER_PORT=3000
TINYAUTH_SERVER_ADDRESS="0.0.0.0" TINYAUTH_SERVER_ADDRESS="0.0.0.0"
# The path to the Unix socket. # The path to the Unix socket.
TINYAUTH_SERVER_SOCKETPATH= TINYAUTH_SERVER_SOCKETPATH=
# Enable listening on both TCP and Unix socket at the same time.
TINYAUTH_SERVER_CONCURRENTLISTENERSENABLED=false
# auth config # auth config
@@ -97,8 +99,6 @@ TINYAUTH_AUTH_SESSIONMAXLIFETIME=0
TINYAUTH_AUTH_LOGINTIMEOUT=300 TINYAUTH_AUTH_LOGINTIMEOUT=300
# Maximum login retries. # Maximum login retries.
TINYAUTH_AUTH_LOGINMAXRETRIES=3 TINYAUTH_AUTH_LOGINMAXRETRIES=3
# Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically.
TINYAUTH_AUTH_LOCKDOWNENABLED=true
# Comma-separated list of trusted proxy addresses. # Comma-separated list of trusted proxy addresses.
TINYAUTH_AUTH_TRUSTEDPROXIES= TINYAUTH_AUTH_TRUSTEDPROXIES=
# ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow. # ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow.
@@ -206,8 +206,6 @@ TINYAUTH_LDAP_ADDRESS=
TINYAUTH_LDAP_BINDDN= TINYAUTH_LDAP_BINDDN=
# Bind password for LDAP authentication. # Bind password for LDAP authentication.
TINYAUTH_LDAP_BINDPASSWORD= TINYAUTH_LDAP_BINDPASSWORD=
# Path to the Bind password.
TINYAUTH_LDAP_BINDPASSWORDFILE=
# Base DN for LDAP searches. # Base DN for LDAP searches.
TINYAUTH_LDAP_BASEDN= TINYAUTH_LDAP_BASEDN=
# Allow insecure LDAP connections. # Allow insecure LDAP connections.
@@ -254,7 +252,3 @@ TINYAUTH_TAILSCALE_HOSTNAME=
TINYAUTH_TAILSCALE_AUTHKEY= TINYAUTH_TAILSCALE_AUTHKEY=
# Use ephemeral Tailscale node. # Use ephemeral Tailscale node.
TINYAUTH_TAILSCALE_EPHEMERAL=false TINYAUTH_TAILSCALE_EPHEMERAL=false
# Enable Tailscale Funnel.
TINYAUTH_TAILSCALE_FUNNEL=false
# Listen on the Tailscale address instead of standard address.
TINYAUTH_TAILSCALE_LISTEN=false
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
@@ -62,6 +62,6 @@ jobs:
run: go test -coverprofile=coverage.txt -v ./... run: go test -coverprofile=coverage.txt -v ./...
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
+10 -10
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Delete old release - name: Delete old release
run: gh release delete --cleanup-tag --yes nightly || echo release not found run: gh release delete --cleanup-tag --yes nightly || echo release not found
@@ -23,7 +23,7 @@ jobs:
REPO: ${{ github.event.repository.name }} REPO: ${{ github.event.repository.name }}
- name: Create release - name: Create release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3 uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
with: with:
prerelease: true prerelease: true
tag_name: nightly tag_name: nightly
@@ -37,7 +37,7 @@ jobs:
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }} BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: nightly ref: nightly
@@ -55,7 +55,7 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: nightly ref: nightly
@@ -100,7 +100,7 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: nightly ref: nightly
@@ -145,7 +145,7 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: nightly ref: nightly
@@ -203,7 +203,7 @@ jobs:
- image-build - image-build
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: nightly ref: nightly
@@ -261,7 +261,7 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: nightly ref: nightly
@@ -319,7 +319,7 @@ jobs:
- image-build-arm - image-build-arm
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: nightly ref: nightly
@@ -461,7 +461,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Release - name: Release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3 uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
with: with:
files: binaries/* files: binaries/*
tag_name: nightly tag_name: nightly
+8 -8
View File
@@ -18,7 +18,7 @@ jobs:
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }} BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Generate metadata - name: Generate metadata
id: metadata id: metadata
@@ -33,7 +33,7 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
@@ -75,7 +75,7 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
@@ -117,7 +117,7 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Docker meta - name: Docker meta
id: meta id: meta
@@ -173,7 +173,7 @@ jobs:
- image-build - image-build
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Docker meta - name: Docker meta
id: meta id: meta
@@ -229,7 +229,7 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Docker meta - name: Docker meta
id: meta id: meta
@@ -285,7 +285,7 @@ jobs:
- image-build-arm - image-build-arm
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Docker meta - name: Docker meta
id: meta id: meta
@@ -432,6 +432,6 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Release - name: Release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3 uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
with: with:
files: binaries/* files: binaries/*
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
with: with:
persist-credentials: false persist-credentials: false
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Generate Sponsors - name: Generate Sponsors
uses: JamesIves/github-sponsors-readme-action@2fd9142e765f755780202122261dc85e78459405 # v1 uses: JamesIves/github-sponsors-readme-action@2fd9142e765f755780202122261dc85e78459405 # v1
+5 -13
View File
@@ -3,7 +3,6 @@ import { Outlet } from "react-router";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { DomainWarning } from "../domain-warning/domain-warning"; import { DomainWarning } from "../domain-warning/domain-warning";
import { QuickActions } from "../quick-actions/quick-actions"; import { QuickActions } from "../quick-actions/quick-actions";
import { isTrustedDomain } from "@/lib/hooks/redirect-uri";
const BaseLayout = ({ children }: { children: React.ReactNode }) => { const BaseLayout = ({ children }: { children: React.ReactNode }) => {
const { ui } = useAppContext(); const { ui } = useAppContext();
@@ -41,18 +40,11 @@ export const Layout = () => {
setIgnoreDomainWarning(true); setIgnoreDomainWarning(true);
}, [setIgnoreDomainWarning]); }, [setIgnoreDomainWarning]);
const isTrusted = (() => { if (
try { !ignoreDomainWarning &&
const appUrlObj = new URL(app.appUrl); ui.warningsEnabled &&
const currentUrlObj = new URL(currentUrl); !app.trustedDomains.includes(currentUrl)
) {
return isTrustedDomain(currentUrlObj, appUrlObj, "", false);
} catch {
return false;
}
})();
if (!ignoreDomainWarning && ui.warningsEnabled && !isTrusted) {
return ( return (
<BaseLayout> <BaseLayout>
<DomainWarning <DomainWarning
+4 -58
View File
@@ -9,27 +9,12 @@ type IuseRedirectUri = {
export const useRedirectUri = ( export const useRedirectUri = (
redirect_uri: string | undefined, redirect_uri: string | undefined,
cookieDomain: string, cookieDomain: string,
appUrl: string,
subdomainsEnabled: boolean,
): IuseRedirectUri => { ): IuseRedirectUri => {
let isValid = false; let isValid = false;
let isTrusted = false; let isTrusted = false;
let isAllowedProto = false; let isAllowedProto = false;
let isHttpsDowngrade = false; let isHttpsDowngrade = false;
let appUrlObj: URL;
try {
appUrlObj = new URL(appUrl);
} catch {
return {
valid: isValid,
trusted: isTrusted,
allowedProto: isAllowedProto,
httpsDowngrade: isHttpsDowngrade,
};
}
if (!redirect_uri) { if (!redirect_uri) {
return { return {
valid: isValid, valid: isValid,
@@ -54,7 +39,10 @@ export const useRedirectUri = (
isValid = true; isValid = true;
if (isTrustedDomain(url, appUrlObj, cookieDomain, subdomainsEnabled)) { if (
url.hostname == cookieDomain ||
url.hostname.endsWith(`.${cookieDomain}`)
) {
isTrusted = true; isTrusted = true;
} }
@@ -74,45 +62,3 @@ export const useRedirectUri = (
httpsDowngrade: isHttpsDowngrade, httpsDowngrade: isHttpsDowngrade,
}; };
}; };
// ported from internal/controller/oauth_controller.go
const getEffectivePort = (url: URL): string => {
if (url.port) {
return url.port;
}
if (url.protocol == "https:") {
return "443";
}
return "80";
};
export const isTrustedDomain = (
url: URL,
appUrl: URL,
cookieDomain: string,
subdomainsEnabled: boolean,
): boolean => {
if (url.protocol != appUrl.protocol) {
return false;
}
if (getEffectivePort(url) != getEffectivePort(appUrl)) {
return false;
}
if (url.hostname == appUrl.hostname) {
return true;
}
if (!subdomainsEnabled) {
return false;
}
if (url.hostname.endsWith("." + cookieDomain.toLowerCase())) {
return true;
}
return false;
};
+1 -7
View File
@@ -37,8 +37,6 @@ export const ContinuePage = () => {
const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri( const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
redirectUri, redirectUri,
app.cookieDomain, app.cookieDomain,
app.appUrl,
app.subdomainsEnabled,
); );
const urlHref = url?.href; const urlHref = url?.href;
@@ -110,11 +108,7 @@ export const ContinuePage = () => {
components={{ components={{
code: <code />, code: <code />,
}} }}
values={{ values={{ cookieDomain: app.cookieDomain }}
cookieDomain: app.subdomainsEnabled
? `.${app.cookieDomain}`
: app.cookieDomain,
}}
shouldUnescape={true} shouldUnescape={true}
/> />
</CardDescription> </CardDescription>
+1 -1
View File
@@ -24,7 +24,7 @@ const uiSchema = z.object({
const appSchema = z.object({ const appSchema = z.object({
appUrl: z.string(), appUrl: z.string(),
cookieDomain: z.string(), cookieDomain: z.string(),
subdomainsEnabled: z.boolean(), trustedDomains: z.array(z.string()),
}); });
export const appContextSchema = z.object({ export const appContextSchema = z.object({
+2 -2
View File
@@ -27,7 +27,7 @@ require (
golang.org/x/tools v0.46.0 golang.org/x/tools v0.46.0
k8s.io/apimachinery v0.36.2 k8s.io/apimachinery v0.36.2
k8s.io/client-go v0.36.2 k8s.io/client-go v0.36.2
modernc.org/sqlite v1.52.0 modernc.org/sqlite v1.53.0
tailscale.com v1.100.0 tailscale.com v1.100.0
) )
@@ -175,7 +175,7 @@ require (
k8s.io/klog/v2 v2.140.0 // indirect k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
modernc.org/libc v1.72.3 // indirect modernc.org/libc v1.73.4 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
rsc.io/qr v0.2.0 // indirect rsc.io/qr v0.2.0 // indirect
+10 -10
View File
@@ -571,20 +571,20 @@ k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hk
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ= modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A= modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU= modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs= modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -593,8 +593,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo= modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+29 -50
View File
@@ -56,6 +56,7 @@ type BootstrapApp struct {
router *gin.Engine router *gin.Engine
db *sql.DB db *sql.DB
ding *ding.Ding ding *ding.Ding
listeners []Listener
dig *dig.Container dig *dig.Container
} }
@@ -97,7 +98,8 @@ func (app *BootstrapApp) Setup() error {
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.Scheme + "://" + appUrl.Host
app.runtime.TrustedDomains = append(app.runtime.TrustedDomains, app.runtime.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 {
@@ -142,6 +144,15 @@ func (app *BootstrapApp) Setup() error {
provider.ClientSecret = secret provider.ClientSecret = secret
provider.ClientSecretFile = "" provider.ClientSecretFile = ""
if provider.RedirectURL == "" {
provider.RedirectURL = app.runtime.AppURL + "/api/oauth/callback/" + id
}
app.runtime.OAuthProviders[id] = provider
}
// set presets for built-in providers
for id, provider := range app.runtime.OAuthProviders {
if provider.Name == "" { if provider.Name == "" {
if name, ok := model.OverrideProviders[id]; ok { if name, ok := model.OverrideProviders[id]; ok {
provider.Name = name provider.Name = name
@@ -149,16 +160,18 @@ func (app *BootstrapApp) Setup() error {
provider.Name = utils.Capitalize(id) provider.Name = utils.Capitalize(id)
} }
} }
app.runtime.OAuthProviders[id] = provider app.runtime.OAuthProviders[id] = provider
} }
// cookie domain // cookie domain
cookieDomainResolver := utils.GetCookieDomain
if !app.config.Auth.SubdomainsEnabled { if !app.config.Auth.SubdomainsEnabled {
app.log.App.Warn().Msg("Subdomains are disabled, cookies will be set for the current domain only") app.log.App.Warn().Msg("Subdomains are disabled, using standalone cookie domain resolver which will not work with subdomains")
cookieDomainResolver = utils.GetStandaloneCookieDomain
} }
cookieDomain, err := utils.GetCookieDomain(app.runtime.AppURL, app.config.Auth.SubdomainsEnabled) cookieDomain, err := cookieDomainResolver(app.runtime.AppURL)
if err != nil { if err != nil {
return fmt.Errorf("failed to get cookie domain: %w", err) return fmt.Errorf("failed to get cookie domain: %w", err)
@@ -273,43 +286,9 @@ func (app *BootstrapApp) Setup() error {
app.runtime.ConfiguredProviders = configuredProviders app.runtime.ConfiguredProviders = configuredProviders
// if tailscale is enabled and listening, replace the app url with the tailscale hostname // throw in tailscale if it's configured just before setting up the controllers
if app.services.tailscaleService != nil && app.config.Tailscale.Listen { if app.services.tailscaleService != nil {
tailscaleUrl := "https://" + app.services.tailscaleService.GetHostname() app.runtime.TrustedDomains = append(app.runtime.TrustedDomains, "https://"+app.services.tailscaleService.GetHostname())
// if the tailscale url is different from the app url, replace it
if tailscaleUrl != app.runtime.AppURL {
app.log.App.Info().Msg("Listening on tailscale, replacing app url with tailscale hostname")
app.runtime.AppURL = tailscaleUrl
// also update cookie domain
cookieDomain, err := utils.GetCookieDomain(tailscaleUrl, app.config.Auth.SubdomainsEnabled)
if err != nil {
return fmt.Errorf("failed to get cookie domain: %w", err)
}
app.runtime.CookieDomain = cookieDomain
}
}
// force an update of the redirect urls for all oauth providers, if they are empty
services := app.services.oauthBrokerService.GetConfiguredServices()
for _, service := range services {
oauthService, ok := app.services.oauthBrokerService.GetService(service)
if !ok {
return fmt.Errorf("failed to get oauth service for provider %s", service)
}
providerConfig := oauthService.GetConfig()
if providerConfig.RedirectURL == "" {
providerConfig.RedirectURL = app.runtime.AppURL + "/api/oauth/callback/" + service
oauthService.UpdateConfig(providerConfig)
}
} }
// setup router // setup router
@@ -329,19 +308,19 @@ func (app *BootstrapApp) Setup() error {
app.ding.Go(app.heartbeatRoutine, ding.RingMinor) app.ding.Go(app.heartbeatRoutine, ding.RingMinor)
} }
// get listener // setup listeners
listenerFunc, err := app.getListenerFunc() app.listeners = app.calculateListenerPolicy()
if err != nil { if app.config.Server.ConcurrentListenersEnabled {
return fmt.Errorf("failed to get listener function: %w", err) app.log.App.Info().Msg("Concurrent listeners enabled, will run on all available listeners")
} }
// run listener // run listeners
lec := make(chan error, 1) lec, err := app.runListeners()
app.ding.Go(func(ctx context.Context) { if err != nil {
lec <- listenerFunc(ctx) return fmt.Errorf("failed to run listeners: %w", err)
}, ding.RingNormal) }
// monitor cancellation and server errors // monitor cancellation and server errors
for { for {
+70 -11
View File
@@ -9,6 +9,7 @@ import (
"os" "os"
"time" "time"
"github.com/steveiliop56/ding"
"github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/controller"
"github.com/tinyauthapp/tinyauth/internal/middleware" "github.com/tinyauthapp/tinyauth/internal/middleware"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
@@ -17,6 +18,14 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type Listener int
const (
ListenerHTTP Listener = iota
ListenerUnix
ListenerTailscale
)
func (app *BootstrapApp) setupRouter() error { func (app *BootstrapApp) setupRouter() error {
// we don't want gin debug mode // we don't want gin debug mode
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
@@ -125,29 +134,79 @@ func (app *BootstrapApp) setupRouter() error {
return nil return nil
} }
// Top down func (app *BootstrapApp) runListeners() (chan error, error) {
// 1. Tailscale (if tailscale.listen) // lec -> listener error channel
// 2. Unix socket (if server.socketPath) lec := make(chan error, len(app.listeners))
// 3. HTTP - default
func (app *BootstrapApp) getListenerFunc() (func(ctx context.Context) error, error) { for _, listenerType := range app.listeners {
if app.config.Tailscale.Listen { listenerFunc, err := app.listenerFromType(listenerType)
if app.services.tailscaleService == nil {
return nil, fmt.Errorf("tailscale.listen is enabled but tailscale service is not initialized") if err != nil {
return nil, fmt.Errorf("failed to get listener function: %w", err)
} }
return app.serveTailscale, nil
app.ding.Go(func(ctx context.Context) {
lec <- listenerFunc(ctx)
}, ding.RingNormal)
}
return lec, nil
}
// The way we calculate listeners is as follows:
// If concurrent listeners are disabled, we pick the first available listener, so:
// 1. If tailscale is enabled, we use tailscale
// 2. If socket path is configured, we use unix socket
// 3. Finally if none is configured we use http
// If concurrent listeners are enabled, we add all available listeners in the following order
func (app *BootstrapApp) calculateListenerPolicy() []Listener {
l := []Listener{}
if !app.config.Server.ConcurrentListenersEnabled {
if app.services.tailscaleService != nil {
l = append(l, ListenerTailscale)
return l
} }
if app.config.Server.SocketPath != "" { if app.config.Server.SocketPath != "" {
return app.serveUnix, nil l = append(l, ListenerUnix)
return l
} }
l = append(l, ListenerHTTP)
return l
}
if app.config.Server.SocketPath != "" {
l = append(l, ListenerUnix)
}
if app.services.tailscaleService != nil {
l = append(l, ListenerTailscale)
}
l = append(l, ListenerHTTP)
return l
}
func (app *BootstrapApp) listenerFromType(listenerType Listener) (func(ctx context.Context) error, error) {
switch listenerType {
case ListenerHTTP:
return app.serveHTTP, nil return app.serveHTTP, nil
case ListenerUnix:
return app.serveUnix, nil
case ListenerTailscale:
return app.serveTailscale, nil
default:
return nil, fmt.Errorf("invalid listener type: %d", listenerType)
}
} }
func (app *BootstrapApp) serveHTTP(ctx context.Context) error { func (app *BootstrapApp) serveHTTP(ctx context.Context) error {
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port) address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
app.log.App.Info().Msgf("Starting server on http://%s", address) app.log.App.Info().Msgf("Starting server on %s", address)
listener, err := net.Listen("tcp", address) listener, err := net.Listen("tcp", address)
+2 -6
View File
@@ -1,8 +1,6 @@
package controller package controller
import ( import (
"errors"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig" "go.uber.org/dig"
@@ -62,7 +60,7 @@ type ACRUI struct {
type ACRApp struct { type ACRApp struct {
AppURL string `json:"appUrl"` AppURL string `json:"appUrl"`
CookieDomain string `json:"cookieDomain"` CookieDomain string `json:"cookieDomain"`
SubdomainsEnabled bool `json:"subdomainsEnabled"` TrustedDomains []string `json:"trustedDomains"`
} }
type AppContextResponse struct { type AppContextResponse struct {
@@ -111,9 +109,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
context, err := new(model.UserContext).NewFromGin(c) context, err := new(model.UserContext).NewFromGin(c)
if err != nil { if err != nil {
if !errors.Is(err, model.ErrUserContextNotFound) {
controller.log.App.Error().Err(err).Msg("Failed to create user context from request") controller.log.App.Error().Err(err).Msg("Failed to create user context from request")
}
c.JSON(200, UserContextResponse{ c.JSON(200, UserContextResponse{
Status: 401, Status: 401,
Message: "Unauthorized", Message: "Unauthorized",
@@ -166,7 +162,7 @@ func (controller *ContextController) appContextHandler(c *gin.Context) {
App: ACRApp{ App: ACRApp{
AppURL: controller.runtime.AppURL, AppURL: controller.runtime.AppURL,
CookieDomain: controller.runtime.CookieDomain, CookieDomain: controller.runtime.CookieDomain,
SubdomainsEnabled: controller.config.Auth.SubdomainsEnabled, TrustedDomains: controller.runtime.TrustedDomains,
}, },
}) })
} }
@@ -50,7 +50,7 @@ func TestContextController(t *testing.T) {
App: ACRApp{ App: ACRApp{
AppURL: runtime.AppURL, AppURL: runtime.AppURL,
CookieDomain: runtime.CookieDomain, CookieDomain: runtime.CookieDomain,
SubdomainsEnabled: cfg.Auth.SubdomainsEnabled, TrustedDomains: runtime.TrustedDomains,
}, },
} }
bytes, err := json.Marshal(expectedAppContextResponse) bytes, err := json.Marshal(expectedAppContextResponse)
+36 -37
View File
@@ -12,6 +12,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
"github.com/weppos/publicsuffix-go/publicsuffix"
"go.uber.org/dig" "go.uber.org/dig"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -304,8 +305,8 @@ func (controller *OAuthController) isOidcRequest(params service.OAuthCallbackPar
} }
func (controller *OAuthController) getCookieDomain() string { func (controller *OAuthController) getCookieDomain() string {
if !controller.config.Auth.SubdomainsEnabled { if controller.config.Auth.SubdomainsEnabled {
return "" return "." + controller.runtime.CookieDomain
} }
return controller.runtime.CookieDomain return controller.runtime.CookieDomain
} }
@@ -313,54 +314,52 @@ func (controller *OAuthController) getCookieDomain() string {
func (controller *OAuthController) isRedirectSafe(redirectURI string) bool { func (controller *OAuthController) isRedirectSafe(redirectURI string) bool {
u, err := url.Parse(redirectURI) u, err := url.Parse(redirectURI)
if err != nil || u.Host == "" || u.Scheme == "" {
return false
}
for _, allowed := range controller.runtime.TrustedDomains {
tu, err := url.Parse(allowed)
if err != nil { if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to parse redirect URI") controller.log.App.Error().Err(err).Str("allowed", allowed).Msg("Failed to parse trusted domain")
return false continue
} }
if u.Scheme == "" || u.Host == "" { if tu.Scheme != u.Scheme {
controller.log.App.Warn().Msg("Redirect URI has invalid scheme or host") continue
return false
} }
au, err := url.Parse(controller.runtime.AppURL) // exact match
if strings.EqualFold(u.Host, tu.Host) {
if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to parse app URL")
return false
}
if u.Scheme != au.Scheme {
controller.log.App.Warn().Msg("Redirect URI scheme does not match app URL scheme")
return false
}
getEffectivePort := func(u *url.URL) string {
if u.Port() != "" {
return u.Port()
}
if u.Scheme == "https" {
return "443"
}
return "80"
}
if getEffectivePort(u) != getEffectivePort(au) {
controller.log.App.Warn().Msg("Redirect URI port does not match app URL port")
return false
}
if strings.EqualFold(u.Hostname(), au.Hostname()) {
return true return true
} }
// if subdomains are disabled, end here
if !controller.config.Auth.SubdomainsEnabled { if !controller.config.Auth.SubdomainsEnabled {
return false continue
} }
if strings.HasSuffix(strings.ToLower(u.Hostname()), "."+strings.ToLower(controller.runtime.CookieDomain)) { // get the root domain (e.g. tinyauth.example.com -> example.com or
// tinyauth.sub.example.com -> sub.example.com)
_, root, ok := strings.Cut(tu.Host, ".")
if !ok {
continue
}
root = strings.ToLower(root)
// check if the root domain is in the psl
_, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, root, nil)
if err != nil {
continue
}
// subdomain match
if strings.HasSuffix(strings.ToLower(u.Host), "."+root) {
return true return true
} }
}
return false return false
} }
+100 -126
View File
@@ -9,7 +9,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
) )
func TestOAuthControllerIsRedirectSafe(t *testing.T) { func TestOAuthController(t *testing.T) {
log := logger.NewLogger().WithTestConfig() log := logger.NewLogger().WithTestConfig()
log.Init() log.Init()
@@ -17,171 +17,145 @@ func TestOAuthControllerIsRedirectSafe(t *testing.T) {
type testCase struct { type testCase struct {
description string description string
appURL string run func(ctrl *OAuthController)
cookieDomain string trustedDomains []string
subdomainsEnabled bool subdomainsEnabled bool
redirectURI string
expected bool
} }
tests := []testCase{ tests := []testCase{
{ {
description: "Exact host match returns true", description: "Test exact match of redirect URI",
appURL: "https://tinyauth.example.com", trustedDomains: []string{"https://tinyauth.example.com"},
cookieDomain: "example.com",
subdomainsEnabled: true, subdomainsEnabled: true,
redirectURI: "https://tinyauth.example.com", run: func(ctrl *OAuthController) {
expected: true, redirectUri := "https://tinyauth.example.com"
assert.True(t, ctrl.isRedirectSafe(redirectUri))
},
}, },
{ {
description: "Exact host match is case insensitive", description: "Test subdomain match of redirect URI",
appURL: "https://tinyauth.example.com", trustedDomains: []string{"https://tinyauth.example.com"},
cookieDomain: "example.com",
subdomainsEnabled: true, subdomainsEnabled: true,
redirectURI: "https://TinyAuth.Example.com", run: func(ctrl *OAuthController) {
expected: true, redirectUri := "https://sub.example.com"
assert.True(t, ctrl.isRedirectSafe(redirectUri))
},
}, },
{ {
description: "Exact host match with subdomains disabled returns true", description: "Test different trusted domain",
appURL: "https://tinyauth.example.com", trustedDomains: []string{"https://tinyauth.example.com", "https://tinyauth.foo.com"},
cookieDomain: "example.com", subdomainsEnabled: true,
run: func(ctrl *OAuthController) {
redirectUri := "https://app.foo.com"
assert.True(t, ctrl.isRedirectSafe(redirectUri))
},
},
{
description: "Test invalid redirect URI",
run: func(ctrl *OAuthController) {
redirectUri := "https:/malicious"
assert.False(t, ctrl.isRedirectSafe(redirectUri))
},
},
{
description: "Test empty redirect URI",
run: func(ctrl *OAuthController) {
redirectUri := ""
assert.False(t, ctrl.isRedirectSafe(redirectUri))
},
},
{
description: "Test redirect URI with different scheme",
trustedDomains: []string{"https://tinyauth.example.com"},
subdomainsEnabled: true,
run: func(ctrl *OAuthController) {
redirectUri := "http://tinyauth.example.com"
assert.False(t, ctrl.isRedirectSafe(redirectUri))
},
},
{
description: "Test redirect URI with different port",
trustedDomains: []string{"https://tinyauth.example.com"},
subdomainsEnabled: true,
run: func(ctrl *OAuthController) {
redirectUri := "https://tinyauth.example.com:8080"
assert.False(t, ctrl.isRedirectSafe(redirectUri))
},
},
{
// weird case, subdomains enabled and domain without subdomain can't happen
description: "Test with trusted domain that's in PSL when split",
trustedDomains: []string{"https://example.com"}, // will become .com which we
// obviously don't want to allow
subdomainsEnabled: true,
run: func(ctrl *OAuthController) {
redirectUri := "https://sub.example.com"
assert.False(t, ctrl.isRedirectSafe(redirectUri))
},
},
{
description: "Test subdomain redirect URI when subdomains are disabled",
trustedDomains: []string{"https://tinyauth.example.com"},
subdomainsEnabled: false, subdomainsEnabled: false,
redirectURI: "https://tinyauth.example.com", run: func(ctrl *OAuthController) {
expected: true, redirectUri := "https://sub.tinyauth.example.com"
assert.False(t, ctrl.isRedirectSafe(redirectUri))
},
}, },
{ {
description: "Subdomain of cookie domain returns true when subdomains enabled", description: "Test domain like the .co.uk",
appURL: "https://tinyauth.example.com", trustedDomains: []string{"https://example.co.uk"},
cookieDomain: "example.com",
subdomainsEnabled: true, subdomainsEnabled: true,
redirectURI: "https://sub.example.com", run: func(ctrl *OAuthController) {
expected: true, redirectUri := "https://sub.example.co.uk"
assert.False(t, ctrl.isRedirectSafe(redirectUri))
},
}, },
{ {
description: "Subdomain of cookie domain is case insensitive", description: "Test domain like the .co.uk with subdomains disabled",
appURL: "https://tinyauth.example.com", trustedDomains: []string{"https://example.co.uk"},
cookieDomain: "Example.COM",
subdomainsEnabled: true,
redirectURI: "https://SUB.example.com",
expected: true,
},
{
description: "Subdomain not matching cookie domain returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "https://sub.evil.com",
expected: false,
},
{
description: "Subdomain returns false when subdomains disabled",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: false, subdomainsEnabled: false,
redirectURI: "https://sub.example.com", run: func(ctrl *OAuthController) {
expected: false, redirectUri := "https://example.co.uk"
assert.True(t, ctrl.isRedirectSafe(redirectUri))
},
}, },
{ {
description: "Cookie domain itself is not a subdomain match", description: "Test caps domain",
appURL: "https://tinyauth.example.com", trustedDomains: []string{"https://TINYAUTH.ExAmpLe.com"},
cookieDomain: "example.com",
subdomainsEnabled: true, subdomainsEnabled: true,
redirectURI: "https://example.com", run: func(ctrl *OAuthController) {
expected: false, redirectUri := "https://sUb.ExAmPle.com"
assert.True(t, ctrl.isRedirectSafe(redirectUri))
},
}, },
{ {
description: "Different scheme returns false", description: "Test edge case with @",
appURL: "https://tinyauth.example.com", trustedDomains: []string{"https://tinyauth.example.com"},
cookieDomain: "example.com",
subdomainsEnabled: true, subdomainsEnabled: true,
redirectURI: "http://tinyauth.example.com", run: func(ctrl *OAuthController) {
expected: false, redirectUri := "https://malicious.example.com@evil.com"
assert.False(t, ctrl.isRedirectSafe(redirectUri))
}, },
{
description: "Different port returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "https://tinyauth.example.com:8080",
expected: false,
},
{
description: "Empty redirect URI returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "",
expected: false,
},
{
description: "Redirect URI without host returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "https:/malicious",
expected: false,
},
{
description: "Redirect URI without scheme returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "tinyauth.example.com",
expected: false,
},
{
description: "Relative redirect URI returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "/some/path",
expected: false,
},
{
description: "Userinfo trick with malicious host returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "https://malicious.example.com@evil.com",
expected: false,
},
{
description: "Unparseable redirect URI returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "https://exa\x7fmple.com",
expected: false,
},
{
description: "Unparseable app URL returns false",
appURL: "https://tinyauth.\x7fexample.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "https://tinyauth.example.com",
expected: false,
}, },
} }
// TODO: add auth service
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.description, func(t *testing.T) { t.Run(tc.description, func(t *testing.T) {
router := gin.Default() router := gin.Default()
group := router.Group("/api") group := router.Group("/api")
gin.SetMode(gin.TestMode) gin.SetMode(gin.TestMode)
// overwrite the trusted domains and subdomain setting for each test case
// Overwrite the app URL, cookie domain and subdomain setting for each test case runtime.TrustedDomains = tc.trustedDomains
runtime.AppURL = tc.appURL
runtime.CookieDomain = tc.cookieDomain
cfg.Auth.SubdomainsEnabled = tc.subdomainsEnabled cfg.Auth.SubdomainsEnabled = tc.subdomainsEnabled
ctrl := NewOAuthController(OAuthControllerInput{ ctrl := NewOAuthController(OAuthControllerInput{
Log: log, Log: log,
Config: &cfg, Config: &cfg,
RuntimeConfig: &runtime, RuntimeConfig: &runtime,
RouterGroup: group, RouterGroup: group,
}) })
tc.run(ctrl)
assert.Equal(t, tc.expected, ctrl.isRedirectSafe(tc.redirectURI))
}) })
} }
} }
-16
View File
@@ -295,14 +295,6 @@ func (controller *UserController) totpHandler(c *gin.Context) {
context, err := new(model.UserContext).NewFromGin(c) context, err := new(model.UserContext).NewFromGin(c)
if err != nil { if err != nil {
if errors.Is(err, model.ErrUserContextNotFound) {
controller.log.App.Warn().Msg("TOTP verification attempt without user context")
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
})
return
}
controller.log.App.Error().Err(err).Msg("Failed to create user context from request for TOTP verification") controller.log.App.Error().Err(err).Msg("Failed to create user context from request for TOTP verification")
c.JSON(500, gin.H{ c.JSON(500, gin.H{
"status": 500, "status": 500,
@@ -413,14 +405,6 @@ func (controller *UserController) tailscaleHandler(c *gin.Context) {
context, err := new(model.UserContext).NewFromGin(c) context, err := new(model.UserContext).NewFromGin(c)
if err != nil { if err != nil {
if errors.Is(err, model.ErrUserContextNotFound) {
controller.log.App.Warn().Msg("Tailscale login attempt without user context")
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
})
return
}
controller.log.App.Error().Err(err).Msg("Failed to create user context from request") controller.log.App.Error().Err(err).Msg("Failed to create user context from request")
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
+2 -2
View File
@@ -17,6 +17,7 @@ func NewDefaultConfiguration() *Config {
Server: ServerConfig{ Server: ServerConfig{
Port: 3000, Port: 3000,
Address: "0.0.0.0", Address: "0.0.0.0",
ConcurrentListenersEnabled: false,
}, },
Auth: AuthConfig{ Auth: AuthConfig{
SubdomainsEnabled: true, SubdomainsEnabled: true,
@@ -106,6 +107,7 @@ type ServerConfig struct {
Port int `description:"The port on which the server listens." yaml:"port"` Port int `description:"The port on which the server listens." yaml:"port"`
Address string `description:"The address on which the server listens." yaml:"address"` Address string `description:"The address on which the server listens." yaml:"address"`
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"` SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"`
ConcurrentListenersEnabled bool `description:"Enable listening on both TCP and Unix socket at the same time." yaml:"concurrentListenersEnabled"`
} }
type AuthConfig struct { type AuthConfig struct {
@@ -216,8 +218,6 @@ type TailscaleConfig struct {
Hostname string `description:"Tailscale hostname." yaml:"hostname"` Hostname string `description:"Tailscale hostname." yaml:"hostname"`
AuthKey string `description:"Tailscale auth key." yaml:"authKey"` AuthKey string `description:"Tailscale auth key." yaml:"authKey"`
Ephemeral bool `description:"Use ephemeral Tailscale node." yaml:"ephemeral"` Ephemeral bool `description:"Use ephemeral Tailscale node." yaml:"ephemeral"`
Funnel bool `description:"Enable Tailscale Funnel." yaml:"funnel"`
Listen bool `description:"Listen on the Tailscale address instead of standard address." yaml:"listen"`
} }
// OAuth/OIDC config // OAuth/OIDC config
+1
View File
@@ -12,6 +12,7 @@ type RuntimeConfig struct {
OAuthProviders map[string]OAuthServiceConfig OAuthProviders map[string]OAuthServiceConfig
OAuthWhitelist []string OAuthWhitelist []string
ConfiguredProviders []Provider ConfiguredProviders []Provider
TrustedDomains []string
} }
type Provider struct { type Provider struct {
+32 -17
View File
@@ -46,7 +46,7 @@ type OAuthPendingSession struct {
State string State string
Verifier string Verifier string
Token *oauth2.Token Token *oauth2.Token
Service IOAuthService Service *OAuthServiceImpl
ExpiresAt time.Time ExpiresAt time.Time
CallbackParams OAuthCallbackParams CallbackParams OAuthCallbackParams
} }
@@ -380,11 +380,33 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess
return nil, fmt.Errorf("failed to create session entry: %w", err) return nil, fmt.Errorf("failed to create session entry: %w", err)
} }
if data.Provider == "tailscale" {
auth.log.App.Trace().Str("url", fmt.Sprintf("https://%s", auth.tailscale.GetHostname())).Msg("Extracting root domain from Tailscale hostname")
tsCookieDomain, err := utils.GetCookieDomain(fmt.Sprintf("https://%s", auth.tailscale.GetHostname()))
if err != nil {
return nil, fmt.Errorf("failed to get cookie domain for tailscale user: %w", err)
}
return &http.Cookie{ return &http.Cookie{
Name: auth.runtime.SessionCookieName, Name: auth.runtime.SessionCookieName,
Value: session.UUID, Value: session.UUID,
Path: "/", Path: "/",
Domain: auth.getCookieDomain(), Domain: fmt.Sprintf(".%s", tsCookieDomain),
Expires: expiresAt,
MaxAge: int(time.Until(expiresAt).Seconds()),
Secure: auth.config.Auth.SecureCookie,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}, nil
}
return &http.Cookie{
Name: auth.runtime.SessionCookieName,
Value: session.UUID,
Path: "/",
Domain: fmt.Sprintf(".%s", auth.runtime.CookieDomain),
Expires: expiresAt, Expires: expiresAt,
MaxAge: int(time.Until(expiresAt).Seconds()), MaxAge: int(time.Until(expiresAt).Seconds()),
Secure: auth.config.Auth.SecureCookie, Secure: auth.config.Auth.SecureCookie,
@@ -437,7 +459,7 @@ func (auth *AuthService) RefreshSession(ctx context.Context, uuid string) (*http
Name: auth.runtime.SessionCookieName, Name: auth.runtime.SessionCookieName,
Value: session.UUID, Value: session.UUID,
Path: "/", Path: "/",
Domain: auth.getCookieDomain(), Domain: fmt.Sprintf(".%s", auth.runtime.CookieDomain),
Expires: time.Now().Add(time.Duration(newExpiry-currentTime) * time.Second), Expires: time.Now().Add(time.Duration(newExpiry-currentTime) * time.Second),
MaxAge: int(newExpiry - currentTime), MaxAge: int(newExpiry - currentTime),
Secure: auth.config.Auth.SecureCookie, Secure: auth.config.Auth.SecureCookie,
@@ -458,7 +480,7 @@ func (auth *AuthService) DeleteSession(ctx context.Context, uuid string) (*http.
Name: auth.runtime.SessionCookieName, Name: auth.runtime.SessionCookieName,
Value: "", Value: "",
Path: "/", Path: "/",
Domain: auth.getCookieDomain(), Domain: fmt.Sprintf(".%s", auth.runtime.CookieDomain),
Expires: time.Now(), Expires: time.Now(),
MaxAge: -1, MaxAge: -1,
Secure: auth.config.Auth.SecureCookie, Secure: auth.config.Auth.SecureCookie,
@@ -527,7 +549,7 @@ func (auth *AuthService) NewOAuthSession(serviceName string, params OAuthCallbac
session := OAuthPendingSession{ session := OAuthPendingSession{
State: state, State: state,
Verifier: verifier, Verifier: verifier,
Service: service, Service: &service,
ExpiresAt: time.Now().Add(1 * time.Hour), ExpiresAt: time.Now().Add(1 * time.Hour),
CallbackParams: params, CallbackParams: params,
} }
@@ -544,7 +566,7 @@ func (auth *AuthService) GetOAuthURL(sessionId string) (string, error) {
return "", err return "", err
} }
return session.Service.GetAuthURL(session.State, session.Verifier), nil return (*session.Service).GetAuthURL(session.State, session.Verifier), nil
} }
func (auth *AuthService) GetOAuthToken(sessionId string, code string) (*oauth2.Token, error) { func (auth *AuthService) GetOAuthToken(sessionId string, code string) (*oauth2.Token, error) {
@@ -554,7 +576,7 @@ func (auth *AuthService) GetOAuthToken(sessionId string, code string) (*oauth2.T
return nil, fmt.Errorf("oauth session not found: %s", sessionId) return nil, fmt.Errorf("oauth session not found: %s", sessionId)
} }
token, err := session.Service.GetToken(code, session.Verifier) token, err := (*session.Service).GetToken(code, session.Verifier)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to exchange code for token: %w", err) return nil, fmt.Errorf("failed to exchange code for token: %w", err)
@@ -583,7 +605,7 @@ func (auth *AuthService) GetOAuthUserinfo(sessionId string) (*model.Claims, erro
return nil, fmt.Errorf("oauth token not found for session: %s", sessionId) return nil, fmt.Errorf("oauth token not found for session: %s", sessionId)
} }
userinfo, err := session.Service.GetUserinfo(session.Token) userinfo, err := (*session.Service).GetUserinfo(session.Token)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get userinfo: %w", err) return nil, fmt.Errorf("failed to get userinfo: %w", err)
@@ -592,14 +614,14 @@ func (auth *AuthService) GetOAuthUserinfo(sessionId string) (*model.Claims, erro
return userinfo, nil return userinfo, nil
} }
func (auth *AuthService) GetOAuthService(sessionId string) (IOAuthService, error) { func (auth *AuthService) GetOAuthService(sessionId string) (OAuthServiceImpl, error) {
session, err := auth.GetOAuthPendingSession(sessionId) session, err := auth.GetOAuthPendingSession(sessionId)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return session.Service, nil return *session.Service, nil
} }
func (auth *AuthService) EndOAuthSession(sessionId string) { func (auth *AuthService) EndOAuthSession(sessionId string) {
@@ -704,10 +726,3 @@ func (auth *AuthService) calculateLockdownLimit() int {
return limit return limit
} }
func (auth *AuthService) getCookieDomain() string {
if !auth.config.Auth.SubdomainsEnabled {
return ""
}
return auth.runtime.CookieDomain
}
+6 -8
View File
@@ -12,21 +12,19 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
type IOAuthService interface { type OAuthServiceImpl interface {
Name() string Name() string
ID() string ID() string
NewRandom() string NewRandom() string
GetAuthURL(state, verifier string) string GetAuthURL(state string, verifier string) string
GetToken(code, verifier string) (*oauth2.Token, error) GetToken(code string, verifier string) (*oauth2.Token, error)
GetUserinfo(token *oauth2.Token) (*model.Claims, error) GetUserinfo(token *oauth2.Token) (*model.Claims, error)
GetConfig() model.OAuthServiceConfig
UpdateConfig(config model.OAuthServiceConfig)
} }
type OAuthBrokerService struct { type OAuthBrokerService struct {
log *logger.Logger log *logger.Logger
services map[string]IOAuthService services map[string]OAuthServiceImpl
configs map[string]model.OAuthServiceConfig configs map[string]model.OAuthServiceConfig
} }
@@ -46,7 +44,7 @@ type OAuthBrokerServiceInput struct {
func NewOAuthBrokerService(i OAuthBrokerServiceInput) *OAuthBrokerService { func NewOAuthBrokerService(i OAuthBrokerServiceInput) *OAuthBrokerService {
service := &OAuthBrokerService{ service := &OAuthBrokerService{
log: i.Log, log: i.Log,
services: make(map[string]IOAuthService), services: make(map[string]OAuthServiceImpl),
configs: i.Runtime.OAuthProviders, configs: i.Runtime.OAuthProviders,
} }
@@ -72,7 +70,7 @@ func (broker *OAuthBrokerService) GetConfiguredServices() []string {
return services return services
} }
func (broker *OAuthBrokerService) GetService(name string) (IOAuthService, bool) { func (broker *OAuthBrokerService) GetService(name string) (OAuthServiceImpl, bool) {
service, exists := broker.services[name] service, exists := broker.services[name]
return service, exists return service, exists
} }
+1 -15
View File
@@ -70,7 +70,7 @@ func (s *OAuthService) NewRandom() string {
return random return random
} }
func (s *OAuthService) GetAuthURL(state, verifier string) string { func (s *OAuthService) GetAuthURL(state string, verifier string) string {
return s.config.AuthCodeURL(state, oauth2.AccessTypeOnline, oauth2.S256ChallengeOption(verifier)) return s.config.AuthCodeURL(state, oauth2.AccessTypeOnline, oauth2.S256ChallengeOption(verifier))
} }
@@ -82,17 +82,3 @@ func (s *OAuthService) GetUserinfo(token *oauth2.Token) (*model.Claims, error) {
client := oauth2.NewClient(s.ctx, oauth2.StaticTokenSource(token)) client := oauth2.NewClient(s.ctx, oauth2.StaticTokenSource(token))
return s.userinfoExtractor(client, s.serviceCfg.UserinfoURL) return s.userinfoExtractor(client, s.serviceCfg.UserinfoURL)
} }
func (s *OAuthService) GetConfig() model.OAuthServiceConfig {
return s.serviceCfg
}
func (s *OAuthService) UpdateConfig(config model.OAuthServiceConfig) {
s.serviceCfg = config
s.config.ClientID = config.ClientID
s.config.ClientSecret = config.ClientSecret
s.config.Scopes = config.Scopes
s.config.Endpoint.AuthURL = config.AuthURL
s.config.Endpoint.TokenURL = config.TokenURL
s.config.RedirectURL = config.RedirectURL
}
-14
View File
@@ -94,10 +94,6 @@ func NewTailscaleService(i TailscaleServiceInput) (*TailscaleService, error) {
i.Ding.Go(service.watchAndClose, ding.RingMajor) i.Ding.Go(service.watchAndClose, ding.RingMajor)
if i.Config.Tailscale.Funnel && !i.Config.Tailscale.Listen {
service.log.App.Warn().Msg("Tailscale Funnel is enabled but listen is disabled. Funnel will not work without listen enabled.")
}
return service, nil return service, nil
} }
@@ -152,16 +148,6 @@ func (ts *TailscaleService) CreateListener() (net.Listener, error) {
if ts.ln != nil { if ts.ln != nil {
return *ts.ln, nil return *ts.ln, nil
} }
if ts.config.Tailscale.Funnel {
ln, err := ts.srv.ListenFunnel("tcp", ":443")
if err != nil {
return nil, err
}
ts.ln = &ln
return ln, nil
}
ln, err := ts.srv.ListenTLS("tcp", ":443") ln, err := ts.srv.ListenTLS("tcp", ":443")
if err != nil { if err != nil {
return nil, err return nil, err
+4 -1
View File
@@ -43,7 +43,6 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
ACLs: model.ACLsConfig{ ACLs: model.ACLsConfig{
Policy: "allow", Policy: "allow",
}, },
SubdomainsEnabled: true,
}, },
Database: model.DatabaseConfig{ Database: model.DatabaseConfig{
Path: filepath.Join(tempDir, "test.db"), Path: filepath.Join(tempDir, "test.db"),
@@ -166,6 +165,10 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
CookieDomain: "example.com", CookieDomain: "example.com",
AppURL: "https://tinyauth.example.com", AppURL: "https://tinyauth.example.com",
SessionCookieName: "tinyauth-session", SessionCookieName: "tinyauth-session",
TrustedDomains: []string{
"https://tinyauth.example.com",
"https://tinyauth.foo.com",
},
} }
return config, runtime return config, runtime
+35 -23
View File
@@ -1,7 +1,7 @@
package utils package utils
import ( import (
"fmt" "errors"
"net" "net"
"net/url" "net/url"
"strings" "strings"
@@ -9,36 +9,27 @@ import (
"github.com/weppos/publicsuffix-go/publicsuffix" "github.com/weppos/publicsuffix-go/publicsuffix"
) )
// GetCookieDomain parses the app url and returns the domain value to use for cookies. // Get cookie domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com)
// When auth for subdomains is enabled, it strips the leftmost label func GetCookieDomain(u string) (string, error) {
// (e.g. sub1.sub2.domain.com -> sub2.domain.com), otherwise it returns the full hostname. parsed, err := url.Parse(u)
func GetCookieDomain(appUrl string, subdomainsEnabled bool) (string, error) {
u, err := url.Parse(appUrl)
if err != nil { if err != nil {
return "", fmt.Errorf("invalid app url: %w", err) return "", err
} }
hostname := strings.ToLower(u.Hostname()) host := parsed.Hostname()
if netIP := net.ParseIP(hostname); netIP != nil { if netIP := net.ParseIP(host); netIP != nil {
return "", fmt.Errorf("ip addresses not allowed") return "", errors.New("ip addresses not allowed")
} }
parts := strings.Split(hostname, ".") parts := strings.Split(host, ".")
if len(parts) < 2 { if len(parts) == 2 {
return "", fmt.Errorf("invalid app url, must be in format subdomain.domain.tld or domain.tld") return host, nil
} }
if !subdomainsEnabled || len(parts) == 2 { if len(parts) < 3 {
_, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, hostname, nil) return "", errors.New("invalid app url, must be at least second level domain")
if err != nil {
return "", fmt.Errorf("domain in public suffix list, cannot set cookies: %w", err)
}
return hostname, nil
} }
domain := strings.Join(parts[1:], ".") domain := strings.Join(parts[1:], ".")
@@ -46,12 +37,33 @@ func GetCookieDomain(appUrl string, subdomainsEnabled bool) (string, error) {
_, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, domain, nil) _, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, domain, nil)
if err != nil { if err != nil {
return "", fmt.Errorf("domain in public suffix list, cannot set cookies: %w", err) return "", errors.New("domain in public suffix list, cannot set cookies")
} }
return domain, nil return domain, nil
} }
func GetStandaloneCookieDomain(u string) (string, error) {
parsed, err := url.Parse(u)
if err != nil {
return "", err
}
host := parsed.Hostname()
if netIP := net.ParseIP(host); netIP != nil {
return "", errors.New("ip addresses not allowed")
}
parts := strings.Split(host, ".")
if len(parts) < 2 {
return "", errors.New("invalid app url")
}
return host, nil
}
func ParseFileToLine(content string) string { func ParseFileToLine(content string) string {
lines := strings.Split(content, "\n") lines := strings.Split(content, "\n")
users := make([]string, 0) users := make([]string, 0)
+55 -31
View File
@@ -11,71 +11,50 @@ func TestGetRootDomain(t *testing.T) {
// Normal case // Normal case
domain := "http://sub.tinyauth.app" domain := "http://sub.tinyauth.app"
expected := "tinyauth.app" expected := "tinyauth.app"
result, err := utils.GetCookieDomain(domain, true) result, err := utils.GetCookieDomain(domain)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, result) assert.Equal(t, expected, result)
// Domain with multiple subdomains // Domain with multiple subdomains
domain = "http://b.c.tinyauth.app" domain = "http://b.c.tinyauth.app"
expected = "c.tinyauth.app" expected = "c.tinyauth.app"
result, err = utils.GetCookieDomain(domain, true) result, err = utils.GetCookieDomain(domain)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, result) assert.Equal(t, expected, result)
// Invalid domain (only TLD) // Invalid domain (only TLD)
domain = "com" domain = "com"
_, err = utils.GetCookieDomain(domain, true) _, err = utils.GetCookieDomain(domain)
assert.EqualError(t, err, "invalid app url, must be in format subdomain.domain.tld or domain.tld") assert.ErrorContains(t, err, "invalid app url, must be at least second level domain")
// IP address // IP address
domain = "http://10.10.10.10" domain = "http://10.10.10.10"
_, err = utils.GetCookieDomain(domain, true) _, err = utils.GetCookieDomain(domain)
assert.ErrorContains(t, err, "ip addresses not allowed") 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)
assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host") assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host")
// URL with scheme and path // URL with scheme and path
domain = "https://sub.tinyauth.app/path" domain = "https://sub.tinyauth.app/path"
expected = "tinyauth.app" expected = "tinyauth.app"
result, err = utils.GetCookieDomain(domain, true) result, err = utils.GetCookieDomain(domain)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, result) assert.Equal(t, expected, result)
// URL with port // URL with port
domain = "http://sub.tinyauth.app:8080" domain = "http://sub.tinyauth.app:8080"
expected = "tinyauth.app" expected = "tinyauth.app"
result, err = utils.GetCookieDomain(domain, true) result, err = utils.GetCookieDomain(domain)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, result) assert.Equal(t, expected, result)
// Domain managed by ICANN // Domain managed by ICANN
domain = "http://example.co.uk" domain = "http://example.co.uk"
_, err = utils.GetCookieDomain(domain, true) _, err = utils.GetCookieDomain(domain)
assert.ErrorContains(t, err, "domain in public suffix list, cannot set cookies") assert.Error(t, err, "domain in public suffix list, cannot set cookies")
// Domain without subdomain
domain = "http://tinyauth.app"
expected = "tinyauth.app"
result, err = utils.GetCookieDomain(domain, true)
assert.NoError(t, err)
assert.Equal(t, expected, result)
// Case insensitivity
domain = "http://Sub.Tinyauth.App"
expected = "tinyauth.app"
result, err = utils.GetCookieDomain(domain, true)
assert.NoError(t, err)
assert.Equal(t, expected, result)
// Subdomains disabled
domain = "http://sub.tinyauth.app"
expected = "sub.tinyauth.app"
result, err = utils.GetCookieDomain(domain, false)
assert.NoError(t, err)
assert.Equal(t, expected, result)
} }
func TestParseFileToLine(t *testing.T) { func TestParseFileToLine(t *testing.T) {
@@ -146,3 +125,48 @@ func TestFilter(t *testing.T) {
resultStr := utils.Filter(sliceStr, testFuncStr) resultStr := utils.Filter(sliceStr, testFuncStr)
assert.Equal(t, expectedStr, resultStr) assert.Equal(t, expectedStr, resultStr)
} }
func TestGetStandaloneCookieDomain(t *testing.T) {
// Normal case
domain := "http://tinyauth.app"
expected := "tinyauth.app"
result, err := utils.GetStandaloneCookieDomain(domain)
assert.NoError(t, err)
assert.Equal(t, expected, result)
// URL with subdomain (full hostname is returned, no subdomain stripping)
domain = "http://sub.tinyauth.app"
expected = "sub.tinyauth.app"
result, err = utils.GetStandaloneCookieDomain(domain)
assert.NoError(t, err)
assert.Equal(t, expected, result)
// URL with port (port should be stripped)
domain = "http://tinyauth.app:8080"
expected = "tinyauth.app"
result, err = utils.GetStandaloneCookieDomain(domain)
assert.NoError(t, err)
assert.Equal(t, expected, result)
// URL with path
domain = "https://tinyauth.app/some/path"
expected = "tinyauth.app"
result, err = utils.GetStandaloneCookieDomain(domain)
assert.NoError(t, err)
assert.Equal(t, expected, result)
// IP address
domain = "http://10.10.10.10"
_, err = utils.GetStandaloneCookieDomain(domain)
assert.ErrorContains(t, err, "ip addresses not allowed")
// Invalid domain (only TLD)
domain = "com"
_, err = utils.GetStandaloneCookieDomain(domain)
assert.ErrorContains(t, err, "invalid app url")
// Invalid URL
domain = "http://[::1]:namedport"
_, err = utils.GetStandaloneCookieDomain(domain)
assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host")
}