Return artifact consent through registered POST callbacks

This commit is contained in:
Thomas Durieux committed 2026-09-27 15:59:36 +00:00
1 parent 09f94137cd
commit d3a8b4a8a7
11 files changed
+202 -13

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
require('ts-node/register/transpile-only');
const { expect } = require('chai');
const { createReviewCallbacks } = require('../src/server/service/review-callbacks');
const entry = () => ({ clientId:'1'.repeat(32),callbackId:'2'.repeat(32),url:'https://review.example.test/api/v1/artifacts/callback' });
const raw = callbacks => JSON.stringify({version:1,callbacks});
describe('registered review callback destinations', () => {
it('binds exact HTTPS destinations to both client and callback IDs', () => {
const registry=createReviewCallbacks(raw([entry()]));
expect(registry.resolve(entry().clientId,entry().callbackId)).equal(entry().url);
expect(registry.resolve('3'.repeat(32),entry().callbackId)).equal(undefined);
expect(registry.resolve(entry().clientId,'4'.repeat(32))).equal(undefined);
expect(registry.formAction).equal("'self' "+entry().url);
});
it('rejects redirect parameters, URL credentials, aliases and unsafe CSP characters', () => {
for(const url of ['http://review.example.test/callback','https://owner:secret@review.example.test/callback','https://review.example.test/callback?','https://review.example.test/callback#','https://review.example.test/callback?redirect=https://other.test','https://review.example.test/a/../callback','https://review.example.test/%63allback','https://review.example.test/callback/','https://review.example.test/callback\n','https://review.example.test/','https://review.example.test/callback;script-src']) expect(()=>createReviewCallbacks(raw([{...entry(),url}]))).to.throw('Invalid review callback registry');
});
it('rejects duplicate decoded configuration fields and bounded registry overflows', () => {
for(const value of [raw([]),raw([entry(),entry()]),raw([{...entry(),extra:'x'}]),raw(Array.from({length:33},(_,i)=>({...entry(),callbackId:i.toString(16).padStart(32,'0')}))),raw([entry()]).replace('"version":1','"version":1,"ver\\u0073ion":1'),'{']) expect(()=>createReviewCallbacks(value)).to.throw('Invalid review callback registry');
const entries=Array.from({length:4},(_,i)=>({...entry(),callbackId:String(i).repeat(32),url:'https://review.example.test/'+('x'.repeat(1100))+i}));
expect(()=>createReviewCallbacks(raw(entries))).to.throw('Invalid review callback registry');
});
});
+32 -1
View File
@@ -7,6 +7,9 @@ const { randomBytes } = require("crypto");
const {
createReviewConsentRouter,
} = require("../src/server/service/review-consent-http");
const { createReviewCallbacks } = require("../src/server/service/review-callbacks");
const callbackURL = "https://review.example.test/api/v1/artifacts/callback";
const callbacks = createReviewCallbacks(JSON.stringify({version:1,callbacks:[{clientId:"1".repeat(32),callbackId:"6".repeat(32),url:callbackURL}]}));
const origin = "https://anonymous.example.test";
const owner = "a".repeat(24);
const intent = {
@@ -108,6 +111,10 @@ describe("review consent browser HTTP transport", function () {
};
},
};
backend.handoff = async (actor, ticket, resolveCallback) => {
calls.push({actor,ticket,handoff:true});
return { callbackUrl: resolveCallback(intent.clientId,"6".repeat(32)), completion: { contract: intent.contract, clientId: intent.clientId, intentId: intent.intentId, code: "9".repeat(64), expiresAt: new Date(Date.now()+60000).toISOString().replace(/\.\d{3}Z$/, "Z"), privateField: "private-field" }, privateField: "private-field" };
};
const app = express();
// Model a TLS-terminating trusted loopback proxy, never trust arbitrary peers.
app.set("trust proxy", "loopback");
@@ -134,8 +141,9 @@ describe("review consent browser HTTP transport", function () {
});
app.use(
"/api/review-consent",
createReviewConsentRouter(origin, backend, randomBytes(32)),
createReviewConsentRouter(origin, backend, randomBytes(32), callbacks),
);
app.use("/api/review-consent-disabled", createReviewConsentRouter(origin, backend, randomBytes(32)));
server = http.createServer(app);
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const login = await request("/test-login");
@@ -500,4 +508,27 @@ describe("review consent browser HTTP transport", function () {
) + "\n",
);
});
it('offers only a configured, current-session completion POST handoff', async function () {
expect((await request('/api/review-consent/csrf')).body.handoffEnabled).equal(true);
const response=await request('/api/review-consent/handoff',{body:{ticket:'synthetic-signed-ticket'}});
expect(response.status).equal(200);expect(response.body.callbackUrl).equal(callbackURL);
expect(response.body.completion.code).equal('9'.repeat(64));
expect(Object.keys(response.body.completion).sort()).deep.equal(['clientId','code','contract','expiresAt','intentId']);
expect(JSON.stringify(response.body)).not.include('private-field');
expect(response.headers['cache-control']).equal('no-store');
expect((await request('/api/review-consent/handoff',{body:{ticket:'synthetic-signed-ticket',callbackUrl:'https://other.example.test/callback'}})).status).equal(400);
expect((await request('/api/review-consent-disabled/csrf')).body.handoffEnabled).equal(false);
expect((await request('/api/review-consent-disabled/handoff',{body:{ticket:'synthetic-signed-ticket'}})).status).equal(404);
});
it('does not disclose a completion code after concurrent logout', async function () {
let entered, release;
const pending=new Promise(resolve=>{entered=resolve;});
const wait=new Promise(resolve=>{release=resolve;});
const original=backend.handoff;
backend.handoff=async(...args)=>{entered();await wait;return original(...args);};
const response=request('/api/review-consent/handoff',{body:{ticket:'synthetic-signed-ticket'}});
await pending;await new Promise(resolve=>store.destroy(sid,resolve));release();
const result=await response;expect(result.status).equal(401);expect(JSON.stringify(result.body)).not.include('9'.repeat(64));
});
});
+33
View File
@@ -127,4 +127,37 @@ describe('review consent browser page', function () {
expect(b.document.body.textContent).not.include('Consent recorded.');
});
it('posts a completion only to the configured callback without placing secrets in URLs or storage', async function () {
const completion={contract:'4open.artifacts/1',clientId:'1'.repeat(32),intentId:'2'.repeat(32),code:'9'.repeat(64),expiresAt:new Date(Date.now()+60000).toISOString()};
const callbackUrl='https://review.example.test/api/v1/artifacts/callback';
b=await browser({'/api/review-consent/csrf':()=>({csrf,handoffEnabled:true}),'/api/review-consent/handoff':()=>({completion,callbackUrl})});
const submissions=[];b.w.HTMLFormElement.prototype.submit=function(){submissions.push({action:this.action,method:this.method,completion:JSON.parse(this.elements.namedItem('completion').value)});};
await b.preview();await b.accept();await b.click('button[type=button]');
expect(b.document.body.textContent).include('Continue to review to finish linking');
await b.click('button[type=button]');
expect(submissions).deep.equal([{action:callbackUrl,method:'post',completion}]);
expect(b.w.location.href).equal('https://anonymous.example.test/review-link');
expect(JSON.stringify(b.w.localStorage)).not.include(completion.code);expect(b.w.sessionStorage.length).equal(0);
expect(b.document.documentElement.outerHTML).not.include(token).not.include('private-ticket');
expect(b.requests.find(r=>r.pathname.endsWith('/handoff')).payload).deep.equal({ticket:'private-ticket'});
});
it('does not submit a late completion after account change', async function () {
let resolve;
b=await browser({'/api/review-consent/csrf':()=>({csrf,handoffEnabled:true}),'/api/review-consent/handoff':()=>new Promise(yes=>{resolve=yes;})});
const submissions=[];b.w.HTMLFormElement.prototype.submit=function(){submissions.push(this.action);};
await b.preview();await b.accept();await b.click('button[type=button]');await b.click('button[type=button]');
b.app.state.user={username:'other'};await delay(5);
resolve({completion:{contract:'4open.artifacts/1',clientId:'1'.repeat(32),intentId:'2'.repeat(32),code:'9'.repeat(64),expiresAt:new Date(Date.now()+60000).toISOString()},callbackUrl:'https://review.example.test/api/v1/artifacts/callback'});await delay(30);
expect(submissions).length(0);expect(b.document.body.textContent).include('Your account changed');
expect(b.document.querySelector('#review-repository').value).equal('');
});
it('rejects a wrong-intent completion or an unsafe callback destination', async function () {
let wrongScope=true;
b=await browser({'/api/review-consent/csrf':()=>({csrf,handoffEnabled:true}),'/api/review-consent/handoff':()=>({completion:{contract:'4open.artifacts/1',clientId:'1'.repeat(32),intentId:wrongScope?'f'.repeat(32):'2'.repeat(32),code:'9'.repeat(64),expiresAt:new Date(Date.now()+60000).toISOString()},callbackUrl:wrongScope?'https://review.example.test/api/v1/artifacts/callback':'javascript:alert(1)'})});
const submissions=[];b.w.HTMLFormElement.prototype.submit=function(){submissions.push(this.action);};
await b.preview();await b.accept();await b.click('button[type=button]');await b.click('button[type=button]');
expect(submissions).length(0);wrongScope=false;await b.click('button[type=button]');expect(submissions).length(0);
expect(b.document.body.textContent).include('return to review could not be prepared');
});
});
+12
View File
@@ -783,4 +783,16 @@ describe("review owner consent transactions", function () {
expect(await connection.db.collection('review_completions').countDocuments({})).equal(0);
});
it('resolves callback scope from the signed intent before issuing a code', async function () {
const quoted=await store.preview(actor(),'synthetic-repository',request());
await store.confirm(actor(),quoted.ticket,confirmation());
await rejected(store.handoff(actor(),quoted.ticket,()=>undefined),'forbidden');
expect(await connection.db.collection('review_completions').countDocuments({})).equal(0);
const selected=[];
const result=await store.handoff(actor(),quoted.ticket,(client,callback)=>{selected.push([client,callback]);return 'https://review.example.test/api/v1/artifacts/callback';});
expect(selected).deep.equal([[intent().clientId,intent().callbackId]]);
expect(result.completion).deep.equal(await store.completion(actor(),quoted.ticket));
expect(result.callbackUrl).equal('https://review.example.test/api/v1/artifacts/callback');
});
});