From 88e89c02fc7b71f7b661fe2fe5e946d4ff192278 Mon Sep 17 00:00:00 2001 From: Til Wegener <38760774+tilwegener@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:10:41 +0200 Subject: [PATCH 1/5] Refactor OIDC callback handling and remove manual exchange --- frontend/src/routes/callback.tsx | 184 +++++++------------------------ 1 file changed, 37 insertions(+), 147 deletions(-) diff --git a/frontend/src/routes/callback.tsx b/frontend/src/routes/callback.tsx index ebd74478..73d1aa91 100644 --- a/frontend/src/routes/callback.tsx +++ b/frontend/src/routes/callback.tsx @@ -1,173 +1,55 @@ /** * OIDC Callback Route * - * Handles the redirect from the OIDC provider after authentication. - * Exchanges authorization code for tokens and redirects to the original destination. - * - * Reference: decision-009 - OIDC Authentication Implementation + * The actual authorization-code exchange, PKCE validation and token storage + * are handled by react-oidc-context / oidc-client-ts. */ import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { useAuth, getAuthority, getClientId } from '@/lib/auth'; -import { useEffect, useState } from 'react'; +import { useAuth } from 'react-oidc-context'; +import { useEffect } from 'react'; export const Route = createFileRoute('/callback')({ component: CallbackRoute, }); -interface TokenResponse { - access_token: string; - id_token: string; - refresh_token?: string; - token_type: string; - expires_in: number; -} - -async function exchangeCodeForTokens(code: string): Promise { - const authority = await getAuthority(); - const clientId = await getClientId(); - const redirectUri = window.location.origin + '/callback'; - - const response = await fetch(`${authority}/protocol/openid-connect/token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - grant_type: 'authorization_code', - client_id: clientId, - code, - redirect_uri: redirectUri, - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Token exchange failed: ${error}`); - } - - return response.json(); -} - function CallbackRoute() { const auth = useAuth(); const navigate = useNavigate(); - const [manualError, setManualError] = useState(null); - const [manualLoading, setManualLoading] = useState(false); - // Handle manual token exchange if library's callback fails useEffect(() => { - // If there's an error from the library (state mismatch), try manual exchange - if (auth.error?.message?.includes('state')) { - const params = new URLSearchParams(window.location.search); - const code = params.get('code'); - const storedState = sessionStorage.getItem('oidc_state'); - const returnedState = params.get('state'); - - // Verify our custom state if present - if (storedState && returnedState && storedState === returnedState && code) { - console.log('[Callback] Library state failed, attempting manual token exchange...'); - setManualLoading(true); - - (async () => { - try { - const tokens = await exchangeCodeForTokens(code); - console.log('[Callback] Manual token exchange successful'); - // Store tokens in localStorage for the app to use - const [authority, clientId] = await Promise.all([getAuthority(), getClientId()]); - const storageKey = `oidc.user:${authority}:${clientId}`; - localStorage.setItem(storageKey, JSON.stringify({ - access_token: tokens.access_token, - id_token: tokens.id_token, - refresh_token: tokens.refresh_token, - token_type: tokens.token_type, - expires_at: Math.floor(Date.now() / 1000) + tokens.expires_in, - })); - - // Clean up - sessionStorage.removeItem('oidc_state'); - sessionStorage.removeItem('oidc_nonce'); - - // Redirect to return URL - const returnUrl = sessionStorage.getItem('returnUrl') || '/'; - sessionStorage.removeItem('returnUrl'); - window.location.href = returnUrl; - } catch (err) { - console.error('[Callback] Manual token exchange failed:', err); - setManualError(err instanceof Error ? err.message : 'Token exchange failed'); - setManualLoading(false); - } - })(); - } + if (!auth.isAuthenticated) { + return; } - }, [auth.error]); - useEffect(() => { - // Once authenticated via library, redirect to stored return URL or home - if (auth.isAuthenticated) { - const returnUrl = sessionStorage.getItem('returnUrl') || '/'; - sessionStorage.removeItem('returnUrl'); - navigate({ to: returnUrl }); - } + const returnUrl = sessionStorage.getItem('returnUrl') || '/'; + sessionStorage.removeItem('returnUrl'); + + navigate({ to: returnUrl }); }, [auth.isAuthenticated, navigate]); - // Manual exchange loading - if (manualLoading) { - return ( -
-
-
-

Completing sign-in...

-

Exchanging credentials...

-
-
- ); - } + const handleReturnHome = () => { + sessionStorage.removeItem('returnUrl'); + navigate({ to: '/' }); + }; - // Manual exchange error - if (manualError) { + if (auth.error) { return (
⚠
-

