Compare commits

..
13 Commits
20 changed files with 699 additions and 52 deletions
+43
View File
@@ -0,0 +1,43 @@
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
@@ -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.blue.Render(secret))
fmt.Print(colors.green.Render(secret))
fmt.Print("\n\n")
fmt.Printf("Finally, add your user '%s' back to your configuration: ", user.Username)
fmt.Print(colors.blue.Render(userStr))
fmt.Print(colors.green.Render(userStr))
fmt.Print("\n")
return nil
+5 -5
View File
@@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"reflect"
"strings"
"charm.land/huh/v2"
@@ -31,11 +32,10 @@ func main() {
Configuration: tConfig,
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)
},
}
+7
View File
@@ -0,0 +1,7 @@
# playwright files
/node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
+15
View File
@@ -0,0 +1,15 @@
{
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
@@ -0,0 +1,22 @@
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
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,47 @@
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
@@ -0,0 +1,30 @@
{
"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
@@ -0,0 +1,49 @@
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
@@ -0,0 +1,338 @@
---
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
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,48 @@
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');
});
+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(search.Username)
user, err := m.auth.GetLDAPUser(username)
if err != nil {
return nil, nil, fmt.Errorf("error retrieving ldap user details: %w", err)
+17 -18
View File
@@ -101,23 +101,22 @@ 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"`
// 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"`
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"`
}
type DatabaseConfig struct {
@@ -238,7 +237,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"`
+7 -8
View File
@@ -15,7 +15,6 @@ 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"
@@ -72,9 +71,9 @@ type AuthService struct {
dummyHash string
caches struct {
login *cache.CacheStore[LoginAttempt]
oauth *cache.CacheStore[OAuthPendingSession]
ldap *cache.CacheStore[[]string]
login *CacheStore[LoginAttempt]
oauth *CacheStore[OAuthPendingSession]
ldap *CacheStore[[]string]
}
}
@@ -116,9 +115,9 @@ func NewAuthService(i AuthServiceInput) (*AuthService, error) {
service.dummyHash = string(dummyHash)
// caches setup
oauthCache := cache.NewCacheStore[OAuthPendingSession](256)
loginCache := cache.NewCacheStore[LoginAttempt](service.calculateLockdownLimit())
ldapCache := cache.NewCacheStore[[]string](1024)
oauthCache := NewCacheStore[OAuthPendingSession](256)
loginCache := NewCacheStore[LoginAttempt](service.calculateLockdownLimit())
ldapCache := NewCacheStore[[]string](1024)
service.caches.oauth = oauthCache
service.caches.login = loginCache
@@ -280,7 +279,7 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
return
}
auth.caches.login.WithLock(func(actions cache.CacheStoreActions[LoginAttempt]) {
auth.caches.login.WithLock(func(actions CacheStoreActions[LoginAttempt]) {
entry, ok := actions.Get(identifier)
if !ok {
@@ -1,4 +1,4 @@
package cache
package service
import (
"slices"
@@ -33,8 +33,8 @@ func NewCacheStore[T any](maxSize int) *CacheStore[T] {
}
}
// WithLock allows performing multiple operations on a single lock.
// The provided mutate function receives a set of actions (Set, Get, Delete, Update) that
// With lock allows performing multiple operations on the cache store atomically.
// The provided mutate function receives a set of actions (Set, Get, Delete) 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 cache
package service
import (
"strconv"
+8 -9
View File
@@ -27,7 +27,6 @@ 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"
)
@@ -159,9 +158,9 @@ type OIDCService struct {
issuer string
caches struct {
code *cache.CacheStore[AuthorizeCodeEntry]
usedCode *cache.CacheStore[UsedCodeEntry]
authorize *cache.CacheStore[AuthorizeRequest]
code *CacheStore[AuthorizeCodeEntry]
usedCode *CacheStore[UsedCodeEntry]
authorize *CacheStore[AuthorizeRequest]
}
}
@@ -340,11 +339,11 @@ func NewOIDCService(i OIDCServiceInput) (*OIDCService, error) {
i.Ding.Go(service.cleanupRoutine, ding.RingMinor)
// Create caches
codeCache := cache.NewCacheStore[AuthorizeCodeEntry](256)
usedCode := cache.NewCacheStore[UsedCodeEntry](256)
authorize := cache.NewCacheStore[AuthorizeRequest](256)
codeCash := NewCacheStore[AuthorizeCodeEntry](256)
usedCode := NewCacheStore[UsedCodeEntry](256)
authorize := NewCacheStore[AuthorizeRequest](256)
service.caches.code = codeCache
service.caches.code = codeCash
service.caches.usedCode = usedCode
service.caches.authorize = authorize
@@ -504,7 +503,7 @@ func (service *OIDCService) GetCodeEntry(codeHash string, clientId string) (*Aut
var entry AuthorizeCodeEntry
var ok bool
service.caches.code.WithLock(func(actions cache.CacheStoreActions[AuthorizeCodeEntry]) {
service.caches.code.WithLock(func(actions CacheStoreActions[AuthorizeCodeEntry]) {
entry, ok = actions.Get(codeHash)
if !ok {
+4 -5
View File
@@ -10,7 +10,6 @@ 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"
)
@@ -60,8 +59,8 @@ type TailscaleService struct {
apiToken string
caches struct {
devices *cache.CacheStore[tailscaleAPIDevices]
users *cache.CacheStore[tailscaleAPIUsers]
devices *CacheStore[tailscaleAPIDevices]
users *CacheStore[tailscaleAPIUsers]
}
urls struct {
@@ -101,8 +100,8 @@ func NewTailscaleService(i TailscaleServiceInput) (*TailscaleService, error) {
apiToken: apiToken,
}
devicesCache := cache.NewCacheStore[tailscaleAPIDevices](0)
usersCache := cache.NewCacheStore[tailscaleAPIUsers](0)
devicesCache := NewCacheStore[tailscaleAPIDevices](0)
usersCache := NewCacheStore[tailscaleAPIUsers](0)
s.caches.devices = devicesCache
s.caches.users = usersCache