Compare commits

..
Author SHA1 Message Date
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
StavrosandGitHub a7eba59a42 refactor: export cache package (#1017) 2026-07-17 02:04:34 +03:00
tsushanthandGitHub ac8703eadb fix(ldap): use DN instead of uid in basicAuth GetLDAPUser call (#1015) 2026-07-17 01:15:48 +03:00
Stavros 04a06ec5ff chore: use blue color for totp generate 2026-07-16 17:13:36 +03:00
46 changed files with 480 additions and 938 deletions
+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"
-43
View File
@@ -1,43 +0,0 @@
name: Run e2e tests
on:
workflow_dispatch:
pull_request:
branches:
- main
jobs:
test:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v7
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # ratchet:pnpm/action-setup@v6
with:
package_json_file: ./e2e/package.json
- name: Set up Docker
uses: docker/setup-docker-action@6d7cfa65f60a9dda7b46e5513fa982536f3c9877 # ratchet:docker/setup-docker-action@v5
- name: Install dependencies
run: pnpm ci
working-directory: e2e
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
working-directory: e2e
- name: Run Playwright tests
run: pnpm test
working-directory: e2e
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # ratchet:actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: e2e/playwright-report/
retention-days: 5
+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
+2 -2
View File
@@ -116,11 +116,11 @@ func generateTotpCmd() *cli.Command {
userStr := fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TOTPSecret)
fmt.Print("\nOr add the following TOTP secret to your authenticator app: ")
fmt.Print(colors.green.Render(secret))
fmt.Print(colors.blue.Render(secret))
fmt.Print("\n\n")
fmt.Printf("Finally, add your user '%s' back to your configuration: ", user.Username)
fmt.Print(colors.green.Render(userStr))
fmt.Print(colors.blue.Render(userStr))
fmt.Print("\n")
return nil
+5 -5
View File
@@ -3,7 +3,6 @@ package main
import (
"fmt"
"os"
"reflect"
"strings"
"charm.land/huh/v2"
@@ -32,10 +31,11 @@ func main() {
Configuration: tConfig,
Resources: loaders,
Run: func(_ []string) error {
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.")
}
// 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.")
//}
return runCmd(*tConfig)
},
}
-7
View File
@@ -1,7 +0,0 @@
# playwright files
/node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
-15
View File
@@ -1,15 +0,0 @@
{
auto_https off
}
http://whoami.127.0.0.1.sslip.io {
forward_auth tinyauth:3000 {
uri /api/auth/caddy
copy_headers Remote-User Remote-Name Remote-Email Remote-Groups
}
reverse_proxy whoami:80
}
http://tinyauth.127.0.0.1.sslip.io {
reverse_proxy tinyauth:3000
}
-22
View File
@@ -1,22 +0,0 @@
appUrl: http://tinyauth.127.0.0.1.sslip.io
log:
level: debug
auth:
users:
# user1:password,user2:password,user3:password:token
- user1:$2a$10$h1laww4k5a4bJcG5KwE3nO45YKSC4mOKHxbcccgxr3Y7H9zHlQe8e
- user2:$2a$10$h1laww4k5a4bJcG5KwE3nO45YKSC4mOKHxbcccgxr3Y7H9zHlQe8e
- user3:$2a$10$h1laww4k5a4bJcG5KwE3nO45YKSC4mOKHxbcccgxr3Y7H9zHlQe8e:MVR4JQWNXYKNM6HHJEYEFP2O74QIIEJE
# disable rate limits for multiple workers to work
loginMaxRetries: 0
apps:
whoami:
config:
domain: whoami.127.0.0.1.sslip.io
path:
allow: /foo
users:
allow: user1
-24
View File
@@ -1,24 +0,0 @@
services:
caddy:
image: caddy:2.11.4
pull_policy: missing
ports:
- 80:80
volumes:
- ./conf:/etc/caddy
whoami:
image: traefik/whoami:v1.11.0
pull_policy: missing
tinyauth:
build:
context: ../
dockerfile: Dockerfile
args:
- VERSION=e2e
- BUILD_TAGS=nomsgpack
- LDFLAGS=-s -w
command: ["--configfile", "/app/config.yaml"]
volumes:
- ./config.e2e.yaml:/app/config.yaml:ro
-47
View File
@@ -1,47 +0,0 @@
import {expect, Page} from '@playwright/test';
import { OTP } from 'otplib';
export class LoginFixture {
constructor(public readonly page: Page) {}
async run(username: string, password: string) {
await expect(this.page.getByText('Welcome back, please login')).toBeVisible();
await this.page.getByLabel('Username').fill(username);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Login' }).click();
}
async expectSuccess(username: string) {
await expect(this.page.getByText(`You are currently logged in as ${username}.`)).toBeVisible()
}
}
export class LogoutFixture {
constructor(public readonly page: Page) {}
async run() {
await expect(this.page.getByText('Click the button below to logout.')).toBeVisible();
await this.page.getByRole('button', { name: 'Logout' }).click();
}
async expectSuccess() {
await expect(this.page.getByText('Welcome back, please login')).toBeVisible();
}
}
export class TOTPFixture {
constructor(public readonly page: Page) {}
async run(secret: string) {
await expect(this.page.getByText('Enter your TOTP code')).toBeVisible();
const otp = new OTP();
const token = await otp.generate({ secret });
await this.page.getByPlaceholder('XXXXXX').fill(token);
// we shouldn't need to click continue, it will auto submit
// await this.page.getByRole('button', { name: 'Continue' }).click();
}
async expectSuccess(username: string) {
await expect(this.page.getByText(`You are currently logged in as ${username}.`)).toBeVisible()
}
}
-30
View File
@@ -1,30 +0,0 @@
{
"name": "e2e",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"down": "docker compose -f docker-compose.e2e.yml down",
"up": "docker compose -f docker-compose.e2e.yml up --build --force-recreate --remove-orphans",
"test": "playwright test",
"report": "playwright show-report"
},
"keywords": [],
"author": "",
"license": "ISC",
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "^11.1.2",
"onFail": "download"
}
},
"type": "module",
"devDependencies": {
"@playwright/test": "^1.61.1",
"@types/node": "^26.1.1"
},
"dependencies": {
"otplib": "^13.4.1"
}
}
-49
View File
@@ -1,49 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './specs',
fullyParallel: true,
forbidOnly: false,
retries: 0,
workers: 4,
reporter: 'html',
use: {
trace: 'on-first-retry',
video: 'on',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'pnpm run up',
url: 'http://tinyauth.127.0.0.1.sslip.io/api/healthz',
reuseExistingServer: true,
timeout: 5 * 60 * 1000,
gracefulShutdown: {
signal: 'SIGINT',
timeout: 1000,
},
stderr: 'pipe',
stdout: 'pipe',
},
});
-338
View File
@@ -1,338 +0,0 @@
---
lockfileVersion: '9.0'
importers:
.:
configDependencies: {}
packageManagerDependencies:
'@pnpm/exe':
specifier: ^11.1.2
version: 11.13.1
pnpm:
specifier: ^11.1.2
version: 11.13.1
packages:
'@pnpm/exe@11.13.1':
resolution: {integrity: sha512-P4euEK6lOFnd5oTHEc5M/HhvyF4XUhTnVsklEcM6rmY0QJxPD6xbT+u1+gskEIBp4nSRorz20IJQtAU1Nerggg==}
hasBin: true
'@pnpm/linux-arm64@11.13.1':
resolution: {integrity: sha512-wB8zloqrYrudPyuA5qbuTCnJGe4eETPwqOjoPjoyyyvA4zFI5XfLpxgqOOcaY5UJBoqzckcGpRVDwhSRfsQ/6A==}
cpu: [arm64]
os: [linux]
'@pnpm/linux-x64@11.13.1':
resolution: {integrity: sha512-A+wnEvzfWEvanXiwww3tnOPmtjPSrrf5tOP6vk8+K0BRFEe/Df0oPytm2nWgGcn5iwPnqtr1Btkof913McnSPA==}
cpu: [x64]
os: [linux]
'@pnpm/linuxstatic-arm64@11.13.1':
resolution: {integrity: sha512-k4t65VeqRX4COMFe45TF58CVmCpmAsKZShaR1HobmUeleo98mWTctggKolrA2MHcVUeSS+12yB5Urb3uDazhmw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/linuxstatic-x64@11.13.1':
resolution: {integrity: sha512-A65GqPzwCl0bAMk3kRWfbjSRBm5RRaqR2oMxV/9AYZrwO0X9yEfngbLBISCPHjt6/Qe4nH7DFemyhy6yODYwEw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/macos-arm64@11.13.1':
resolution: {integrity: sha512-MJvOtyGOWSfBoqdVEfAH8ljmHs13mt82k/UxN4f+q7koDxJRR2n4Nie6Og6RwbnbaubCz0Fh2bTeL1+MxDSFpA==}
cpu: [arm64]
os: [darwin]
'@pnpm/win-arm64@11.13.1':
resolution: {integrity: sha512-kl/g1cCKOJPe4HntspyrAJW0LRco0UHnVfxHSspezo4Zj4AanJAZ8WzLqfa6/w3lBSKHTEN4x0pb3m4J7B7Vpw==}
cpu: [arm64]
os: [win32]
'@pnpm/win-x64@11.13.1':
resolution: {integrity: sha512-Bcb14NeBlbHS2Gq1qr8VnCiAz5eC1lYzXOls7zH0bnV0Taaj4/xyfm0HVO4dn9R2TQtVtz1qnBZHHL9PDFumqQ==}
cpu: [x64]
os: [win32]
'@reflink/reflink-darwin-arm64@0.1.19':
resolution: {integrity: sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@reflink/reflink-darwin-x64@0.1.19':
resolution: {integrity: sha512-By85MSWrMZa+c26TcnAy8SDk0sTUkYlNnwknSchkhHpGXOtjNDUOxJE9oByBnGbeuIE1PiQsxDG3Ud+IVV9yuA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@reflink/reflink-linux-arm64-gnu@0.1.19':
resolution: {integrity: sha512-7P+er8+rP9iNeN+bfmccM4hTAaLP6PQJPKWSA4iSk2bNvo6KU6RyPgYeHxXmzNKzPVRcypZQTpFgstHam6maVg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@reflink/reflink-linux-arm64-musl@0.1.19':
resolution: {integrity: sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@reflink/reflink-linux-x64-gnu@0.1.19':
resolution: {integrity: sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@reflink/reflink-linux-x64-musl@0.1.19':
resolution: {integrity: sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@reflink/reflink-win32-arm64-msvc@0.1.19':
resolution: {integrity: sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@reflink/reflink-win32-x64-msvc@0.1.19':
resolution: {integrity: sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@reflink/reflink@0.1.19':
resolution: {integrity: sha512-DmCG8GzysnCZ15bres3N5AHCmwBwYgp0As6xjhQ47rAUTUXxJiK+lLUxaGsX3hd/30qUpVElh05PbGuxRPgJwA==}
engines: {node: '>= 10'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
pnpm@11.13.1:
resolution: {integrity: sha512-svx2g7imUlQU59E+G6KMqt3elr9m7FQL+ut+cCuB8+C+TR8pXt9/n+A5Z0Co3ORQnFgt33mJH0VD/qMtN2RfJQ==}
engines: {node: '>=22.13'}
hasBin: true
snapshots:
'@pnpm/exe@11.13.1':
dependencies:
'@reflink/reflink': 0.1.19
detect-libc: 2.1.2
optionalDependencies:
'@pnpm/linux-arm64': 11.13.1
'@pnpm/linux-x64': 11.13.1
'@pnpm/linuxstatic-arm64': 11.13.1
'@pnpm/linuxstatic-x64': 11.13.1
'@pnpm/macos-arm64': 11.13.1
'@pnpm/win-arm64': 11.13.1
'@pnpm/win-x64': 11.13.1
'@pnpm/linux-arm64@11.13.1':
optional: true
'@pnpm/linux-x64@11.13.1':
optional: true
'@pnpm/linuxstatic-arm64@11.13.1':
optional: true
'@pnpm/linuxstatic-x64@11.13.1':
optional: true
'@pnpm/macos-arm64@11.13.1':
optional: true
'@pnpm/win-arm64@11.13.1':
optional: true
'@pnpm/win-x64@11.13.1':
optional: true
'@reflink/reflink-darwin-arm64@0.1.19':
optional: true
'@reflink/reflink-darwin-x64@0.1.19':
optional: true
'@reflink/reflink-linux-arm64-gnu@0.1.19':
optional: true
'@reflink/reflink-linux-arm64-musl@0.1.19':
optional: true
'@reflink/reflink-linux-x64-gnu@0.1.19':
optional: true
'@reflink/reflink-linux-x64-musl@0.1.19':
optional: true
'@reflink/reflink-win32-arm64-msvc@0.1.19':
optional: true
'@reflink/reflink-win32-x64-msvc@0.1.19':
optional: true
'@reflink/reflink@0.1.19':
optionalDependencies:
'@reflink/reflink-darwin-arm64': 0.1.19
'@reflink/reflink-darwin-x64': 0.1.19
'@reflink/reflink-linux-arm64-gnu': 0.1.19
'@reflink/reflink-linux-arm64-musl': 0.1.19
'@reflink/reflink-linux-x64-gnu': 0.1.19
'@reflink/reflink-linux-x64-musl': 0.1.19
'@reflink/reflink-win32-arm64-msvc': 0.1.19
'@reflink/reflink-win32-x64-msvc': 0.1.19
detect-libc@2.1.2: {}
pnpm@11.13.1: {}
---
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
otplib:
specifier: ^13.4.1
version: 13.4.1
devDependencies:
'@playwright/test':
specifier: ^1.61.1
version: 1.61.1
'@types/node':
specifier: ^26.1.1
version: 26.1.1
packages:
'@noble/hashes@2.2.0':
resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==}
engines: {node: '>= 20.19.0'}
'@otplib/core@13.4.1':
resolution: {integrity: sha512-KIXgK1hNtWJEBMTastbe1bpmuais+3f+ATeO8TkMs2rNkfGO1FbQy8+/UWVEu3TR/iTJerU0idkPudaPmLP2BA==}
'@otplib/hotp@13.4.1':
resolution: {integrity: sha512-g9q04SwpG5ZtMnVkUcgcoAlwCH4YLROZN1qhyBwgkBzqYYVSYhpP6gSGaxGHwePLt1c+e6NqDlgIZN+e1/XPuA==}
'@otplib/plugin-base32-scure@13.4.1':
resolution: {integrity: sha512-Fs/r5qisC05SRhT6xWXaypB6PVC0vgWf6zztmi0J5RnQ09OJiPDWCJFH6cDm6ANsrdvB9di7X+Jb7L13BoEbUA==}
'@otplib/plugin-crypto-noble@13.4.1':
resolution: {integrity: sha512-PJfVW8/1hdS6CfxLheKPZSLTwDq4TijZbN4yRjxlv0ODdzmxpM+wGwWr1JXMdy0xJPxLziydQD5gdVqrR4/gAg==}
'@otplib/totp@13.4.1':
resolution: {integrity: sha512-QOkBVPrf6AM4qZaReZPSk9/I8ATVdZpIISJz115MqeVtcrbcr5llPZ0J7804tpnjnp1vCRkI5Qjd47HhgVteBQ==}
'@otplib/uri@13.4.1':
resolution: {integrity: sha512-xaIm7bvICMhoB2rZIR5luiaMdssWR5nY5nXnR1fdezUgZuEO58D6zrGzLp7pQuBmlpmL0HagnscDQFoskp9yiA==}
'@playwright/test@1.61.1':
resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==}
engines: {node: '>=18'}
hasBin: true
'@scure/base@2.2.0':
resolution: {integrity: sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==}
'@types/node@26.1.1':
resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==}
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
otplib@13.4.1:
resolution: {integrity: sha512-o5CxfDw6bh7hoDv0NUUIcc0RqzJ9ipfUrzeKheKJ+vs4rXZnDlA9n4a/7R1cDjpmLjKLix4BgNVRmoDkm5rLSQ==}
playwright-core@1.61.1:
resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==}
engines: {node: '>=18'}
hasBin: true
playwright@1.61.1:
resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==}
engines: {node: '>=18'}
hasBin: true
undici-types@8.3.0:
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
snapshots:
'@noble/hashes@2.2.0': {}
'@otplib/core@13.4.1': {}
'@otplib/hotp@13.4.1':
dependencies:
'@otplib/core': 13.4.1
'@otplib/uri': 13.4.1
'@otplib/plugin-base32-scure@13.4.1':
dependencies:
'@otplib/core': 13.4.1
'@scure/base': 2.2.0
'@otplib/plugin-crypto-noble@13.4.1':
dependencies:
'@noble/hashes': 2.2.0
'@otplib/core': 13.4.1
'@otplib/totp@13.4.1':
dependencies:
'@otplib/core': 13.4.1
'@otplib/hotp': 13.4.1
'@otplib/uri': 13.4.1
'@otplib/uri@13.4.1':
dependencies:
'@otplib/core': 13.4.1
'@playwright/test@1.61.1':
dependencies:
playwright: 1.61.1
'@scure/base@2.2.0': {}
'@types/node@26.1.1':
dependencies:
undici-types: 8.3.0
fsevents@2.3.2:
optional: true
otplib@13.4.1:
dependencies:
'@otplib/core': 13.4.1
'@otplib/hotp': 13.4.1
'@otplib/plugin-base32-scure': 13.4.1
'@otplib/plugin-crypto-noble': 13.4.1
'@otplib/totp': 13.4.1
'@otplib/uri': 13.4.1
playwright-core@1.61.1: {}
playwright@1.61.1:
dependencies:
playwright-core: 1.61.1
optionalDependencies:
fsevents: 2.3.2
undici-types@8.3.0: {}
-28
View File
@@ -1,28 +0,0 @@
import {expect, test} from '@playwright/test';
import {LoginFixture} from "../fixtures/auth.fixtures";
test('should be able to login to app with forward-auth', async ({ page }) => {
const loginFixture = new LoginFixture(page);
await page.goto('http://whoami.127.0.0.1.sslip.io');
// redirect to tinyauth
await expect(page.getByText('Welcome back, please login')).toBeVisible()
await loginFixture.run('user1', 'password')
// redirect to app
await expect(page.getByText('whoami.127.0.0.1.sslip.io')).toBeVisible()
});
test('non authorized user should not be able to access app', async ({ page }) => {
const loginFixture = new LoginFixture(page);
await page.goto('http://whoami.127.0.0.1.sslip.io');
// redirect to tinyauth
await expect(page.getByText('Welcome back, please login')).toBeVisible()
// user2 is not authorized to access app
await loginFixture.run('user2', 'password')
// redirect to app
await expect(page.getByText('The user with username user2 is not authorized to access the resource whoami.')).toBeVisible()
})
test('allowed path should skip authentication', async ({ page }) => {
await page.goto('http://whoami.127.0.0.1.sslip.io/foo');
await expect(page.getByText('whoami.127.0.0.1.sslip.io')).toBeVisible()
})
-48
View File
@@ -1,48 +0,0 @@
import { test, expect } from '@playwright/test';
import {LoginFixture, LogoutFixture, TOTPFixture} from "../fixtures/auth.fixtures";
test('should be able to login', async ({ page }) => {
const loginFixture = new LoginFixture(page);
await page.goto('http://tinyauth.127.0.0.1.sslip.io');
await loginFixture.run('user1', 'password')
await loginFixture.expectSuccess('user1')
});
test('should fail to login with wrong credentials', async ({ page }) => {
const loginFixture = new LoginFixture(page);
await page.goto('http://tinyauth.127.0.0.1.sslip.io');
await loginFixture.run('user27267', 'password')
const toast = page.locator('[data-sonner-toast]').first();
await expect(toast).toBeVisible();
await expect(toast).toContainText('Failed to log in');
});
test('should be able to logout', async ({ page }) => {
const loginFixture = new LoginFixture(page);
await page.goto('http://tinyauth.127.0.0.1.sslip.io');
await loginFixture.run('user1', 'password')
await loginFixture.expectSuccess('user1')
const logoutFixture = new LogoutFixture(page);
await logoutFixture.run()
})
test('should be able to login with totp', async ({ page }) => {
const loginFixture = new LoginFixture(page);
const totpFixture = new TOTPFixture(page);
await page.goto('http://tinyauth.127.0.0.1.sslip.io');
await loginFixture.run('user3', 'password')
await totpFixture.run('MVR4JQWNXYKNM6HHJEYEFP2O74QIIEJE')
await loginFixture.expectSuccess('user3');
});
test('should fail to login with wrong totp', async ({ page }) => {
const loginFixture = new LoginFixture(page);
const totpFixture = new TOTPFixture(page);
await page.goto('http://tinyauth.127.0.0.1.sslip.io');
await loginFixture.run('user3', 'password')
await totpFixture.run('VZVMOMQCBN24DJ5VRFAL5TJAZGBHXMN3')
const toast = page.locator('[data-sonner-toast]').first();
await expect(toast).toBeVisible();
await expect(toast).toContainText('Failed to verify code');
});
+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";
}
};
+20 -7
View File
@@ -88,13 +88,29 @@ const getEffectivePort = (url: URL): string => {
return "80";
};
// https://www.geeksforgeeks.org/javascript/how-to-check-if-a-string-is-a-valid-ip-address-format-in-javascript
const isIP = (str: string): boolean => {
const ipv4 =
/^(\d{1,3}\.){3}\d{1,3}$/;
const ipv6 =
/^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
return ipv4.test(str) || ipv6.test(str) || str.startsWith("[");
}
const trimPeriod = (str: string): string => {
if(str.lastIndexOf('.') === (str.length - 1)){
str = str.substring(0, str.length - 1);
}
return str
}
export const isTrustedDomain = (
url: URL,
appUrl: URL,
cookieDomain: string,
subdomainsEnabled: boolean,
): boolean => {
if (url.protocol != appUrl.protocol) {
if (isIP(url.hostname)) {
return false;
}
@@ -102,7 +118,7 @@ export const isTrustedDomain = (
return false;
}
if (url.hostname == appUrl.hostname) {
if (trimPeriod(url.hostname) == trimPeriod(appUrl.hostname)) {
return true;
}
@@ -110,9 +126,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 =
+9 -3
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";
@@ -32,7 +32,13 @@ export const ContinuePage = () => {
const screenParams = useScreenParams(searchParams);
const redirectUri = screenParams.redirect_uri;
const isAppLogin = screenParams.login_for === "app";
const recompiledParams = recompileScreenParams(screenParams);
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>
+9 -6
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,13 +63,16 @@ 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(
+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
+3 -4
View File
@@ -313,8 +313,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)
@@ -333,7 +332,6 @@ 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) {
return false
}
@@ -351,7 +349,8 @@ 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
}
@@ -81,14 +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",
@@ -121,14 +113,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",
+1 -1
View File
@@ -282,7 +282,7 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model.
}
userContext.Provider = model.ProviderLocal
case model.UserLDAP:
user, err := m.auth.GetLDAPUser(username)
user, err := m.auth.GetLDAPUser(search.Username)
if err != nil {
return nil, nil, fmt.Errorf("error retrieving ldap user details: %w", err)
+18 -17
View File
@@ -101,22 +101,23 @@ func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config {
}
type Config struct {
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl,omitempty"`
ConfigFile string `description:"Path to config file." yaml:"-" gen:"include"`
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider,omitempty"`
Database DatabaseConfig `description:"Database configuration." yaml:"database,omitempty"`
Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics,omitempty"`
Resources ResourcesConfig `description:"Resources configuration." yaml:"resources,omitempty"`
Server ServerConfig `description:"Server configuration." yaml:"server,omitempty"`
Auth AuthConfig `description:"Authentication configuration." yaml:"auth,omitempty"`
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps,omitempty"`
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth,omitempty"`
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc,omitempty"`
UI UIConfig `description:"UI customization." yaml:"ui,omitempty"`
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,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"`
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl,omitempty"`
ConfigFile string `description:"Path to config file." yaml:"-" gen:"include"`
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider,omitempty"`
Database DatabaseConfig `description:"Database configuration." yaml:"database,omitempty"`
Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics,omitempty"`
Resources ResourcesConfig `description:"Resources configuration." yaml:"resources,omitempty"`
Server ServerConfig `description:"Server configuration." yaml:"server,omitempty"`
Auth AuthConfig `description:"Authentication configuration." yaml:"auth,omitempty"`
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps,omitempty"`
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth,omitempty"`
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc,omitempty"`
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"`
}
type DatabaseConfig struct {
@@ -237,7 +238,7 @@ type LogStreamConfig struct {
Level string `description:"Log level for this stream. Use global if empty." yaml:"level,omitempty"`
}
type ExperimentalConfig struct{}
//type ExperimentalConfig struct{}
type TailscaleConfig struct {
Enabled bool `description:"Enable Tailscale integration." yaml:"enabled,omitempty"`
+11 -7
View File
@@ -44,13 +44,17 @@ 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) &&
!errors.Is(err, validators.ErrPortMismatch) &&
!errors.Is(err, validators.ErrSchemeMismatch) {
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")
+8 -7
View File
@@ -15,6 +15,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"github.com/tinyauthapp/tinyauth/pkg/cache"
"go.uber.org/dig"
"github.com/google/uuid"
@@ -71,9 +72,9 @@ type AuthService struct {
dummyHash string
caches struct {
login *CacheStore[LoginAttempt]
oauth *CacheStore[OAuthPendingSession]
ldap *CacheStore[[]string]
login *cache.CacheStore[LoginAttempt]
oauth *cache.CacheStore[OAuthPendingSession]
ldap *cache.CacheStore[[]string]
}
}
@@ -115,9 +116,9 @@ func NewAuthService(i AuthServiceInput) (*AuthService, error) {
service.dummyHash = string(dummyHash)
// caches setup
oauthCache := NewCacheStore[OAuthPendingSession](256)
loginCache := NewCacheStore[LoginAttempt](service.calculateLockdownLimit())
ldapCache := NewCacheStore[[]string](1024)
oauthCache := cache.NewCacheStore[OAuthPendingSession](256)
loginCache := cache.NewCacheStore[LoginAttempt](service.calculateLockdownLimit())
ldapCache := cache.NewCacheStore[[]string](1024)
service.caches.oauth = oauthCache
service.caches.login = loginCache
@@ -279,7 +280,7 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
return
}
auth.caches.login.WithLock(func(actions CacheStoreActions[LoginAttempt]) {
auth.caches.login.WithLock(func(actions cache.CacheStoreActions[LoginAttempt]) {
entry, ok := actions.Get(identifier)
if !ok {
+9 -8
View File
@@ -27,6 +27,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"github.com/tinyauthapp/tinyauth/pkg/cache"
"go.uber.org/dig"
)
@@ -158,9 +159,9 @@ type OIDCService struct {
issuer string
caches struct {
code *CacheStore[AuthorizeCodeEntry]
usedCode *CacheStore[UsedCodeEntry]
authorize *CacheStore[AuthorizeRequest]
code *cache.CacheStore[AuthorizeCodeEntry]
usedCode *cache.CacheStore[UsedCodeEntry]
authorize *cache.CacheStore[AuthorizeRequest]
}
}
@@ -339,11 +340,11 @@ func NewOIDCService(i OIDCServiceInput) (*OIDCService, error) {
i.Ding.Go(service.cleanupRoutine, ding.RingMinor)
// Create caches
codeCash := NewCacheStore[AuthorizeCodeEntry](256)
usedCode := NewCacheStore[UsedCodeEntry](256)
authorize := NewCacheStore[AuthorizeRequest](256)
codeCache := cache.NewCacheStore[AuthorizeCodeEntry](256)
usedCode := cache.NewCacheStore[UsedCodeEntry](256)
authorize := cache.NewCacheStore[AuthorizeRequest](256)
service.caches.code = codeCash
service.caches.code = codeCache
service.caches.usedCode = usedCode
service.caches.authorize = authorize
@@ -503,7 +504,7 @@ func (service *OIDCService) GetCodeEntry(codeHash string, clientId string) (*Aut
var entry AuthorizeCodeEntry
var ok bool
service.caches.code.WithLock(func(actions CacheStoreActions[AuthorizeCodeEntry]) {
service.caches.code.WithLock(func(actions cache.CacheStoreActions[AuthorizeCodeEntry]) {
entry, ok = actions.Get(codeHash)
if !ok {
+5 -4
View File
@@ -10,6 +10,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"github.com/tinyauthapp/tinyauth/pkg/cache"
"go.uber.org/dig"
)
@@ -59,8 +60,8 @@ type TailscaleService struct {
apiToken string
caches struct {
devices *CacheStore[tailscaleAPIDevices]
users *CacheStore[tailscaleAPIUsers]
devices *cache.CacheStore[tailscaleAPIDevices]
users *cache.CacheStore[tailscaleAPIUsers]
}
urls struct {
@@ -100,8 +101,8 @@ func NewTailscaleService(i TailscaleServiceInput) (*TailscaleService, error) {
apiToken: apiToken,
}
devicesCache := NewCacheStore[tailscaleAPIDevices](0)
usersCache := NewCacheStore[tailscaleAPIUsers](0)
devicesCache := cache.NewCacheStore[tailscaleAPIDevices](0)
usersCache := cache.NewCacheStore[tailscaleAPIUsers](0)
s.caches.devices = devicesCache
s.caches.users = usersCache
+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"
@@ -1,4 +1,4 @@
package service
package cache
import (
"slices"
@@ -33,8 +33,8 @@ func NewCacheStore[T any](maxSize int) *CacheStore[T] {
}
}
// With lock allows performing multiple operations on the cache store atomically.
// The provided mutate function receives a set of actions (Set, Get, Delete) that
// WithLock allows performing multiple operations on a single lock.
// The provided mutate function receives a set of actions (Set, Get, Delete, Update) that
// can be used to manipulate the cache store within the locked context.
func (cs *CacheStore[T]) WithLock(mutate func(actions CacheStoreActions[T])) {
cs.mu.Lock()
@@ -1,4 +1,4 @@
package service
package cache
import (
"strconv"
+1 -1
View File
@@ -63,7 +63,7 @@ func (v *DomainValidator) getURL(i string) (*url.URL, error) {
return nil, ErrInvalidURL
}
if v.opts.WithPort && !v.opts.WithScheme && u.Port() == "" {
if v.opts.WithPort && u.Port() == "" && (u.Scheme != "http" && u.Scheme != "https") {
return nil, fmt.Errorf("port validation is enabled but port is missing in input url and schemes are not enabled")
}
+32 -2
View File
@@ -104,6 +104,24 @@ func TestDomainValidator_SafeHostname(t *testing.T) {
assert.ErrorContains(t, e, "input url is missing scheme")
},
},
{
description: "With port enabled but without scheme and url with https should work",
options: DomainValidatorOptions{WithPort: true},
input: "https://example.com",
expected: "example.com",
},
{
description: "With port enabled but without scheme and url with http should work",
options: DomainValidatorOptions{WithPort: true},
input: "http://example.com",
expected: "example.com",
},
{
description: "With port enabled but without scheme and url with port should work",
options: DomainValidatorOptions{WithPort: true},
input: "example.com:8080",
expected: "example.com",
},
}
for _, test := range tests {
@@ -194,7 +212,7 @@ func TestDomainValidator_Validate(t *testing.T) {
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")
assert.ErrorContains(t, e, "port validation is enabled but port is missing in input url and schemes are not enabled")
},
},
{
@@ -203,9 +221,21 @@ func TestDomainValidator_Validate(t *testing.T) {
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")
assert.ErrorContains(t, e, "port validation is enabled but port is missing in input url and schemes are not enabled")
},
},
{
description: "Port enabled but no scheme and https scheme should pass",
options: DomainValidatorOptions{WithPort: true},
expected: "https://example.com",
actual: "https://example.com",
},
{
description: "Port enabled but no scheme and http scheme should pass",
options: DomainValidatorOptions{WithPort: true},
expected: "http://example.com",
actual: "http://example.com",
},
{
description: "Port validation with port and no scheme should fail with different port",
options: DomainValidatorOptions{WithPort: true},