e2e: refactor oidc server from dex to tinyauth (#754)
This commit is contained in:
parent
f11e3b7f82
commit
ad8a83e60e
9 changed files with 168 additions and 85 deletions
14
.github/workflows/e2e.yml
vendored
14
.github/workflows/e2e.yml
vendored
|
|
@ -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
2
.gitignore
vendored
|
|
@ -29,6 +29,8 @@ node_modules/
|
|||
/playwright/.cache/
|
||||
/playwright/.auth/
|
||||
/playwright/*.pass
|
||||
/playwright/tinyauth/app-data/
|
||||
/playwright/tinyauth/caddy-data/
|
||||
|
||||
playwright/.auth
|
||||
playwright/temp
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
45
playwright/tinyauth/caddy.json
Normal file
45
playwright/tinyauth/caddy.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
playwright/tinyauth/users.txt
Normal file
5
playwright/tinyauth/users.txt
Normal 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
|
||||
Loading…
Reference in a new issue