Authentication Failed

-

{manualError}

- -
-
- ); - } - // Loading state during token exchange - if (auth.isLoading) { - return ( -
-
-
-

Completing sign-in...

-

- Please wait while we verify your credentials. +

+ Authentication Failed +

+ +

+ {auth.error.message}

-
-
- ); - } - // Error state from library (will trigger manual exchange if state-related) - if (auth.error && !auth.error.message?.includes('state')) { - return ( -
-
-
⚠
-

Authentication Failed

-

{auth.error.message}

); @@ -81,14 +72,8 @@ function CallbackRoute() {
- -

- Completing sign-in... -

- -

- Please wait while we verify your credentials. -

+

Completing sign-in...

+

Please wait while we verify your credentials.

); diff --git a/tests/auth.spec.ts b/tests/auth.spec.ts index 253901b4..7fa7ddda 100644 --- a/tests/auth.spec.ts +++ b/tests/auth.spec.ts @@ -25,8 +25,8 @@ import { test, expect, type Page } from '@playwright/test'; * pnpm playwright test tests/auth.spec.ts * * # With custom URLs: - * PLAYWRIGHT_BASE_URL=http://wsl.ymbihq.local:52080 \ - * KEYCLOAK_URL=http://wsl.ymbihq.local:42997 \ + * PLAYWRIGHT_BASE_URL=http://localhost:52080 \ + * KEYCLOAK_URL=http://localhost:42997 \ * pnpm playwright test tests/auth.spec.ts */ @@ -218,15 +218,8 @@ test.describe('Authentication Flow - OIDC Enabled', () => { await page.getByRole('button', { name: /admin user/i }).click(); await page.getByRole('button', { name: /logout/i }).click(); - // Should redirect to Keycloak logout confirmation - await page.waitForURL(/.*protocol\/openid-connect\/logout/, { - timeout: 10000, - }); - - // Confirm logout - await page.getByRole('button', { name: /logout/i }).click(); - - // Should redirect back to login page + // signoutRedirect sends id_token_hint, so Keycloak ends the session without a + // confirmation page and returns to the app, which redirects to the login page await page.waitForURL(/.*realms.*protocol\/openid-connect\/auth/, { timeout: 10000, }); @@ -479,6 +472,45 @@ test.describe('Session Management', () => { expect(tokenData.expires_at).toBeDefined(); }); + test('an expiring access token is renewed without a new login', async ({ page }) => { + await page.goto('/'); + await page.waitForURL(/.*realms.*protocol\/openid-connect\/auth/, { + timeout: 10000, + }); + await keycloakLogin(page, TEST_USER, TEST_PASSWORD); + await waitForAppLoaded(page); + + // Make the stored token look like it expires in 10s (inside the 60s renewal buffer) + const oldToken = await page.evaluate(() => { + const key = Object.keys(localStorage).find((k) => k.startsWith('oidc.user:'))!; + const data = JSON.parse(localStorage.getItem(key)!); + data.expires_at = Math.floor(Date.now() / 1000) + 10; + localStorage.setItem(key, JSON.stringify(data)); + return data.access_token as string; + }); + + // Reload: API calls must go out with a renewed token (refresh_token grant), no 401s + const unauthorized: string[] = []; + page.on('response', (r) => { + if (r.url().includes('/trpc') && r.status() === 401) unauthorized.push(r.url()); + }); + await page.reload(); + await waitForAppLoaded(page); + + await expect + .poll( + () => + page.evaluate(() => { + const key = Object.keys(localStorage).find((k) => k.startsWith('oidc.user:'))!; + return JSON.parse(localStorage.getItem(key)!).access_token as string; + }), + { timeout: 15000 } + ) + .not.toBe(oldToken); + expect(page.url()).not.toContain('realms'); + expect(unauthorized).toEqual([]); + }); + test('logout clears tokens from localStorage', async ({ page }) => { // Login await page.goto('/'); @@ -498,12 +530,8 @@ test.describe('Session Management', () => { // Logout await page.getByRole('button', { name: /admin user/i }).click(); await page.getByRole('button', { name: /logout/i }).click(); - await page.waitForURL(/.*protocol\/openid-connect\/logout/, { - timeout: 10000, - }); - await page.getByRole('button', { name: /logout/i }).click(); - // Wait for redirect back to login + // Wait for redirect back to login (no Keycloak confirmation page: id_token_hint is sent) await page.waitForURL(/.*realms.*protocol\/openid-connect\/auth/, { timeout: 10000, }); From 6d8f91e640446f8400955072530e3f759eef283c Mon Sep 17 00:00:00 2001 From: Oriol Rius Date: Wed, 21 Oct 2026 17:11:17 +0200 Subject: [PATCH 5/5] build(dev): run the dev stack on localhost only (drop wsl.ymbihq.local) OIDC login now needs a secure context (HTTPS or localhost), so the dev stack uses http://localhost everywhere: - vite: drop the *.ymbihq.local allowedHosts (Vite's default allows localhost and IP addresses). - dev realm pki-web client: only localhost:52080 / :8080 redirect URIs and web origins. PKCE S256 is required and post-logout redirects are allowed. - .env.example files now match the real dev ports (52080/52081), so `cp .env.example .env && pnpm dev` works as-is. - KMS_URL defaults: localhost:42998. - Docs (DEVELOPMENT/CLAUDE/README/keycloak README) and test usage examples updated. Document how a Windows localhost reaches WSL (localhostForwarding or loopback-only netsh portproxy). Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 5 ++-- DEVELOPMENT.md | 40 +++++++++++++++++++++--------- README.md | 4 +-- backend/.env.example | 6 ++--- backend/CLAUDE.md | 2 +- backend/src/kms/service.ts | 2 +- backend/src/kms/spike-ssh-ecies.ts | 2 +- backend/src/kms/spike-ssh-sign.ts | 2 +- frontend/.env.example | 2 +- frontend/vite.config.ts | 8 +++--- keycloak/README.md | 2 +- keycloak/pki-dev-realm.json | 18 ++++++++------ tests/auth-m2m.spec.ts | 4 +-- 13 files changed, 57 insertions(+), 40 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fd8bf32d..4b4c6a1e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -117,8 +117,9 @@ you can run `pnpm dev` headlessly (no `Stdin is not a tty`) and then read the st `backlog browser` on 6430) needs a socat relay. That doc also has the real port table, verify commands, and the cleanup recipe. -The committed dev config targets `*.ymbihq.local` on the `42xxx`/`52xxx` port range — see -[frontend/CLAUDE.md](frontend/CLAUDE.md) and edit the `.env` files for local-only dev. +The dev stack runs on **`http://localhost`** (`42xxx`/`52xxx` ports, IPv4 binds, so the same +URLs work in Orca's and in the Windows browser). Use `localhost`, never a LAN/WSL hostname: +OIDC login needs a secure context (HTTPS or localhost) — see [frontend/CLAUDE.md](frontend/CLAUDE.md). Details: [DEVELOPMENT.md](DEVELOPMENT.md) (dev), [DEPLOYMENT.md](DEPLOYMENT.md) (Docker), [KEYCLOAK.md](KEYCLOAK.md) / [OIDC.md](OIDC.md) (auth). diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 2535b9ae..83f5c6a9 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -31,14 +31,20 @@ pnpm dev ### Dev stack ports -The committed dev config targets `*.ymbihq.local` on the `42xxx`/`52xxx` range +The dev stack runs on **`localhost`** on the `42xxx`/`52xxx` range (`backend/.env`, `frontend/.env`, `frontend/vite.config.ts`) — **not** the 3000/5173 -defaults. +defaults. Everything binds IPv4 `0.0.0.0`, so WSL2 forwards each port to Windows and the +same `http://localhost:PORT` URLs work in Orca's browser **and** in any Windows browser. + +**Use `localhost`, not a LAN/WSL hostname.** OIDC login (PKCE) needs `crypto.subtle`, which +browsers only expose in a secure context — HTTPS or `http://localhost`. Over plain HTTP on +any other name (e.g. the old `wsl.ymbihq.local`) the app shows *"Login requires a secure +context"*. Production must be served over HTTPS. | Service | URL | Bound by | |---|---|---| -| Frontend (Vite) | http://wsl.ymbihq.local:52080 | `frontend/vite.config.ts` (`host: '0.0.0.0'`) | -| Backend (Fastify) | http://wsl.ymbihq.local:52081 — Swagger at `/api/docs` | `backend/.env` `PORT`/`HOST` | +| Frontend (Vite) | http://localhost:52080 | `frontend/vite.config.ts` (`host: '0.0.0.0'`) | +| Backend (Fastify) | http://localhost:52081 — Swagger at `/api/docs` | `backend/.env` `PORT`/`HOST` | | Cosmian KMS | http://localhost:42998 | `kms/docker-compose.yml` | | Keycloak | http://localhost:42997 (admin/admin) | `keycloak/docker-compose.yml` | | Backlog.md web UI | http://localhost:6430 | `backlog/config.yml` `defaultPort` | @@ -104,9 +110,19 @@ serving fine — probe the HTTP endpoints, don't trust the healthcheck label. ### WSL2 / Windows reachability gotcha -The Orca browser and the user resolve `localhost` on **Windows**, and WSL2 only forwards -**IPv4 (`0.0.0.0`)** binds. A server on the IPv6 wildcard (`*:PORT` in `ss`) works from -inside WSL but gives `ERR_EMPTY_RESPONSE` / a `chrome-error://` page in the browser. +The Orca browser and the user resolve `localhost` on **Windows**. How a Windows +`localhost:PORT` reaches WSL depends on the machine: + +- **WSL default (`localhostForwarding=true`)**: WSL2 forwards **IPv4 (`0.0.0.0`)** binds + automatically. A server on the IPv6 wildcard (`*:PORT` in `ss`) works from inside WSL + but gives `ERR_EMPTY_RESPONSE` / a `chrome-error://` page in the browser. +- **d0 (`localhostForwarding=false` in `%USERPROFILE%\.wslconfig`, on purpose)**: ports are + exposed with `netsh portproxy` rules instead. The dev stack's browser-facing ports are + forwarded loopback-only (`127.0.0.1:PORT → wsl.ymbihq.local:PORT`, not reachable from the + LAN) by `C:\tools\private\add-pki-manager-dev-ports.ps1` (elevated, additive): + **52080** frontend, **52081** API, **42997** Keycloak, **52091** backlog relay. A new + browser-facing port needs a rule there too. Check with + `netsh.exe interface portproxy show v4tov4`. - Backend and frontend already bind `0.0.0.0` — fine as-is. - **`backlog browser` binds the IPv6 wildcard on 6430** and has no `--host` flag, so it @@ -180,7 +196,7 @@ HOST=0.0.0.0 NODE_ENV=development # Frontend URL for CORS -FRONTEND_URL=http://wsl.ymbihq.local:52080/ +FRONTEND_URL=http://localhost:52080 # Database DATABASE_PATH=./data/pki.db @@ -192,7 +208,7 @@ KMS_URL=http://localhost:42998 ALLOW_UNAUTHENTICATED_SSH_CA=true # CRL Distribution Point -CRL_DISTRIBUTION_URL=http://wsl.ymbihq.local:52081/crl +CRL_DISTRIBUTION_URL=http://localhost:52081/crl ``` With no `OIDC_ISSUER`/`OIDC_AUDIENCE` set the backend runs **fully unauthenticated** and @@ -202,7 +218,7 @@ when the Keycloak container is up. See [OIDC.md](OIDC.md) to enable it. ### Frontend (`frontend/.env`) ```env -VITE_API_URL=http://wsl.ymbihq.local:52081/trpc +VITE_API_URL=http://localhost:52081/trpc ``` ## Development Servers @@ -223,8 +239,8 @@ Use this when you can't give mprocs a TTY (scripts, agents, CI-ish runs): |----------|-----------|---------|-----| | 1 | `kms/` | `docker compose up -d` | http://localhost:42998 | | 2 | `keycloak/` | `docker compose up -d` | http://localhost:42997 | -| 3 | `backend/` | `pnpm dev` | http://wsl.ymbihq.local:52081 | -| 4 | `frontend/` | `pnpm dev` | http://wsl.ymbihq.local:52080 | +| 3 | `backend/` | `pnpm dev` | http://localhost:52081 | +| 4 | `frontend/` | `pnpm dev` | http://localhost:52080 | | 5 | repo root | `backlog browser --no-open` | http://localhost:6430 | ## Available Scripts diff --git a/README.md b/README.md index 4d09bbb6..935e9473 100644 --- a/README.md +++ b/README.md @@ -347,8 +347,8 @@ cd backend && pnpm db:migrate pnpm dev # from root ``` -- **Frontend**: http://localhost:5173 -- **Backend API**: http://localhost:3000 +- **Frontend**: http://localhost:52080 +- **Backend API**: http://localhost:52081 - **Cosmian KMS**: http://localhost:42998 - **Keycloak**: http://localhost:42997 (admin/admin) diff --git a/backend/.env.example b/backend/.env.example index eef189ea..fc5069c8 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,10 +1,10 @@ # Server Configuration -PORT=3000 +PORT=52081 HOST=0.0.0.0 NODE_ENV=development # Frontend URL for CORS -FRONTEND_URL=http://localhost:5173 +FRONTEND_URL=http://localhost:52080 # Database DATABASE_PATH=./data/pki.db @@ -16,7 +16,7 @@ KMS_URL=http://localhost:42998 # CRL Distribution Point URL (for X.509 certificates) # Format: http://your-domain.com/crl/{caId}.crl # Example: http://crl.example.com/crl/{caId}.crl -CRL_DISTRIBUTION_URL=http://localhost:3000/crl +CRL_DISTRIBUTION_URL=http://localhost:52081/crl # ============================================================================= # OIDC Authentication Configuration diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index e4334eae..5da0a2aa 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -68,7 +68,7 @@ trusts only its own zone's user CAs (`ssh-host-krl.service` narrows the composed ## Gotchas - **KMS is a hard dependency** for almost all CA/cert paths; unreachable → most ops fail - (`CAKmsInconsistencyError`). `getKMSService()` defaults `KMS_URL` to `http://wsl.ymbihq.local:42998`. + (`CAKmsInconsistencyError`). `getKMSService()` defaults `KMS_URL` to `http://localhost:42998`. - KMS `certify` modes: pass a `publicKeyId` to certify a KMS-held key (CA creation, manual issuance); pass a `csr` + `preserveCsrKey` to sign a CSR's **own** public key (the external `/sign` path — the CA key stays in the KMS). The CSR goes in the KMIP `CertificateRequestValue` diff --git a/backend/src/kms/service.ts b/backend/src/kms/service.ts index f6c547cd..8b4fcf22 100644 --- a/backend/src/kms/service.ts +++ b/backend/src/kms/service.ts @@ -561,7 +561,7 @@ let kmsServiceInstance: KMSService | null = null; */ export function getKMSService(): KMSService { if (!kmsServiceInstance) { - const kmsUrl = process.env.KMS_URL || 'http://wsl.ymbihq.local:42998'; + const kmsUrl = process.env.KMS_URL || 'http://localhost:42998'; kmsServiceInstance = new KMSService({ kmsUrl, diff --git a/backend/src/kms/spike-ssh-ecies.ts b/backend/src/kms/spike-ssh-ecies.ts index ef5dabf5..2314327d 100644 --- a/backend/src/kms/spike-ssh-ecies.ts +++ b/backend/src/kms/spike-ssh-ecies.ts @@ -13,7 +13,7 @@ import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -const KMS_URL = (process.env.KMS_URL || 'http://wsl.ymbihq.local:42998').replace(/\/$/, ''); +const KMS_URL = (process.env.KMS_URL || 'http://localhost:42998').replace(/\/$/, ''); const COSMIAN = process.env.COSMIAN_BIN || 'cosmian'; const cosmian = (args: string[]) => execFileSync(COSMIAN, ['--kms-url', KMS_URL, 'kms', ...args], { encoding: 'utf8' }); diff --git a/backend/src/kms/spike-ssh-sign.ts b/backend/src/kms/spike-ssh-sign.ts index e8a5d7a1..e901486d 100644 --- a/backend/src/kms/spike-ssh-sign.ts +++ b/backend/src/kms/spike-ssh-sign.ts @@ -25,7 +25,7 @@ import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -const KMS_URL = (process.env.KMS_URL || "http://wsl.ymbihq.local:42998").replace(/\/$/, ""); +const KMS_URL = (process.env.KMS_URL || "http://localhost:42998").replace(/\/$/, ""); const COSMIAN_BIN = process.env.COSMIAN_BIN || "cosmian"; type Probe = { path: string; ok: boolean; detail: string }; diff --git a/frontend/.env.example b/frontend/.env.example index 57ab398d..21d40564 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,5 +1,5 @@ # Backend API URL -VITE_API_URL=http://localhost:3000/trpc +VITE_API_URL=http://localhost:52081/trpc # ============================================================================= # OIDC Authentication Configuration diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8c4c0136..27e96038 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -20,10 +20,8 @@ export default defineConfig({ // Fail loudly instead of silently hopping to 52082 when a stale dev server // still owns the port — a shifted port is how you end up with two stacks. strictPort: true, - allowedHosts: [ - 'wsl.ymbihq.local', - 'localhost', - '.ymbihq.local', // Allow all subdomains - ], + // No allowedHosts: Vite's default (localhost + IP literals) is exactly what dev needs. + // Open the app as http://localhost:52080 — WSL2 forwards it to the Windows browser, + // and localhost is a secure context, which OIDC login (PKCE / crypto.subtle) requires. }, }) diff --git a/keycloak/README.md b/keycloak/README.md index 9ff4c46e..0d7f6ddc 100644 --- a/keycloak/README.md +++ b/keycloak/README.md @@ -135,7 +135,7 @@ curl -X POST http://localhost:42997/realms/pki-dev/protocol/openid-connect/token ### Authorization Code Flow (pki-web) For web application authentication, use the `pki-web` client with redirect URIs: -- `http://localhost:5173/*` (Vite dev server) +- `http://localhost:52080/*` (Vite dev server) - `http://localhost:8080/*` (Production frontend) ## Persistent Data diff --git a/keycloak/pki-dev-realm.json b/keycloak/pki-dev-realm.json index 143b46ff..c9314b53 100644 --- a/keycloak/pki-dev-realm.json +++ b/keycloak/pki-dev-realm.json @@ -52,17 +52,15 @@ "standardFlowEnabled": true, "directAccessGrantsEnabled": true, "serviceAccountsEnabled": false, - "rootUrl": "http://localhost:5173", + "rootUrl": "http://localhost:52080", "baseUrl": "/", "redirectUris": [ - "http://localhost:5173/*", - "http://localhost:8080/*", - "http://wsl.ymbihq.local:52080/*" + "http://localhost:52080/*", + "http://localhost:8080/*" ], "webOrigins": [ - "http://localhost:5173", - "http://localhost:8080", - "http://wsl.ymbihq.local:52080" + "http://localhost:52080", + "http://localhost:8080" ], "defaultClientScopes": [ "web-origins", @@ -72,7 +70,11 @@ ], "optionalClientScopes": [ "offline_access" - ] + ], + "attributes": { + "pkce.code.challenge.method": "S256", + "post.logout.redirect.uris": "+" + } } ], "users": [ diff --git a/tests/auth-m2m.spec.ts b/tests/auth-m2m.spec.ts index 8a590750..844f5cfe 100644 --- a/tests/auth-m2m.spec.ts +++ b/tests/auth-m2m.spec.ts @@ -16,8 +16,8 @@ import { test, expect } from '@playwright/test'; * KEYCLOAK_URL - Keycloak base URL (default: http://localhost:42997) * * Usage: - * BACKEND_URL=http://wsl.ymbihq.local:52081 \ - * KEYCLOAK_URL=http://wsl.ymbihq.local:42997 \ + * BACKEND_URL=http://localhost:52081 \ + * KEYCLOAK_URL=http://localhost:42997 \ * pnpm playwright test tests/auth-m2m.spec.ts */