Compare commits

...
Author SHA1 Message Date
StavrosandGitHub 0e7bdf6cd5 feat: experimental option to run tinyauth in oauth bridge mode (#1027) 2026-07-20 00:03:26 +03:00
Stavros 4db1b33034 fix: allow for oauth auto redirect in oidc authorization 2026-07-19 13:34:05 +03:00
Stavros f6637c0f65 fix: don't force login_for=app in continue page 2026-07-19 13:03:55 +03:00
StavrosandGitHub f43d690320 refactor: rework scheme validation in oauth controller and frontend (#1026) 2026-07-19 00:41:28 +03:00
Stavros 50c25e4478 fix: fix domain validator with port but no scheme and https or http 2026-07-18 18:17:24 +03:00
Stavros d6087dc45e fix: allow for mismatching protocol in trusted domain check - ui warnings 2026-07-18 18:06:26 +03:00
Stavros 6efb970673 fix: fix lint errors 2026-07-18 17:52:08 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a9221ece92 chore(deps): bump actions/setup-go from 6.5.0 to 7.0.0 (#1012)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-18 17:10:09 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d5b7e60362 chore(deps): bump the minor-patch group across 1 directory with 4 updates (#1020)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-18 17:09:38 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4d8cd86eec chore(deps): bump the minor-patch group with 2 updates (#1018)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-18 17:09:17 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
504866a432 chore(deps): bump github/codeql-action/upload-sarif from 4.37.0 to 4.37.1 (#1019)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-18 17:08:27 +03:00
Stavros 0c1a64d47d fix: make login_for optional 2026-07-18 17:05:52 +03:00
Stavros a79438d6ec fix: fix typo 2026-07-18 16:47:53 +03:00
Stavros 286aad2e08 test: remove obsolete test case 2026-07-18 16:37:52 +03:00
Stavros 5091fc2d7a fix: suppress irrelevant debugs logs in acls service 2026-07-18 16:30:12 +03:00
Stavros 0fc87ad58f fix: support for parent trusted domain, fixes #1021 2026-07-18 16:30:12 +03:00
31 changed files with 600 additions and 412 deletions
+5
View File
@@ -223,6 +223,11 @@ TINYAUTH_LDAP_AUTHKEY=
# Cache duration for LDAP group membership in seconds.
TINYAUTH_LDAP_GROUPCACHETTL=900
# experimental config
# Enable the OAuth bridge, uses a new way to format OAuth user information.
TINYAUTH_EXPERIMENTAL_OAUTHBRIDGEENABLED=false
# tailscale config
# Enable Tailscale integration.
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
package_json_file: ./frontend/package.json
- name: Setup go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "^1.26.4"
+2 -2
View File
@@ -68,7 +68,7 @@ jobs:
package_json_file: ./frontend/package.json
- name: Install go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "^1.26.4"
@@ -117,7 +117,7 @@ jobs:
package_json_file: ./frontend/package.json
- name: Install go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "^1.26.4"
+2 -2
View File
@@ -46,7 +46,7 @@ jobs:
package_json_file: ./frontend/package.json
- name: Install go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "^1.26.4"
@@ -92,7 +92,7 @@ jobs:
package_json_file: ./frontend/package.json
- name: Install go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "^1.26.4"
+1 -1
View File
@@ -38,6 +38,6 @@ jobs:
retention-days: 5
- name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a # v4
with:
sarif_file: results.sarif
+5 -4
View File
@@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"reflect"
"strings"
"charm.land/huh/v2"
@@ -32,10 +33,10 @@ func main() {
Resources: loaders,
Run: func(_ []string) error {
// enable this on experimental features
//if !reflect.DeepEqual(model.NewDefaultConfiguration(env).Experimental, tConfig.Experimental) {
// colors := getColors()
// fmt.Println(colors.yellow.Render("⚠") + " Experimental features are enabled, use with caution. Experimental features may change with each release.")
//}
if !reflect.DeepEqual(model.NewDefaultConfiguration(env).Experimental, tConfig.Experimental) {
colors := getColors()
fmt.Println(colors.yellow.Render("⚠") + " Experimental features are enabled, use with caution. Experimental features may change with each release.")
}
return runCmd(*tConfig)
},
}
+4 -4
View File
@@ -32,7 +32,7 @@
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-hook-form": "^7.81.0",
"react-i18next": "^17.0.9",
"react-i18next": "^17.0.10",
"react-markdown": "^10.1.0",
"react-router": "^8.2.0",
"sonner": "^2.0.7",
@@ -47,14 +47,14 @@
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"eslint": "^10.6.0",
"eslint": "^10.7.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^17.7.0",
"rollup-plugin-visualizer": "^7.0.1",
"tw-animate-css": "^1.4.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.63.0",
"vite": "^8.1.4"
"typescript-eslint": "^8.64.0",
"vite": "^8.1.5"
}
}
+122 -122
View File
@@ -28,7 +28,7 @@ importers:
version: 1.3.0(@types/react@19.2.17)(react@19.2.7)
'@tailwindcss/vite':
specifier: ^4.3.2
version: 4.3.2(vite@8.1.4(@types/node@26.1.1)(jiti@2.7.0))
version: 4.3.2(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0))
'@tanstack/react-query':
specifier: ^5.101.2
version: 5.101.2(react@19.2.7)
@@ -69,8 +69,8 @@ importers:
specifier: ^7.81.0
version: 7.81.0(react@19.2.7)
react-i18next:
specifier: ^17.0.9
version: 17.0.9(i18next@26.3.6(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
specifier: ^17.0.10
version: 17.0.10(i18next@26.3.6(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
react-markdown:
specifier: ^10.1.0
version: 10.1.0(@types/react@19.2.17)(react@19.2.7)
@@ -92,10 +92,10 @@ importers:
devDependencies:
'@eslint/js':
specifier: ^10.0.1
version: 10.0.1(eslint@10.6.0(jiti@2.7.0))
version: 10.0.1(eslint@10.7.0(jiti@2.7.0))
'@tanstack/eslint-plugin-query':
specifier: ^5.101.2
version: 5.101.2(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)
version: 5.101.2(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)
'@types/node':
specifier: ^26.1.1
version: 26.1.1
@@ -107,16 +107,16 @@ importers:
version: 19.2.3(@types/react@19.2.17)
'@vitejs/plugin-react':
specifier: ^6.0.3
version: 6.0.3(vite@8.1.4(@types/node@26.1.1)(jiti@2.7.0))
version: 6.0.3(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0))
eslint:
specifier: ^10.6.0
version: 10.6.0(jiti@2.7.0)
specifier: ^10.7.0
version: 10.7.0(jiti@2.7.0)
eslint-plugin-react-hooks:
specifier: ^7.0.1
version: 7.1.1(eslint@10.6.0(jiti@2.7.0))
version: 7.1.1(eslint@10.7.0(jiti@2.7.0))
eslint-plugin-react-refresh:
specifier: ^0.5.3
version: 0.5.3(eslint@10.6.0(jiti@2.7.0))
version: 0.5.3(eslint@10.7.0(jiti@2.7.0))
globals:
specifier: ^17.7.0
version: 17.7.0
@@ -130,11 +130,11 @@ importers:
specifier: ~6.0.2
version: 6.0.3
typescript-eslint:
specifier: ^8.63.0
version: 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)
specifier: ^8.64.0
version: 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)
vite:
specifier: ^8.1.4
version: 8.1.4(@types/node@26.1.1)(jiti@2.7.0)
specifier: ^8.1.5
version: 8.1.5(@types/node@26.1.1)(jiti@2.7.0)
packages:
@@ -1272,16 +1272,16 @@ packages:
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@typescript-eslint/eslint-plugin@8.63.0':
resolution: {integrity: sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==}
'@typescript-eslint/eslint-plugin@8.64.0':
resolution: {integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.63.0
'@typescript-eslint/parser': ^8.64.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/parser@8.63.0':
resolution: {integrity: sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==}
'@typescript-eslint/parser@8.64.0':
resolution: {integrity: sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -1293,8 +1293,8 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.63.0':
resolution: {integrity: sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==}
'@typescript-eslint/project-service@8.64.0':
resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
@@ -1303,8 +1303,8 @@ packages:
resolution: {integrity: sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/scope-manager@8.63.0':
resolution: {integrity: sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==}
'@typescript-eslint/scope-manager@8.64.0':
resolution: {integrity: sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.62.1':
@@ -1313,14 +1313,14 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/tsconfig-utils@8.63.0':
resolution: {integrity: sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==}
'@typescript-eslint/tsconfig-utils@8.64.0':
resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/type-utils@8.63.0':
resolution: {integrity: sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==}
'@typescript-eslint/type-utils@8.64.0':
resolution: {integrity: sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -1330,8 +1330,8 @@ packages:
resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.63.0':
resolution: {integrity: sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==}
'@typescript-eslint/types@8.64.0':
resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.62.1':
@@ -1340,8 +1340,8 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/typescript-estree@8.63.0':
resolution: {integrity: sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==}
'@typescript-eslint/typescript-estree@8.64.0':
resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
@@ -1353,8 +1353,8 @@ packages:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.63.0':
resolution: {integrity: sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==}
'@typescript-eslint/utils@8.64.0':
resolution: {integrity: sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -1364,8 +1364,8 @@ packages:
resolution: {integrity: sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.63.0':
resolution: {integrity: sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==}
'@typescript-eslint/visitor-keys@8.64.0':
resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.3.1':
@@ -1603,8 +1603,8 @@ packages:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@10.6.0:
resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==}
eslint@10.7.0:
resolution: {integrity: sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true
peerDependencies:
@@ -1784,8 +1784,8 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
ignore@7.0.5:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
ignore@7.0.6:
resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==}
engines: {node: '>= 4'}
imurmurhash@0.1.4:
@@ -2071,8 +2071,8 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nanoid@3.3.15:
resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
nanoid@3.3.16:
resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
@@ -2126,8 +2126,8 @@ packages:
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
engines: {node: '>=12'}
postcss@8.5.16:
resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
postcss@8.5.19:
resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==}
engines: {node: ^10 || ^12 || >=14}
powershell-utils@0.1.0:
@@ -2173,8 +2173,8 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-i18next@17.0.9:
resolution: {integrity: sha512-buLzOSqHtXxjf+qgSrLWNTXVZ1jSwO6kUv3uJqSP1roGBPgNnbhFm7OmdVwWcgf2gIbUyP0J333uPyx+Btsi3w==}
react-i18next@17.0.10:
resolution: {integrity: sha512-XneHftyYA774MJkkccSkZ5oKrUpCnXIPmxio3wemqrVzCRLWiGXOMbIzObrer03fNDEnm8g8R5yYls4HcE+esg==}
peerDependencies:
i18next: '>= 26.2.0'
react: '>= 16.8.0'
@@ -2357,8 +2357,8 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
typescript-eslint@8.63.0:
resolution: {integrity: sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==}
typescript-eslint@8.64.0:
resolution: {integrity: sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -2430,8 +2430,8 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
vite@8.1.4:
resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==}
vite@8.1.5:
resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
@@ -2647,9 +2647,9 @@ snapshots:
tslib: 2.8.1
optional: true
'@eslint-community/eslint-utils@4.9.1(eslint@10.6.0(jiti@2.7.0))':
'@eslint-community/eslint-utils@4.9.1(eslint@10.7.0(jiti@2.7.0))':
dependencies:
eslint: 10.6.0(jiti@2.7.0)
eslint: 10.7.0(jiti@2.7.0)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
@@ -2670,9 +2670,9 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
'@eslint/js@10.0.1(eslint@10.6.0(jiti@2.7.0))':
'@eslint/js@10.0.1(eslint@10.7.0(jiti@2.7.0))':
optionalDependencies:
eslint: 10.6.0(jiti@2.7.0)
eslint: 10.7.0(jiti@2.7.0)
'@eslint/object-schema@3.0.5': {}
@@ -3607,17 +3607,17 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.2
'@tailwindcss/oxide-win32-x64-msvc': 4.3.2
'@tailwindcss/vite@4.3.2(vite@8.1.4(@types/node@26.1.1)(jiti@2.7.0))':
'@tailwindcss/vite@4.3.2(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0))':
dependencies:
'@tailwindcss/node': 4.3.2
'@tailwindcss/oxide': 4.3.2
tailwindcss: 4.3.2
vite: 8.1.4(@types/node@26.1.1)(jiti@2.7.0)
vite: 8.1.5(@types/node@26.1.1)(jiti@2.7.0)
'@tanstack/eslint-plugin-query@5.101.2(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)':
'@tanstack/eslint-plugin-query@5.101.2(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)
eslint: 10.6.0(jiti@2.7.0)
'@typescript-eslint/utils': 8.62.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)
eslint: 10.7.0(jiti@2.7.0)
optionalDependencies:
typescript: 6.0.3
transitivePeerDependencies:
@@ -3675,30 +3675,30 @@ snapshots:
'@types/unist@3.0.3': {}
'@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)':
'@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/scope-manager': 8.63.0
'@typescript-eslint/type-utils': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/utils': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.63.0
eslint: 10.6.0(jiti@2.7.0)
ignore: 7.0.5
'@typescript-eslint/parser': 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/scope-manager': 8.64.0
'@typescript-eslint/type-utils': 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.64.0
eslint: 10.7.0(jiti@2.7.0)
ignore: 7.0.6
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)':
'@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.63.0
'@typescript-eslint/types': 8.63.0
'@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.63.0
'@typescript-eslint/scope-manager': 8.64.0
'@typescript-eslint/types': 8.64.0
'@typescript-eslint/typescript-estree': 8.64.0(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.64.0
debug: 4.4.3
eslint: 10.6.0(jiti@2.7.0)
eslint: 10.7.0(jiti@2.7.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
@@ -3712,10 +3712,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.63.0(typescript@6.0.3)':
'@typescript-eslint/project-service@8.64.0(typescript@6.0.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.63.0(typescript@6.0.3)
'@typescript-eslint/types': 8.63.0
'@typescript-eslint/tsconfig-utils': 8.64.0(typescript@6.0.3)
'@typescript-eslint/types': 8.64.0
debug: 4.4.3
typescript: 6.0.3
transitivePeerDependencies:
@@ -3726,26 +3726,26 @@ snapshots:
'@typescript-eslint/types': 8.62.1
'@typescript-eslint/visitor-keys': 8.62.1
'@typescript-eslint/scope-manager@8.63.0':
'@typescript-eslint/scope-manager@8.64.0':
dependencies:
'@typescript-eslint/types': 8.63.0
'@typescript-eslint/visitor-keys': 8.63.0
'@typescript-eslint/types': 8.64.0
'@typescript-eslint/visitor-keys': 8.64.0
'@typescript-eslint/tsconfig-utils@8.62.1(typescript@6.0.3)':
dependencies:
typescript: 6.0.3
'@typescript-eslint/tsconfig-utils@8.63.0(typescript@6.0.3)':
'@typescript-eslint/tsconfig-utils@8.64.0(typescript@6.0.3)':
dependencies:
typescript: 6.0.3
'@typescript-eslint/type-utils@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)':
'@typescript-eslint/type-utils@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@typescript-eslint/types': 8.63.0
'@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3)
'@typescript-eslint/utils': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/types': 8.64.0
'@typescript-eslint/typescript-estree': 8.64.0(typescript@6.0.3)
'@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)
debug: 4.4.3
eslint: 10.6.0(jiti@2.7.0)
eslint: 10.7.0(jiti@2.7.0)
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
@@ -3753,7 +3753,7 @@ snapshots:
'@typescript-eslint/types@8.62.1': {}
'@typescript-eslint/types@8.63.0': {}
'@typescript-eslint/types@8.64.0': {}
'@typescript-eslint/typescript-estree@8.62.1(typescript@6.0.3)':
dependencies:
@@ -3770,12 +3770,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.63.0(typescript@6.0.3)':
'@typescript-eslint/typescript-estree@8.64.0(typescript@6.0.3)':
dependencies:
'@typescript-eslint/project-service': 8.63.0(typescript@6.0.3)
'@typescript-eslint/tsconfig-utils': 8.63.0(typescript@6.0.3)
'@typescript-eslint/types': 8.63.0
'@typescript-eslint/visitor-keys': 8.63.0
'@typescript-eslint/project-service': 8.64.0(typescript@6.0.3)
'@typescript-eslint/tsconfig-utils': 8.64.0(typescript@6.0.3)
'@typescript-eslint/types': 8.64.0
'@typescript-eslint/visitor-keys': 8.64.0
debug: 4.4.3
minimatch: 10.2.5
semver: 7.8.5
@@ -3785,24 +3785,24 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)':
'@typescript-eslint/utils@8.62.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0))
'@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0))
'@typescript-eslint/scope-manager': 8.62.1
'@typescript-eslint/types': 8.62.1
'@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3)
eslint: 10.6.0(jiti@2.7.0)
eslint: 10.7.0(jiti@2.7.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)':
'@typescript-eslint/utils@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0))
'@typescript-eslint/scope-manager': 8.63.0
'@typescript-eslint/types': 8.63.0
'@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3)
eslint: 10.6.0(jiti@2.7.0)
'@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0))
'@typescript-eslint/scope-manager': 8.64.0
'@typescript-eslint/types': 8.64.0
'@typescript-eslint/typescript-estree': 8.64.0(typescript@6.0.3)
eslint: 10.7.0(jiti@2.7.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
@@ -3812,17 +3812,17 @@ snapshots:
'@typescript-eslint/types': 8.62.1
eslint-visitor-keys: 5.0.1
'@typescript-eslint/visitor-keys@8.63.0':
'@typescript-eslint/visitor-keys@8.64.0':
dependencies:
'@typescript-eslint/types': 8.63.0
'@typescript-eslint/types': 8.64.0
eslint-visitor-keys: 5.0.1
'@ungap/structured-clone@1.3.1': {}
'@vitejs/plugin-react@6.0.3(vite@8.1.4(@types/node@26.1.1)(jiti@2.7.0))':
'@vitejs/plugin-react@6.0.3(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0))':
dependencies:
'@rolldown/pluginutils': 1.0.1
vite: 8.1.4(@types/node@26.1.1)(jiti@2.7.0)
vite: 8.1.5(@types/node@26.1.1)(jiti@2.7.0)
acorn-jsx@5.3.2(acorn@8.17.0):
dependencies:
@@ -3997,20 +3997,20 @@ snapshots:
escape-string-regexp@4.0.0: {}
eslint-plugin-react-hooks@7.1.1(eslint@10.6.0(jiti@2.7.0)):
eslint-plugin-react-hooks@7.1.1(eslint@10.7.0(jiti@2.7.0)):
dependencies:
'@babel/core': 7.29.0
'@babel/parser': 7.29.3
eslint: 10.6.0(jiti@2.7.0)
eslint: 10.7.0(jiti@2.7.0)
hermes-parser: 0.25.1
zod: 4.4.3
zod-validation-error: 4.0.2(zod@4.4.3)
transitivePeerDependencies:
- supports-color
eslint-plugin-react-refresh@0.5.3(eslint@10.6.0(jiti@2.7.0)):
eslint-plugin-react-refresh@0.5.3(eslint@10.7.0(jiti@2.7.0)):
dependencies:
eslint: 10.6.0(jiti@2.7.0)
eslint: 10.7.0(jiti@2.7.0)
eslint-scope@9.1.2:
dependencies:
@@ -4023,9 +4023,9 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
eslint@10.6.0(jiti@2.7.0):
eslint@10.7.0(jiti@2.7.0):
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0))
'@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0))
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.23.5
'@eslint/config-helpers': 0.6.0
@@ -4226,7 +4226,7 @@ snapshots:
ignore@5.3.2: {}
ignore@7.0.5: {}
ignore@7.0.6: {}
imurmurhash@0.1.4: {}
@@ -4591,7 +4591,7 @@ snapshots:
ms@2.1.3: {}
nanoid@3.3.15: {}
nanoid@3.3.16: {}
natural-compare@1.4.0: {}
@@ -4648,9 +4648,9 @@ snapshots:
picomatch@4.0.5: {}
postcss@8.5.16:
postcss@8.5.19:
dependencies:
nanoid: 3.3.15
nanoid: 3.3.16
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -4736,7 +4736,7 @@ snapshots:
dependencies:
react: 19.2.7
react-i18next@17.0.9(i18next@26.3.6(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3):
react-i18next@17.0.10(i18next@26.3.6(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3):
dependencies:
'@babel/runtime': 7.29.7
html-parse-stringify: 3.0.1
@@ -4923,13 +4923,13 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
typescript-eslint@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3):
typescript-eslint@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/parser': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3)
'@typescript-eslint/utils': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)
eslint: 10.6.0(jiti@2.7.0)
'@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/parser': 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/typescript-estree': 8.64.0(typescript@6.0.3)
'@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)
eslint: 10.7.0(jiti@2.7.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
@@ -5010,11 +5010,11 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vite@8.1.4(@types/node@26.1.1)(jiti@2.7.0):
vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.5
postcss: 8.5.16
postcss: 8.5.19
rolldown: 1.1.5
tinyglobby: 0.2.17
optionalDependencies:
@@ -32,8 +32,8 @@ import { useTranslation } from "react-i18next";
import { useLocation } from "react-router";
import { useRef } from "react";
import {
searchParamsFromObject,
useScreenParams,
recompileScreenParams,
} from "@/lib/hooks/screen-params";
import { useMutation } from "@tanstack/react-query";
import axios from "axios";
@@ -70,7 +70,13 @@ export const QuickActions = () => {
const redirectTimer = useRef<number | null>(null);
const searchParams = new URLSearchParams(search);
const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams(screenParams);
const compiledParams = (() => {
const params = searchParamsFromObject(screenParams).toString();
if (params.length > 0) {
return `?${params}`;
}
return "";
})();
const [isOpen, setIsOpen] = useState(false);
+6 -2
View File
@@ -1,10 +1,11 @@
type UseLoginForProps = {
login_for?: "oidc" | "app";
compiledParams: string;
params: URLSearchParams;
};
export const useLoginFor = (props: UseLoginForProps): string => {
const { login_for, compiledParams } = props;
const { login_for, params } = props;
const compiledParams = params.toString() ? "?" + params.toString() : "";
switch (login_for) {
case "oidc":
@@ -12,6 +13,9 @@ export const useLoginFor = (props: UseLoginForProps): string => {
case "app":
return "/continue" + compiledParams;
default:
if (params.get("redirect_uri")) {
return "/continue" + compiledParams
}
return "/logout";
}
};
+18 -18
View File
@@ -75,18 +75,21 @@ export const useRedirectUri = (
};
};
// ported from internal/controller/oauth_controller.go
const getEffectivePort = (url: URL): string => {
if (url.port) {
return url.port;
}
// 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("[");
}
if (url.protocol == "https:") {
return "443";
const trimPeriod = (str: string): string => {
if(str.lastIndexOf('.') === (str.length - 1)){
str = str.substring(0, str.length - 1);
}
return "80";
};
return str
}
export const isTrustedDomain = (
url: URL,
@@ -94,15 +97,15 @@ export const isTrustedDomain = (
cookieDomain: string,
subdomainsEnabled: boolean,
): boolean => {
if (url.protocol != appUrl.protocol) {
if (isIP(url.hostname)) {
return false;
}
if (getEffectivePort(url) != getEffectivePort(appUrl)) {
if (url.port != appUrl.port) {
return false;
}
if (url.hostname == appUrl.hostname) {
if (trimPeriod(url.hostname) == trimPeriod(appUrl.hostname)) {
return true;
}
@@ -110,9 +113,6 @@ export const isTrustedDomain = (
return false;
}
if (url.hostname.endsWith("." + cookieDomain.toLowerCase())) {
return true;
}
return false;
return trimPeriod(url.hostname).endsWith("." + cookieDomain.toLowerCase())
|| trimPeriod(url.hostname) == cookieDomain.toLowerCase();
};
+4 -10
View File
@@ -27,16 +27,10 @@ export function useScreenParams(params: URLSearchParams): ScreenParams {
return parsed.data;
}
export function recompileScreenParams(params: ScreenParams): string {
const p = new URLSearchParams(
export function searchParamsFromObject(obj: object): URLSearchParams {
return new URLSearchParams(
Object.fromEntries(
Object.entries(params).filter(([, v]) => v !== undefined),
Object.entries(obj).filter(([, v]) => v !== undefined),
) as Record<string, string>,
).toString();
if (p.length > 0) {
return "?" + p;
}
return "";
);
}
+8 -2
View File
@@ -22,7 +22,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
recompileScreenParams,
searchParamsFromObject,
useScreenParams,
} from "@/lib/hooks/screen-params";
import { useEffect } from "react";
@@ -89,7 +89,13 @@ export const AuthorizePage = () => {
const searchParams = new URLSearchParams(search);
const screenParams = useScreenParams(searchParams);
const isOidc = screenParams.login_for === "oidc";
const compiledParams = recompileScreenParams(screenParams);
const compiledParams = (() => {
const params = searchParamsFromObject(screenParams).toString();
if (params.length > 0) {
return `?${params}`;
}
return "";
})();
// TODO: maybe a better way to do this
const shouldAutoAuthorize =
+10 -4
View File
@@ -13,7 +13,7 @@ import { Navigate, useLocation, useNavigate } from "react-router";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRedirectUri } from "@/lib/hooks/redirect-uri";
import {
recompileScreenParams,
searchParamsFromObject,
useScreenParams,
} from "@/lib/hooks/screen-params";
@@ -31,8 +31,14 @@ export const ContinuePage = () => {
const searchParams = new URLSearchParams(search);
const screenParams = useScreenParams(searchParams);
const redirectUri = screenParams.redirect_uri;
const isAppLogin = screenParams.login_for === "app";
const recompiledParams = recompileScreenParams(screenParams);
const isAppLogin = screenParams.login_for === "app" || !screenParams.login_for;
const compiledParams = (() => {
const params = searchParamsFromObject(screenParams).toString();
if (params.length > 0) {
return `?${params}`;
}
return "";
})();
const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
redirectUri,
@@ -89,7 +95,7 @@ export const ContinuePage = () => {
}, [shouldAutoRedirect, redirectToTarget]);
if (!auth.authenticated) {
return <Navigate to={`/login${recompiledParams}`} replace />;
return <Navigate to={`/login${compiledParams}`} replace />;
}
if (!hasValidRedirect || !isAppLogin) {
+9 -3
View File
@@ -12,8 +12,8 @@ import { useTranslation } from "react-i18next";
import Markdown from "react-markdown";
import { useLocation } from "react-router";
import {
recompileScreenParams,
useScreenParams,
searchParamsFromObject,
useScreenParams,
} from "@/lib/hooks/screen-params";
export const ForgotPasswordPage = () => {
@@ -22,7 +22,13 @@ export const ForgotPasswordPage = () => {
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams(screenParams);
const compiledParams = (() => {
const params = searchParamsFromObject(screenParams).toString();
if (params.length > 0) {
return `?${params}`;
}
return "";
})();
return (
<Card>
+12 -10
View File
@@ -26,7 +26,7 @@ import { useTranslation } from "react-i18next";
import { Navigate, useLocation } from "react-router";
import { toast } from "sonner";
import {
recompileScreenParams,
searchParamsFromObject,
useScreenParams,
} from "@/lib/hooks/screen-params";
import { useLoginFor } from "@/lib/hooks/login-for";
@@ -63,18 +63,21 @@ export const LoginPage = () => {
const searchParams = new URLSearchParams(search);
const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams({
...screenParams,
oidc_prompt: undefined,
});
const compiledParams = (() => {
const params = searchParamsFromObject(screenParams).toString();
if (params.length > 0) {
return `?${params}`;
}
return "";
})();
const loginForUrl = useLoginFor({
login_for: screenParams.login_for,
compiledParams,
params: searchParamsFromObject({ ...screenParams, oidc_prompt: undefined}),
});
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
providers.find((provider) => provider.id === oauth.autoRedirect) !==
undefined && screenParams.redirect_uri !== undefined,
undefined && (screenParams.redirect_uri || screenParams.oidc_ticket),
);
const oauthProviders = providers.filter(
@@ -171,8 +174,7 @@ export const LoginPage = () => {
!auth.authenticated &&
isOauthAutoRedirect &&
!hasAutoRedirectedRef.current &&
screenParams.redirect_uri &&
screenParams.login_for
(screenParams.redirect_uri || screenParams.oidc_ticket)
) {
hasAutoRedirectedRef.current = true;
oauthMutate(oauth.autoRedirect);
@@ -183,8 +185,8 @@ export const LoginPage = () => {
hasAutoRedirectedRef,
oauth.autoRedirect,
isOauthAutoRedirect,
screenParams.login_for,
screenParams.redirect_uri,
screenParams.oidc_ticket
]);
useEffect(() => {
+8 -2
View File
@@ -17,8 +17,8 @@ import { type UseMutationResult } from "@tanstack/react-query";
import { type AxiosResponse } from "axios";
import { useLocation } from "react-router";
import {
searchParamsFromObject,
useScreenParams,
recompileScreenParams,
} from "@/lib/hooks/screen-params";
export const LogoutPage = () => {
@@ -29,7 +29,13 @@ export const LogoutPage = () => {
const redirectTimer = useRef<number | null>(null);
const searchParams = new URLSearchParams(search);
const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams(screenParams);
const compiledParams = (() => {
const params = searchParamsFromObject(screenParams).toString();
if (params.length > 0) {
return `?${params}`;
}
return "";
})();
const logoutMutation = useMutation({
mutationFn: () => axios.post("/api/user/logout"),
+9 -3
View File
@@ -17,7 +17,7 @@ import { useTranslation } from "react-i18next";
import { Navigate, useLocation } from "react-router";
import { toast } from "sonner";
import {
recompileScreenParams,
searchParamsFromObject,
useScreenParams,
} from "@/lib/hooks/screen-params";
import { useLoginFor } from "@/lib/hooks/login-for";
@@ -32,10 +32,16 @@ export const TotpPage = () => {
const searchParams = new URLSearchParams(search);
const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams(screenParams);
const compiledParams = (() => {
const params = searchParamsFromObject(screenParams).toString();
if (params.length > 0) {
return `?${params}`;
}
return "";
})();
const loginForUrl = useLoginFor({
login_for: screenParams.login_for,
compiledParams,
params: searchParamsFromObject(screenParams),
});
const totpMutation = useMutation({
+5 -5
View File
@@ -9,7 +9,7 @@ require (
github.com/docker/docker v28.5.2+incompatible
github.com/gin-gonic/gin v1.12.0
github.com/go-jose/go-jose/v4 v4.1.4
github.com/go-ldap/ldap/v3 v3.4.13
github.com/go-ldap/ldap/v3 v3.4.14
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/google/go-querystring v1.2.0
@@ -24,12 +24,13 @@ require (
github.com/weppos/publicsuffix-go v0.50.3
go.uber.org/dig v1.19.0
golang.org/x/crypto v0.54.0
golang.org/x/net v0.57.0
golang.org/x/oauth2 v0.36.0
golang.org/x/tools v0.48.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.36.2
k8s.io/client-go v0.36.2
modernc.org/sqlite v1.53.0
modernc.org/sqlite v1.54.0
)
require (
@@ -72,7 +73,7 @@ require (
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect
@@ -135,7 +136,6 @@ require (
golang.org/x/arch v0.22.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/term v0.45.0 // indirect
@@ -147,7 +147,7 @@ require (
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
modernc.org/libc v1.73.4 // indirect
modernc.org/libc v1.74.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
rsc.io/qr v0.2.0 // indirect
+14 -14
View File
@@ -113,12 +113,12 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ=
github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs=
github.com/go-ldap/ldap/v3 v3.4.14/go.mod h1:S4eJUMUNjDkE0ZJtIZdybwyb03sGGLW6gxXT1Hs8VKA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -392,20 +392,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/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=
modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
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/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ=
modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -414,8 +414,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
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/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+9 -7
View File
@@ -88,17 +88,13 @@ func (app *BootstrapApp) Setup() error {
app.log.App.Info().Msgf("Starting Tinyauth version: %s", model.Version)
// get app url
if app.config.AppURL == "" {
return errors.New("app url cannot be empty, perhaps config loading failed")
}
appUrl, err := url.Parse(app.config.AppURL)
appURL, err := utils.SafeParseAppURL(app.config.AppURL)
if err != nil {
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
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
// 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
+68 -33
View File
@@ -220,35 +220,16 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
return
}
var name string
if strings.TrimSpace(user.Name) != "" {
controller.log.App.Debug().Msg("Using name from OAuth provider")
name = user.Name
} else {
controller.log.App.Debug().Msg("No name from OAuth provider, generating from email")
parts := strings.SplitN(user.Email, "@", 2)
if len(parts) == 2 {
name = fmt.Sprintf("%s (%s)", utils.Capitalize(parts[0]), parts[1])
} else {
name = utils.Capitalize(user.Email)
}
}
var username string
if strings.TrimSpace(user.PreferredUsername) != "" {
controller.log.App.Debug().Msg("Using preferred username from OAuth provider")
username = user.PreferredUsername
} else {
controller.log.App.Debug().Msg("No preferred username from OAuth provider, generating from email")
username = strings.Replace(user.Email, "@", "_", 1)
}
oauthUserInfo := controller.createOAuthUserInfo(oauthUserInfo{
Username: user.PreferredUsername,
Email: user.Email,
Name: user.Name,
})
sessionCookie := repository.Session{
Username: username,
Name: name,
Email: user.Email,
Username: oauthUserInfo.Username,
Name: oauthUserInfo.Name,
Email: oauthUserInfo.Email,
Provider: svc.ID(),
OAuthGroups: utils.CoalesceToString(user.Groups),
OAuthName: svc.Name(),
@@ -313,8 +294,7 @@ func (controller *OAuthController) getCookieDomain() string {
func (controller *OAuthController) isRedirectSafe(redirectURI string) bool {
v := validators.NewDomainValidator(validators.DomainValidatorOptions{
WithScheme: true,
WithPort: true,
WithPort: true,
})
_, err := v.SafeHostname(controller.runtime.AppURL)
@@ -332,9 +312,7 @@ func (controller *OAuthController) isRedirectSafe(redirectURI string) bool {
controller.log.App.Debug().Err(err).Msg("Failed to validate redirect URI")
if errors.Is(err, validators.ErrInvalidURL) ||
errors.Is(err, validators.ErrSchemeMismatch) ||
errors.Is(err, validators.ErrPortMismatch) {
if !errors.Is(err, validators.ErrHostnameMismatch) {
return false
}
@@ -351,9 +329,66 @@ func (controller *OAuthController) isRedirectSafe(redirectURI string) bool {
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 false
}
type oauthUserInfo struct {
Email string
Username string
Name string
}
func (controller *OAuthController) createOAuthUserInfo(input oauthUserInfo) oauthUserInfo {
info := oauthUserInfo{
Email: input.Email,
}
if controller.config.Experimental.OAuthBridgeEnabled {
if input.Username != "" {
info.Username = input.Username
} else {
parts := strings.SplitN(input.Email, "@", 2)
if len(parts) != 2 {
controller.log.App.Error().Str("email", input.Email).Msg("Invalid email address")
} else {
info.Username = parts[0]
}
}
if input.Name != "" {
info.Name = input.Name
} else {
info.Name = utils.Capitalize(info.Username)
}
return info
}
if input.Name != "" {
controller.log.App.Debug().Msg("Using name from OAuth provider")
info.Name = input.Name
} else {
controller.log.App.Debug().Msg("No name from OAuth provider, generating from email")
parts := strings.SplitN(input.Email, "@", 2)
if len(parts) != 2 {
controller.log.App.Error().Str("email", input.Email).Msg("Invalid email address")
} else {
info.Name = fmt.Sprintf("%s (%s)", utils.Capitalize(parts[0]), parts[1])
}
}
if input.Username != "" {
controller.log.App.Debug().Msg("Using preferred username from OAuth provider")
info.Username = input.Username
} else {
controller.log.App.Debug().Msg("No preferred username from OAuth provider, generating from email")
info.Username = strings.Replace(info.Email, "@", "_", 1)
}
return info
}
@@ -81,22 +81,6 @@ func TestOAuthController_isRedirectSafe(t *testing.T) {
redirectURI: "https://sub.example.com",
expected: false,
},
{
description: "Cookie domain itself is not a subdomain match",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "https://example.com",
expected: false,
},
{
description: "Different scheme returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "http://tinyauth.example.com",
expected: false,
},
{
description: "Different port returns false",
appURL: "https://tinyauth.example.com",
@@ -121,14 +105,6 @@ func TestOAuthController_isRedirectSafe(t *testing.T) {
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",
+11 -5
View File
@@ -40,6 +40,7 @@ var (
type ContextMiddleware struct {
log *logger.Logger
runtime *model.RuntimeConfig
config *model.Config
auth *service.AuthService
broker *service.OAuthBrokerService
tailscale *service.TailscaleService
@@ -50,6 +51,7 @@ type ContextMiddlewareInput struct {
Log *logger.Logger
RuntimeConfig *model.RuntimeConfig
StaticConfig *model.Config
AuthService *service.AuthService
BrokerService *service.OAuthBrokerService
TailscaleService *service.TailscaleService
@@ -59,6 +61,7 @@ func NewContextMiddleware(i ContextMiddlewareInput) *ContextMiddleware {
return &ContextMiddleware{
log: i.Log,
runtime: i.RuntimeConfig,
config: i.StaticConfig,
auth: i.AuthService,
broker: i.BrokerService,
tailscale: i.TailscaleService,
@@ -332,16 +335,19 @@ func (m *ContextMiddleware) tailscaleWhois(ip string) (*model.TailscaleContext,
return nil, nil
}
username := strings.Replace(whois.LoginName, "@", "_", 1)
uctx := model.TailscaleContext{
BaseContext: model.BaseContext{
Username: username,
Email: whois.LoginName,
Name: whois.DisplayName,
Email: whois.LoginName,
Name: whois.DisplayName,
},
NodeName: whois.NodeName,
}
if m.config.Experimental.OAuthBridgeEnabled {
uctx.BaseContext.Username = strings.SplitN(whois.LoginName, "@", 2)[0]
} else {
uctx.BaseContext.Username = strings.Replace(whois.LoginName, "@", "_", 1)
}
return &uctx, nil
}
+6 -4
View File
@@ -115,9 +115,9 @@ type Config struct {
UI UIConfig `description:"UI customization." yaml:"ui,omitempty"`
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,omitempty"`
// enable the cli warning on experimental features
//Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"`
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale,omitempty"`
Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"`
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"`
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale,omitempty"`
Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"`
}
type DatabaseConfig struct {
@@ -238,7 +238,9 @@ type LogStreamConfig struct {
Level string `description:"Log level for this stream. Use global if empty." yaml:"level,omitempty"`
}
//type ExperimentalConfig struct{}
type ExperimentalConfig struct {
OAuthBridgeEnabled bool `description:"Enable the OAuth bridge, uses a new way to format OAuth user information." yaml:"oauthBridgeEnabled,omitempty"`
}
type TailscaleConfig struct {
Enabled bool `description:"Enable Tailscale integration." yaml:"enabled,omitempty"`
+9 -7
View File
@@ -44,13 +44,15 @@ func (service *AccessControlsService) lookupStaticACLs(domain string) *model.App
// First try to find a matching app by domain, then fallback to matching by app name (subdomain)
for app, config := range service.config.Apps {
err := v.Validate(config.Config.Domain, domain)
if err == nil {
service.log.App.Debug().Str("name", app).Msg("Found matching container by domain")
return &config
}
if !errors.Is(err, validators.ErrHostnameMismatch) {
service.log.App.Debug().Str("name", app).Err(err).Msg("Domain validation failed")
if config.Config.Domain != "" {
err := v.Validate(config.Config.Domain, domain)
if err == nil {
service.log.App.Debug().Str("name", app).Msg("Found matching container by domain")
return &config
}
if !errors.Is(err, validators.ErrHostnameMismatch) {
service.log.App.Debug().Str("name", app).Err(err).Msg("Domain validation failed")
}
}
if strings.HasPrefix(strings.ToLower(domain), strings.ToLower(app+".")) {
service.log.App.Debug().Str("name", app).Msg("Found matching container by app name")
+46 -5
View File
@@ -7,10 +7,55 @@ import (
"strings"
"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
// 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.
func GetCookieDomain(appUrl string, subdomainsEnabled bool) (string, error) {
u, err := url.Parse(appUrl)
@@ -21,10 +66,6 @@ func GetCookieDomain(appUrl string, subdomainsEnabled bool) (string, error) {
hostname := strings.ToLower(u.Hostname())
if netIP := net.ParseIP(hostname); netIP != nil {
return "", fmt.Errorf("ip addresses not allowed")
}
parts := strings.Split(hostname, ".")
if len(parts) < 2 {
+85 -6
View File
@@ -7,7 +7,86 @@ import (
"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
domain := "http://sub.tinyauth.app"
expected := "tinyauth.app"
@@ -27,11 +106,6 @@ func TestGetRootDomain(t *testing.T) {
_, err = utils.GetCookieDomain(domain, true)
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
domain = "http://[::1]:namedport"
_, err = utils.GetCookieDomain(domain, true)
@@ -56,6 +130,11 @@ func TestGetRootDomain(t *testing.T) {
_, err = utils.GetCookieDomain(domain, true)
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 = "http://tinyauth.app"
expected = "tinyauth.app"
+10
View File
@@ -0,0 +1,10 @@
# Public packages
This directory contains packages that can be used by
other projects.
While we try to maintain a consistent API, no promises
can be made for non-breaking changes throughout updates
as we constantly need to make changes to comply with the
needs of Tinyauth. We advise pinning the version of the
package you wish to use.
+64 -53
View File
@@ -10,14 +10,13 @@ import (
"fmt"
"net"
"net/url"
"slices"
"strings"
"golang.org/x/net/idna"
)
// Errors
var (
ErrInvalidURL = fmt.Errorf("invalid url")
ErrSchemeMismatch = fmt.Errorf("scheme mismatch")
ErrPortMismatch = fmt.Errorf("port mismatch")
ErrHostnameMismatch = fmt.Errorf("hostname mismatch")
@@ -29,8 +28,7 @@ type DomainValidatorOptions struct {
WithScheme bool
// Ensure domains have the same port.
WithPort bool
// Specify a list of allowed schemes IF WithScheme is set to true.
// Leave empty to allow any scheme.
// Specify a list of allowed schemes if WithScheme is set to true.
AllowedSchemes []string
}
@@ -48,53 +46,74 @@ func NewDomainValidator(opts DomainValidatorOptions) *DomainValidator {
}
}
func (v *DomainValidator) checkScheme(rawURL string) error {
if !v.opts.WithScheme {
return nil
}
if len(v.opts.AllowedSchemes) == 0 {
return fmt.Errorf("allowed schemes must be specified")
}
for _, scheme := range v.opts.AllowedSchemes {
if strings.HasPrefix(strings.ToLower(rawURL), strings.ToLower(scheme)+"://") {
return nil
}
}
return fmt.Errorf("invalid scheme")
}
func (v *DomainValidator) getURL(i string) (*url.URL, error) {
u, err := url.Parse(i)
if !v.opts.WithScheme && (err != nil || u.Host == "") {
u, err = url.Parse("tinyauth://" + i)
}
if err != nil {
return nil, fmt.Errorf("failed to parse input url: %w", err)
}
if u.Host == "" {
return nil, ErrInvalidURL
}
if v.opts.WithPort && !v.opts.WithScheme && u.Port() == "" {
return nil, fmt.Errorf("port validation is enabled but port is missing in input url and schemes are not enabled")
if i == "" {
return nil, fmt.Errorf("url cannot be empty")
}
if v.opts.WithScheme {
// Empty scheme means that we parsed the url with the tinyauth:// placeholder
if u.Scheme == "tinyauth" {
return nil, fmt.Errorf("input url is missing scheme")
err := v.checkScheme(i)
if err != nil {
return nil, fmt.Errorf("invalid scheme: %w", err)
}
if len(v.opts.AllowedSchemes) > 0 && !slices.Contains(v.opts.AllowedSchemes, u.Scheme) {
return nil, fmt.Errorf("scheme %s not allowed", u.Scheme)
u, err := url.Parse(i)
if err != nil {
return nil, fmt.Errorf("failed to parse input url: %w", err)
}
if u.Host == "" || u.Scheme == "" {
return nil, fmt.Errorf("missing host or scheme in url: %s", i)
}
return u, nil
}
rawURL := i
if !strings.Contains(i, "://") {
// From godoc: [scheme:][//[userinfo@]host][/]path[?query][#fragment]
// So, we can omit the colon and tell the Go URL lib that we want
// to parse the URL without the scheme. If we don't do this,
// the URL lib will parse our entire domain as the path.
rawURL = "//" + i
}
u, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("failed to parse host: %w", err)
}
if u.Host == "" {
return nil, fmt.Errorf("missing host in url: %s", i)
}
return u, nil
}
func (v *DomainValidator) getEffectivePort(u *url.URL) (string, bool) {
if u.Port() != "" {
return u.Port(), true
}
switch u.Scheme {
case "http":
return "80", true
case "https":
return "443", true
default:
return "", false
}
}
func (v *DomainValidator) formatHostname(hostname string) (string, error) {
func (v *DomainValidator) getHostname(hostname string) (string, error) {
hostname = strings.ToLower(hostname)
hostname = strings.TrimSuffix(hostname, ".")
if net.ParseIP(hostname) != nil {
@@ -133,26 +152,18 @@ func (v *DomainValidator) Validate(expected, actual string) error {
}
if v.opts.WithPort {
eup, ok := v.getEffectivePort(eu)
if !ok {
return fmt.Errorf("failed to get effective port for url: %s", eu.String())
}
aup, ok := v.getEffectivePort(au)
if !ok {
return fmt.Errorf("failed to get effective port for url: %s", au.String())
}
if eup != aup {
if eu.Port() != au.Port() {
return ErrPortMismatch
}
}
euf, err := v.formatHostname(eu.Hostname())
euf, err := v.getHostname(eu.Hostname())
if err != nil {
return err
}
auf, err := v.formatHostname(au.Hostname())
auf, err := v.getHostname(au.Hostname())
if err != nil {
return err
@@ -165,7 +176,7 @@ func (v *DomainValidator) Validate(expected, actual string) error {
return nil
}
// SafeHostname uses the internal validation for domains that Validator uses
// SafeHostname uses the internal validation for domains that the validator uses
// to parse a hostname. It ensures the input URL is a valid URL, that a host
// is present and that the hostname is lowercased and without a trailing dot.
func (v *DomainValidator) SafeHostname(input string) (string, error) {
@@ -175,5 +186,5 @@ func (v *DomainValidator) SafeHostname(input string) (string, error) {
return "", err
}
return v.formatHostname(u.Hostname())
return v.getHostname(u.Hostname())
}
+39 -57
View File
@@ -20,16 +20,36 @@ func TestDomainValidator_SafeHostname(t *testing.T) {
{
description: "Empty url fails",
errorFunc: func(t *testing.T, e error) {
assert.ErrorIs(t, e, ErrInvalidURL)
assert.ErrorContains(t, e, "url cannot be empty")
},
},
{
description: "URL without host fails",
input: "/foo",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "missing host in url")
},
},
{
description: "Invalid url fails",
input: "foo:foo",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "failed to parse host")
},
},
{
description: "With scheme and invalid url should fail",
options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https"}},
input: "https://foo:foo",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "failed to parse input url")
},
},
{
description: "Scheme disabled with scheme should pass",
input: "https://example.com",
expected: "example.com",
},
{
description: "Domain without scheme should parse if scheme is disabled",
input: "example.com",
@@ -40,7 +60,7 @@ func TestDomainValidator_SafeHostname(t *testing.T) {
options: DomainValidatorOptions{WithScheme: true},
input: "example.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorIs(t, e, ErrInvalidURL)
assert.ErrorContains(t, e, "invalid scheme")
},
},
{
@@ -48,7 +68,7 @@ func TestDomainValidator_SafeHostname(t *testing.T) {
options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https"}},
input: "foo://example.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "foo not allowed")
assert.ErrorContains(t, e, "invalid scheme")
},
},
{
@@ -94,15 +114,10 @@ func TestDomainValidator_SafeHostname(t *testing.T) {
},
},
{
// Placeholder should not be used by users and is reserved for the validator.
// Using it is like not using any scheme for the validator, and thus it will fail
// with schemes enabled.
description: "Placeholder scheme supplied directly should fail",
options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https"}},
input: "tinyauth://example.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "input url is missing scheme")
},
description: "With port enabled without any port should work",
options: DomainValidatorOptions{WithPort: true},
input: "example.com",
expected: "example.com",
},
}
@@ -135,7 +150,7 @@ func TestDomainValidator_Validate(t *testing.T) {
expected: "foo:foo",
actual: "bar.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "failed to parse input url:")
assert.ErrorContains(t, e, "failed to parse host:")
},
},
{
@@ -143,7 +158,7 @@ func TestDomainValidator_Validate(t *testing.T) {
expected: "example.com",
actual: "foo:foo",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "failed to parse input url:")
assert.ErrorContains(t, e, "failed to parse host:")
},
},
{
@@ -162,58 +177,22 @@ func TestDomainValidator_Validate(t *testing.T) {
actual: "https://example.com",
},
{
description: "Port validation without ports and schemes disabled should fail",
description: "Port validation with ports enabled and empty ports should work",
options: DomainValidatorOptions{WithPort: true},
expected: "example.com",
actual: "example.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "port validation is enabled but port is missing in input url and schemes are not enabled")
},
},
{
description: "Port validation with no port and http should pass",
options: DomainValidatorOptions{WithPort: true, WithScheme: true, AllowedSchemes: []string{"http"}},
expected: "http://example.com",
actual: "http://example.com",
},
{
description: "Port validation with no port and https should pass",
options: DomainValidatorOptions{WithPort: true, WithScheme: true, AllowedSchemes: []string{"https"}},
expected: "https://example.com",
actual: "https://example.com",
},
{
description: "Port validation with port and no scheme should pass with same port",
description: "Port validation should pass with same port",
options: DomainValidatorOptions{WithPort: true},
expected: "example.com:8080",
actual: "example.com:8080",
},
{
description: "Domains with unknown scheme and port enabled but no port should fail",
options: DomainValidatorOptions{WithPort: true, WithScheme: true},
expected: "ssh://example.com:22",
actual: "ssh://example.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "failed to get effective port for url")
},
},
{
description: "Domains with unknown scheme and port enabled but no port should fail, reverse",
options: DomainValidatorOptions{WithPort: true, WithScheme: true},
expected: "ssh://example.com",
actual: "ssh://example.com:22",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "failed to get effective port for url")
},
},
{
description: "Port validation with port and no scheme should fail with different port",
description: "Port enabled with scheme and matching port should pass",
options: DomainValidatorOptions{WithPort: true},
expected: "example.com:8080",
actual: "example.com:8081",
errorFunc: func(t *testing.T, e error) {
assert.ErrorIs(t, e, ErrPortMismatch)
},
expected: "https://example.com:443",
actual: "https://example.com:443",
},
{
description: "Failure to format expected domain should fail",
@@ -244,10 +223,13 @@ func TestDomainValidator_Validate(t *testing.T) {
actual: "https://example.com",
},
{
description: "Valid domains with matching ports should pass",
description: "Valid domains with non matching ports should fail",
options: DomainValidatorOptions{WithPort: true},
expected: "example.com:8080",
actual: "example.com:8080",
actual: "example.com:8085",
errorFunc: func(t *testing.T, e error) {
assert.ErrorIs(t, e, ErrPortMismatch)
},
},
{
description: "Valid domains without ports or schemes should pass",