Define identity, action and object before the effect
The lab creates fictional report-export jobs over loopback HTTP. It makes no payments and calls no external services. A test-credential map identifies tenant, subject and write permission. This is not an identity provider and does not validate real tokens. POST /exports requires write permission and a report in the authorized tenant. GET /jobs/{id} allows reading the job within the tenant. Tenant comes from the test identity rather than a free body field. The exercise distinguishes knowing an identifier from being authorized to use it. Hard-to-guess IDs may reduce discovery but do not replace access decisions. This worksheet’s policy is tenant-level; it does not demonstrate finer rights among users in the same tenant.
Apply the contract to inputs and operations
The implementation accepts only reportId and format, with csv or json. An extra tenant field, malformed JSON, an array or unsupported format is rejected. The body has a teaching limit of 512 bytes and rejected requests create no jobs. However, the script drains the received body before returning rejection and implements no production protection against slow connections or endless streams. The budget endpoint permits two requests per tenant per window of 1000 injected-time units; this is not a measurement of one real second. The test shows that subjects in one tenant share budget while another tenant has separate allowance. Production also needs decisions about per-operation size, concurrency, cost and downstream behavior.
Give replay explicit scope and a contract
A client can lose the response after a job has been created. Repeating a POST without knowing its semantics can duplicate the effect. RFC9110 distinguishes idempotent methods and retry conditions; it does not make every POST idempotent. In this original contract, the key includes tenant, subject, method, route and Idempotency-Key. The fingerprint uses relevant request fields rather than textual JSON-property order. The first call creates a job and returns 201; an equivalent replay returns the same ID with 200. The same key with a different format returns 409. An equal key under another subject or tenant has separate scope. These codes and in-memory retention belong to the lab rather than a universal idempotency-key standard.
Reauthorize before returning old results
The lab checks current permission and the report before looking up a stored result. After removing write permission from the identity that created the job, replaying the key returns 403 and creates no new effects. Returning stored output before that check could ignore an access change. Eight concurrent calls with the same key produce one job in this process: map lookup and insertion have no await between them. This does not prove exactly one execution in a distributed system. Multiple instances, process failure or an external effect require persistent storage, atomicity, intermediate states and reconciliation to be designed. Expiring a key also changes late-replay behavior. Define these limits before announcing a guarantee to clients.
Run the matrix and interpret only the evidence obtained
Run the complete script with the Node version recorded in the README. It opens an ephemeral port on 127.0.0.1, sends fictional-data requests, retains HTTP observations and closes the server. Checks cover missing identity, read without write, another tenant’s report, invalid fields, size, key conflicts, concurrency, permission changes and budget. Inspect outcomes and the effect counter rather than only HTTP status. Four jobs are expected across accepted requests. Maps are not persistent, there is no TLS or database transaction, and the budget clock is test-controlled. Summary: idempotency does not grant authorization and a limit at one layer does not protect the whole service. Use evidence to plan an actual trial rather than declare production readiness.
// Original teaching fixture. Loopback only; fake credentials and in-memory state.
import http from 'node:http'
import fs from 'node:fs'
import {createHash} from 'node:crypto'
const identities=new Map([['a-reader',{tenant:'a',subject:'reader',write:false}],['a-writer',{tenant:'a',subject:'writer',write:true}],['a-other',{tenant:'a',subject:'other',write:true}],['b-writer',{tenant:'b',subject:'writer',write:true}]]);
const objects=new Map([['report-a',{tenant:'a'}],['report-b',{tenant:'b'}]]),jobs=new Map,ledger=new Map,budgets=new Map;
const checks=[],observations=[];let effects=0,now=0;
function check(id,pass){checks.push({id,pass:Boolean(pass)});}
function send(res,status,body){res.writeHead(status,{'Content-Type':'application/json','Cache-Control':'no-store'});res.end(JSON.stringify(body));}
const server=http.createServer(async(req,res)=>{
const principal=identities.get((req.headers.authorization||'').replace(/^Bearer /,''));
if(!principal)return send(res,401,{error:'unauthenticated'});
const path=new URL(req.url,'http://127.0.0.1').pathname;
if(path==='/limited'&&req.method==='GET'){
const window=Math.floor(now/1000),key=JSON.stringify([principal.tenant,window]),used=budgets.get(key)||0;
if(used>=2)return send(res,429,{error:'fixture-rate-limit'});
budgets.set(key,used+1);return send(res,200,{accepted:true});
}
if(path.startsWith('/jobs/')&&req.method==='GET'){
const job=jobs.get(path.slice(6));return job&&job.tenant===principal.tenant?send(res,200,{id:job.id,reportId:job.reportId}):send(res,404,{error:'not-found'});
}
if(path!=='/exports'||req.method!=='POST')return send(res,405,{error:'unsupported-operation'});
if(!principal.write)return send(res,403,{error:'function-denied'});
let bytes=0,parts=[];
for await(const chunk of req){bytes+=chunk.length;if(bytes<=512)parts.push(chunk);}
if(bytes>512)return send(res,413,{error:'body-limit'});
let body;try{body=JSON.parse(Buffer.concat(parts).toString('utf8'));}catch{return send(res,400,{error:'invalid-json'});}
if(!body||Array.isArray(body)||typeof body!=='object'||Object.keys(body).sort.join(',')!=='format,reportId'||typeof body.reportId!=='string'||!['csv','json'].includes(body.format))return send(res,400,{error:'invalid-fields'});
const object=objects.get(body.reportId);
if(!object||object.tenant!==principal.tenant)return send(res,404,{error:'not-found'});
const idempotency=req.headers['idempotency-key'];
if(typeof idempotency!=='string'||!/^[A-Za-z0-9_-]{1,64}$/.test(idempotency))return send(res,400,{error:'invalid-idempotency-key'});
const key=JSON.stringify([principal.tenant,principal.subject,req.method,path,idempotency]);
const fingerprint=JSON.stringify([body.reportId,body.format]);
// No await between lookup and insertion: atomic only inside this single JS process.
const previous=ledger.get(key);
if(previous)return previous.fingerprint===fingerprint?send(res,200,{id:previous.id,replayed:true}):send(res,409,{error:'key-payload-conflict'});
const id='job-'+(++effects),job={id,tenant:principal.tenant,reportId:body.reportId,format:body.format};
jobs.set(id,job);ledger.set(key,{id,fingerprint});return send(res,201,{id,replayed:false});
});
await new Promise(r=>server.listen(0,'127.0.0.1',r));const origin='http://127.0.0.1:'+server.address.port;
async function request(path,token='a-writer',body,opts={}){
const headers={};if(token)headers.Authorization='Bearer '+token;if(body!==undefined)headers['Content-Type']='application/json'if(opts.key!==null)headers['Idempotency-Key']=opts.key||'request-1'
const method=opts.method||(body===undefined?'GET':'POST'),res=await fetch(origin+path,{method,headers,body:body===undefined?undefined:typeof body==='string'?body:JSON.stringify(body),signal:AbortSignal.timeout(3000)}),data=await res.json;observations.push({path,method,identity:token||'absent',status:res.status,body:data});return{status:res.status,data};
}
const payload={reportId:'report-a',format:'csv'};
try{
check('missing-identity-denied',(await request('/exports',null,payload)).status===401);
check('unknown-identity-denied',(await request('/exports','unknown',payload)).status===401);
check('reader-cannot-create',(await request('/exports','a-reader',payload)).status===403);
check('cross-tenant-report-denied',(await request('/exports','b-writer',payload)).status===404);
check('missing-report-denied',(await request('/exports','a-writer',{...payload,reportId:'missing'})).status===404);
check('body-tenant-injection-denied',(await request('/exports','a-writer',{...payload,tenant:'b'})).status===400);
check('unsupported-format-denied',(await request('/exports','a-writer',{...payload,format:'xml'})).status===400);
check('malformed-json-denied',(await request('/exports','a-writer','{')).status===400);
check('array-body-denied',(await request('/exports','a-writer',[])).status===400);
check('oversize-body-denied',(await request('/exports','a-writer','x'.repeat(513))).status===413);
check('missing-idempotency-key-denied',(await request('/exports','a-writer',payload,{key:null})).status===400);
check('unsafe-idempotency-key-denied',(await request('/exports','a-writer',payload,{key:'bad key'})).status===400);
check('rejected-inputs-have-no-effects',effects===0);
const first=await request('/exports','a-writer',payload);check('first-create',first.status===201&&effects===1);
const repeat=await request('/exports','a-writer',{format:'csv',reportId:'report-a'});check('reordered-fields-replay-same-result',repeat.status===200&&repeat.data.id===first.data.id&&effects===1);
check('same-key-different-payload-conflict',(await request('/exports','a-writer',{...payload,format:'json'})).status===409&&effects===1);
const parallel=await Promise.all(Array.from({length:8},=>request('/exports','a-writer',payload,{key:'parallel'})));
check('concurrent-same-key-one-effect',new Set(parallel.map(r=>r.data.id)).size===1&¶llel.filter(r=>r.status===201).length===1&¶llel.filter(r=>r.status===200).length===7&&effects===2);
const other=await request('/exports','a-other',payload);check('same-key-other-principal-is-separate',other.status===201&&other.data.id!==first.data.id&&effects===3);
const b=await request('/exports','b-writer',{reportId:'report-b',format:'csv'});check('same-key-other-tenant-is-separate',b.status===201&&b.data.id!==first.data.id&&effects===4);
check('own-job-readable',(await request('/jobs/'+first.data.id,'a-reader')).status===200);
check('other-tenant-job-not-disclosed',(await request('/jobs/'+first.data.id,'b-writer')).status===404);
identities.get('a-writer').write=false;check('replay-reauthorizes-current-permission',(await request('/exports','a-writer',payload)).status===403&&effects===4);identities.get('a-writer').write=true;
check('first-budget-request',(await request('/limited','a-reader')).status===200);
check('second-budget-request',(await request('/limited','a-writer')).status===200);
check('same-tenant-budget-shared',(await request('/limited','a-other')).status===429);
check('other-tenant-budget-independent',(await request('/limited','b-writer')).status===200);
now=1000;check('injected-clock-next-window',(await request('/limited','a-reader')).status===200);
check('wrong-method-denied',(await request('/exports','a-writer',undefined,{method:'GET'})).status===405);
check('no-additional-side-effects',effects===4);
}finally{await new Promise(r=>server.close(r));check('owned-loopback-server-closed',!server.listening);}
const result={runtime:process.version,scriptSHA256:createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex'),passed:checks.filter(x=>x.pass).length,failed:checks.filter(x=>!x.pass).length,checks,requestCount:observations.length,observations,effects,limits:['Fictional credentials, tenants and reports; no actual identity provider, payment or cloud system.','HTTP loopback teaching server; no TLS, database durability, distributed lock or process-failure recovery.','Rate window uses an injected clock; not a wall-clock performance measurement.','Input is counted while drained; no production connection, timeout or streaming-abuse protection.','In-memory job/idempotency/budget maps are finite in this run, not a production retention design.','Authorization is tenant-level in this fixture, not a claim of per-user entitlement to every object in a tenant.','Single-process atomic insertion does not prove distributed exactly-once execution.']};
console.log(JSON.stringify(result,null,2));if(result.failed)process.exitCode=1Eight concurrent same-key requests create one local job. Removing permission before replay returns 403 even with a stored result.
Common pitfalls
A key without tenant scope; cache before authorization; blind POST retry; local result as distributed guarantee; byte limit as complete protection.
Related topics: Capacity, availability and isolation · Authorization, replay and operational acceptance
Authorize every operation and object, bound consumption and distinguish controlled replay from distributed guarantees.
Reference: API1:2023 Broken Object Level Authorization · CCSP examination outline effective 2026-08-01; January2026 V2 PDF