e2e: refactor oidc server from dex to tinyauth (#754)

This commit is contained in:
Nico 2026-04-06 17:48:37 +02:00 committed by GitHub
parent f11e3b7f82
commit ad8a83e60e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 168 additions and 85 deletions

View file

@ -46,12 +46,14 @@ jobs:
mkdir -p data
mkdir -p playwright/temp
mkdir -p playwright/data
mkdir -p playwright/tinyauth/app-data
mkdir -p playwright/tinyauth/caddy-data
chmod -R 777 playwright/temp
touch .env.local
echo "SERVER_IP=localhost" >> .env.local
echo "ZEROBYTE_DATABASE_URL=./data/zerobyte.db" >> .env.local
echo "BASE_URL=http://localhost:4096" >> .env.local
echo "E2E_DEX_ORIGIN=http://dex:5557" >> .env.local
echo "E2E_TINYAUTH_ORIGIN=https://tinyauth.example.com:5557" >> .env.local
echo "TZ=Europe/Zurich" >> .env.local
- name: Start zerobyte-e2e service
@ -68,13 +70,13 @@ jobs:
run: |
timeout 30s bash -c 'until curl -f http://localhost:4096/api/healthcheck; do echo "Waiting for server..." && sleep 2; done'
- name: Wait for Dex to be ready
- name: Wait for Tinyauth to be ready
run: |
timeout 30s bash -c 'until curl -sf http://localhost:5557/dex/.well-known/openid-configuration; do echo "Waiting for Dex..." && sleep 2; done'
timeout 30s bash -c 'until curl -skf https://localhost:5557/.well-known/openid-configuration; do echo "Waiting for Tinyauth..." && sleep 2; done'
- name: Print docker logs if failed to start
if: failure()
run: docker compose logs zerobyte-e2e dex || true
run: docker compose logs zerobyte-e2e tinyauth tinyauth-app || true
- name: Make playwright directory writable
run: sudo chmod 777 playwright/data/zerobyte.db
@ -85,9 +87,9 @@ jobs:
- name: Dump container logs in playwright-report folder
if: always()
run: |
tree playwright
tree playwright || true
docker compose logs zerobyte-e2e > playwright-report/container-logs.txt || true
docker compose logs dex > playwright-report/dex-logs.txt || true
docker compose logs tinyauth tinyauth-app > playwright-report/tinyauth-logs.txt || true
- name: Debug - print content of /test-data in container
if: failure()

2
.gitignore vendored
View file

@ -29,6 +29,8 @@ node_modules/
/playwright/.cache/
/playwright/.auth/
/playwright/*.pass
/playwright/tinyauth/app-data/
/playwright/tinyauth/caddy-data/
playwright/.auth
playwright/temp

View file

@ -61,7 +61,9 @@ services:
- DISABLE_RATE_LIMITING=true
- APP_SECRET=94bad4678ce84a60b9789bd2114a6bf780aeb38df426f7352c941c66e25d5c2b
- BASE_URL=http://localhost:4096
- TRUSTED_ORIGINS=http://dex:5557,http://localhost:5557
- TRUSTED_ORIGINS=https://tinyauth.example.com:5557,https://localhost:5557
- NODE_EXTRA_CA_CERTS=/tinyauth-ca/caddy/pki/authorities/local/root.crt
- SSL_CERT_FILE=/tinyauth-ca/caddy/pki/authorities/local/root.crt
devices:
- /dev/fuse:/dev/fuse
cap_add:
@ -69,17 +71,56 @@ services:
ports:
- "4096:4096"
depends_on:
- dex
tinyauth:
condition: service_healthy
volumes:
- /etc/localtime:/etc/localtime:ro
- ./playwright/data:/var/lib/zerobyte/data
- ./playwright/temp:/test-data
- ./playwright/tinyauth/caddy-data:/tinyauth-ca:ro
dex:
image: ghcr.io/dexidp/dex:latest
tinyauth-app:
image: ghcr.io/steveiliop56/tinyauth:v5
restart: unless-stopped
environment:
- TINYAUTH_APPURL=https://tinyauth.example.com:5557
- TINYAUTH_SERVER_PORT=3000
- TINYAUTH_SERVER_ADDRESS=0.0.0.0
- TINYAUTH_DATABASE_PATH=/data/tinyauth.db
- TINYAUTH_AUTH_USERSFILE=/config/users.txt
- TINYAUTH_OIDC_PRIVATEKEYPATH=/data/tinyauth_oidc_key
- TINYAUTH_OIDC_PUBLICKEYPATH=/data/tinyauth_oidc_key.pub
- TINYAUTH_OIDC_CLIENTS_ZEROBYTE_CLIENTID=zerobyte-test
- TINYAUTH_OIDC_CLIENTS_ZEROBYTE_CLIENTSECRET=test-secret-12345
- TINYAUTH_OIDC_CLIENTS_ZEROBYTE_TRUSTEDREDIRECTURIS=http://localhost:4096/api/auth/sso/callback/test-oidc-register,http://localhost:4096/api/auth/sso/callback/test-oidc-uninvited,http://localhost:4096/api/auth/sso/callback/test-oidc-invited,http://localhost:4096/api/auth/sso/callback/test-oidc-autolink
- TINYAUTH_OIDC_CLIENTS_ZEROBYTE_NAME=Zerobyte Test
- TINYAUTH_LOG_LEVEL=debug
- TINYAUTH_LOG_JSON=true
volumes:
- ./playwright/tinyauth/users.txt:/config/users.txt:ro
- ./playwright/tinyauth/app-data:/data
tinyauth:
image: caddy:2-alpine
restart: unless-stopped
depends_on:
- tinyauth-app
ports:
- "5557:5557"
volumes:
- ./playwright/dex-config.yaml:/etc/dex/cfg.yaml:ro
command: dex serve /etc/dex/cfg.yaml
- ./playwright/tinyauth/caddy.json:/etc/caddy/caddy.json:ro
- ./playwright/tinyauth/caddy-data:/data
command: caddy run --config /etc/caddy/caddy.json
healthcheck:
test:
[
"CMD-SHELL",
"test -f /data/caddy/pki/authorities/local/root.crt && wget -qO- --no-check-certificate https://localhost:5557/.well-known/openid-configuration >/dev/null",
]
interval: 1s
timeout: 5s
retries: 30
networks:
default:
aliases:
- tinyauth.example.com

View file

@ -4,10 +4,11 @@ import { expect, test } from "./test";
import { trackBrowserErrors } from "./helpers/browser-errors";
import { gotoAndWaitForAppReady, waitForAppReady } from "./helpers/page";
const dexOrigin = process.env.E2E_DEX_ORIGIN ?? "http://dex:5557";
const issuer = `${dexOrigin}/dex`;
const tinyauthOrigin = process.env.E2E_TINYAUTH_ORIGIN ?? "https://tinyauth.example.com:5557";
const issuer = tinyauthOrigin;
const discoveryEndpoint = `${issuer}/.well-known/openid-configuration`;
const appBaseUrl = `http://${process.env.SERVER_IP ?? "localhost"}:4096`;
const appOrigin = new URL(appBaseUrl).origin;
const setupAuthFile = path.join(process.cwd(), "playwright", ".auth", "user.json");
const providerIds = {
@ -17,7 +18,7 @@ const providerIds = {
autoLink: "test-oidc-autolink",
} as const;
const dexPassword = "password";
const tinyauthPassword = "password";
const uninvitedUserEmail = "admin@example.com";
const invitedUserEmail = "user@example.com";
const autoLinkUninvitedLocalEmail = "linkguard@example.com";
@ -49,6 +50,8 @@ type SsoSignInResponse = {
url?: string;
};
type OidcUiState = "app" | "authorize" | "loading" | "login";
async function openSsoSettings(page: Page) {
await gotoAndWaitForAppReady(page, "/settings?tab=organization");
await expect(page.getByText("Single Sign-On")).toBeVisible();
@ -98,7 +101,7 @@ async function createLocalUser(browser: Browser, email: string, username: string
},
data: {
email,
password: dexPassword,
password: tinyauthPassword,
name: "SSO Link Target",
role: "user",
data: {
@ -206,10 +209,45 @@ async function startSsoLogin(page: Page, providerId: string) {
return body.url;
}
async function getOidcUiState(page: Page) {
const currentUrl = new URL(page.url());
if (currentUrl.origin === appOrigin) {
return "app" satisfies OidcUiState;
}
const tinyauthLoginInput = page.locator('input[name="username"]');
if (await tinyauthLoginInput.isVisible().catch(() => false)) {
return "login" satisfies OidcUiState;
}
const authorizeButton = page.getByRole("button", { name: /authorize|allow/i });
if (await authorizeButton.isVisible().catch(() => false)) {
return "authorize" satisfies OidcUiState;
}
return "loading" satisfies OidcUiState;
}
async function waitForOidcUiState(page: Page, expectedStates: OidcUiState[], timeout = 15000) {
const expectedPattern = new RegExp(`^(?:${expectedStates.join("|")})$`);
await expect
.poll(
async () => {
return getOidcUiState(page);
},
{ timeout },
)
.toMatch(expectedPattern);
return getOidcUiState(page);
}
async function withOidcLoginAttempt(
browser: Browser,
providerId: string,
dexLogin: string,
tinyauthLogin: string,
assertions: (page: Page) => Promise<void>,
) {
const context = await browser.newContext({
@ -229,13 +267,19 @@ async function withOidcLoginAttempt(
const ssoUrl = await startSsoLogin(page, providerId);
await page.goto(ssoUrl);
const dexLoginInput = page.locator('input[name="login"]');
const dexLoginIsVisible = await dexLoginInput.isVisible({ timeout: 5000 }).catch(() => false);
let oidcUiState = await waitForOidcUiState(page, ["app", "authorize", "login"]);
if (dexLoginIsVisible) {
await dexLoginInput.fill(dexLogin);
await page.locator('input[name="password"]').fill(dexPassword);
if (oidcUiState === "login") {
const tinyauthLoginInput = page.locator('input[name="username"]');
await tinyauthLoginInput.fill(tinyauthLogin);
await page.locator('input[name="password"]').fill(tinyauthPassword);
await page.locator('button[type="submit"]').click();
oidcUiState = await waitForOidcUiState(page, ["app", "authorize"]);
}
if (oidcUiState === "authorize") {
await page.getByRole("button", { name: /authorize|allow/i }).click();
}
await assertions(page);
@ -246,12 +290,13 @@ async function withOidcLoginAttempt(
}
function isLoginPath(url: string): boolean {
const pathname = new URL(url).pathname;
return pathname === "/login" || pathname === "/login/error";
const parsedUrl = new URL(url);
return parsedUrl.origin === appOrigin && (parsedUrl.pathname === "/login" || parsedUrl.pathname === "/login/error");
}
function isSsoCallbackPath(url: string): boolean {
return new URL(url).pathname.startsWith("/api/auth/sso/callback/");
const parsedUrl = new URL(url);
return parsedUrl.origin === appOrigin && parsedUrl.pathname.startsWith("/api/auth/sso/callback/");
}
async function expectInviteOnlyLoginError(page: Page) {

View file

@ -17,7 +17,7 @@
"tsc": "tsc --noEmit && turbo run tsc",
"start:dev": "docker compose down && docker compose up --build zerobyte-dev",
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
"start:e2e": "docker compose down && rm -rf playwright/data/* playwright/.auth/user.json playwright/restic.pass && mkdir -p playwright/temp && rm -rf playwright/temp/* && docker compose up --build zerobyte-e2e",
"start:e2e": "docker compose down && mkdir -p playwright/data playwright/temp playwright/tinyauth/app-data playwright/tinyauth/caddy-data && rm -rf playwright/data/* playwright/.auth/user.json playwright/restic.pass playwright/temp/* playwright/tinyauth/app-data/* && docker compose up --build zerobyte-e2e",
"start:distroless": "docker compose down && docker compose up --build zerobyte-distroless",
"gen:api-client": "dotenv -e .env.local -- openapi-ts",
"gen:migrations": "dotenv -e .env.local -- drizzle-kit generate",

View file

@ -12,6 +12,7 @@ export default defineConfig({
baseURL: `http://${process.env.SERVER_IP}:4096`,
video: "retain-on-failure",
trace: "retain-on-failure",
ignoreHTTPSErrors: true,
},
projects: [
{
@ -24,7 +25,7 @@ export default defineConfig({
use: {
...devices["Desktop Chrome"],
launchOptions: {
args: ["--host-rules=MAP dex 127.0.0.1"],
args: ["--host-rules=MAP tinyauth.example.com 127.0.0.1"],
},
},
dependencies: ["setup"],

View file

@ -1,58 +0,0 @@
issuer: http://dex:5557/dex
storage:
type: sqlite3
config:
file: /tmp/dex.db
web:
http: 0.0.0.0:5557
enablePasswordDB: true
staticPasswords:
- email: "admin@example.com"
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
username: "admin"
userID: "001"
- email: "user@example.com"
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
username: "user"
userID: "002"
- email: "test@example.com"
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
username: "test"
userID: "003"
- email: "test@test.com"
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
username: "test-local"
userID: "004"
- email: "linkguard@example.com"
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
username: "linkguard"
userID: "005"
staticClients:
- id: zerobyte-test
name: Zerobyte Test
redirectURIs:
- "http://localhost:3000/api/auth/sso/callback/credential"
- "http://localhost:3000/api/auth/sso/callback/dex"
- "http://localhost:4096/api/auth/sso/callback/test-oidc"
- "http://localhost:4096/api/auth/sso/callback/test-oidc-register"
- "http://localhost:4096/api/auth/sso/callback/test-oidc-uninvited"
- "http://localhost:4096/api/auth/sso/callback/test-oidc-invited"
- "http://localhost:4096/api/auth/sso/callback/test-oidc-autolink"
secret: test-secret-12345
oauth2:
skipApprovalScreen: true
logger:
level: debug
format: json
# Issuer URL: http://dex:5557/dex
# Discovery URL: http://dex:5557/dex/.well-known/openid-configuration
# Client ID: zerobyte-test
# Client Secret: test-secret-12345

View file

@ -0,0 +1,45 @@
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [":5557"],
"routes": [
{
"match": [
{
"host": ["tinyauth.example.com", "localhost"]
}
],
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "tinyauth-app:3000"
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": ["tinyauth.example.com", "localhost"],
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
}

View file

@ -0,0 +1,5 @@
admin@example.com:$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W
user@example.com:$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W
test@example.com:$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W
test@test.com:$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W
linkguard@example.com:$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W