Some checks failed
Use-case regression gate / usecase-regressions (push) Has been cancelled
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
124 lines
6.6 KiB
JavaScript
124 lines
6.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
import { createHash, randomBytes } from 'node:crypto';
|
|
import { chromium } from './node_modules/playwright/index.mjs';
|
|
|
|
const input = JSON.parse(await readFile('/run/fixture/app-browser-input.json', 'utf8'));
|
|
const auth = 'https://auth.recovery.test:9091';
|
|
const issuer = 'http://127.0.0.1:8080';
|
|
const app = 'http://127.0.0.1:8000';
|
|
const portal = 'http://127.0.0.1:8089';
|
|
const fixture = 'http://127.0.0.1:8088';
|
|
const allowed = new Set([auth, issuer, app, portal, fixture]);
|
|
let browser;
|
|
let stage = 'native_app_browser_startup';
|
|
let outside = 0;
|
|
try {
|
|
browser = await chromium.launch({
|
|
executablePath: '/run/fixture/browser/chrome-headless-shell-linux64/chrome-headless-shell',
|
|
headless: true, args: ['--host-resolver-rules=MAP auth.recovery.test 127.0.0.1',
|
|
'--no-proxy-server', '--disable-background-networking'],
|
|
});
|
|
const context = await browser.newContext({ignoreHTTPSErrors: true, serviceWorkers: 'block'});
|
|
context.on('request', request => {
|
|
const u = new URL(request.url());
|
|
if (['http:', 'https:'].includes(u.protocol) && !allowed.has(u.origin)) outside += 1;
|
|
});
|
|
await context.route('**/*', route => {
|
|
const u = new URL(route.request().url());
|
|
if (!allowed.has(u.origin)) {
|
|
return route.abort();
|
|
}
|
|
return route.continue();
|
|
});
|
|
const page = await context.newPage();
|
|
page.setDefaultTimeout(20000);
|
|
let callback;
|
|
page.on('request', request => {
|
|
const u = new URL(request.url());
|
|
if (u.origin === app && u.pathname === '/auth/callback/' && u.searchParams.has('code')) {
|
|
callback = u.href; // private, never emitted
|
|
}
|
|
});
|
|
stage = 'native_app_login_redirect';
|
|
await page.goto(app + '/auth/login/');
|
|
await page.locator('#username-textfield').fill('alice');
|
|
stage = 'native_old_password_refusal';
|
|
await page.locator('#password-textfield').fill(input.old_password);
|
|
const rejected = page.waitForResponse(r => r.url() === auth + '/api/firstfactor');
|
|
await page.locator('#sign-in-button').click();
|
|
const denied = await rejected;
|
|
assert(denied.status() >= 400 || (await denied.json()).status !== 'OK');
|
|
await page.getByText('Incorrect username or password', {exact: false}).first().waitFor();
|
|
stage = 'native_replacement_password_login';
|
|
await page.locator('#password-textfield').fill(input.new_password);
|
|
await page.locator('#sign-in-button').click();
|
|
await page.waitForURL(app + '/app/');
|
|
assert(callback);
|
|
await page.goto(app + '/account/session/');
|
|
await page.getByRole('heading', {name: 'Session details', exact: true}).waitFor();
|
|
const value = label => page.locator('dt').filter({hasText: label}).locator('xpath=following-sibling::dd[1]');
|
|
assert.equal((await value(/^Issuer$/).innerText()).trim(), issuer);
|
|
assert.equal((await value(/^Subject$/).innerText()).trim(), 'uid=alice,ou=people,dc=example,dc=test');
|
|
assert.equal((await value(/^Tenant$/).innerText()).trim(), 'tenant:coulomb');
|
|
assert.equal((await value(/^user-engine source$/).innerText()).trim(), 'http');
|
|
stage = 'native_app_callback_replay_refusal';
|
|
const replay = await context.request.get(callback, {maxRedirects: 0});
|
|
assert.equal(replay.status(), 400);
|
|
stage = 'native_unknown_client_refusal';
|
|
const deniedQuery = {redirect_uri: app + '/auth/callback/', response_type: 'code',
|
|
scope: 'openid profile email groups', state: randomBytes(32).toString('base64url'),
|
|
code_challenge_method: 'S256', code_challenge: randomBytes(32).toString('base64url')};
|
|
const invalid = await context.request.get(issuer + '/authorize?'
|
|
+ new URLSearchParams({...deniedQuery, client_id: 'not-registered'}),
|
|
{maxRedirects: 0});
|
|
assert.equal(invalid.status(), 400);
|
|
assert((await invalid.text()).includes('unknown client_id'));
|
|
const wrongRedirect = await context.request.get(issuer + '/authorize?'
|
|
+ new URLSearchParams({...deniedQuery, client_id: 'coulomb-recovery-test',
|
|
redirect_uri: app + '/wrong'}),
|
|
{maxRedirects: 0});
|
|
assert.equal(wrongRedirect.status(), 400);
|
|
assert((await wrongRedirect.text()).includes('redirect_uri does not match'));
|
|
stage = 'native_foreign_tenant_client_refusal';
|
|
const verifier = randomBytes(32).toString('base64url');
|
|
const foreignState = randomBytes(32).toString('base64url');
|
|
const query = new URLSearchParams({client_id: 'foreign-recovery-test',
|
|
redirect_uri: fixture + '/client-callback', response_type: 'code',
|
|
scope: 'openid profile email groups', state: foreignState,
|
|
code_challenge_method: 'S256',
|
|
code_challenge: createHash('sha256').update(verifier).digest('base64url')});
|
|
await page.goto(issuer + '/authorize?' + query);
|
|
await page.waitForURL(u => u.origin === fixture && u.pathname === '/client-callback');
|
|
const foreignCapture = new URL(page.url());
|
|
assert.equal(foreignCapture.searchParams.get('state'), foreignState);
|
|
if (!foreignCapture?.searchParams.get('code')) {
|
|
throw new Error('Foreign callback metadata: ' + JSON.stringify({captured: !!foreignCapture,
|
|
parameters: foreignCapture ? [...foreignCapture.searchParams.keys()] : [],
|
|
error: foreignCapture?.searchParams.get('error'),
|
|
tenant_error: /tenant/i.test(foreignCapture?.searchParams.get('error_description') || '')}));
|
|
}
|
|
const foreign = await context.request.post(issuer + '/token', {maxRedirects: 0,
|
|
form: {grant_type: 'authorization_code', client_id: 'foreign-recovery-test',
|
|
redirect_uri: fixture + '/client-callback', code: foreignCapture.searchParams.get('code'),
|
|
code_verifier: verifier}});
|
|
assert.equal(foreign.status(), 403);
|
|
assert((await foreign.text()).includes('tenant binding conflict'));
|
|
const state = await context.request.get(portal + '/fixture/state', {maxRedirects: 0});
|
|
assert.equal((await state.json()).users, 1);
|
|
assert.equal(outside, 0);
|
|
console.log(JSON.stringify({status: 'native_app_journey_passed', old_password_login_refused: true,
|
|
replacement_password_login_completed: true, native_keycape_code_and_app_session: true,
|
|
real_owner_http_identity_link: true, app_callback_replay_refused: true,
|
|
native_unknown_client_refused: true, native_wrong_redirect_refused: true,
|
|
native_foreign_tenant_token_refused: true, negative_client_callback_is_fixture: true,
|
|
issuer_subject_tenant_provenance_verified: true,
|
|
owner_users: 1, requests_outside_fixture_origins: outside}));
|
|
await context.close();
|
|
} catch (error) {
|
|
await writeFile('/run/fixture/browser-diagnostic.txt', String(error.message), {mode: 0o600});
|
|
console.log(JSON.stringify({status: 'native_app_journey_failed', stage, failure_type: error.name}));
|
|
process.exitCode = 1;
|
|
} finally {
|
|
if (browser) await browser.close();
|
|
}
|