Beta

Create a tenant

A new tenant starts with its own users, OAuth settings, audit history and logs. You are its first Tenant Admin.

BTL Admin

OPENID CONNECT · LAB

Compare CIBA delivery modes

Plan the same approval delivered by poll and by ping, with push refused, and today practise the two client-side checks the notification modes depend on: the notification token and at_hash.

PlannedIncludes a simulationUses your lab tenant

The lesson

Builds on: Following a CIBA exchange.

New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.

Planned. The core of this lab waits on platform features that are not built yet. The planned walkthrough shows exactly how it will run; Do today is a real exercise you can do now.

Setup

CIBA is not implemented yet (G32). Ping and push also need an HTTPS notification endpoint the tenant can reach, such as a hosted lab receiver (G4) or your own tunnel.

  1. source ~/btl-oidc.sh and run btl-lab callback before each sign-in.

  2. Once G32 exists: set lab-tmp-kiosk's delivery mode to ping with the notification endpoint https://<your receiver>/ciba/notify.

Planned walkthrough

  1. Poll mode. Run the exchange from Run a CIBA poll-mode exchange and measure the delay between Ava's approval and the kiosk's next poll. Poll faster than interval and see slow_down; add five seconds to the interval for the rest of the attempt.

  2. Ping mode. Send a fresh client_notification_token with the request:

NOTIFY=$(openssl rand -base64 24 | tr '+/' '-_' | tr -d '=')
curl -s -u "$KIOSK_ID:$KIOSK_SECRET" "$ISSUER/oidc/backchannel_authentication" -d scope=openid --data-urlencode "[email protected]" -d binding_message=H4PX --data-urlencode "client_notification_token=$NOTIFY"

On approval, the tenant posts {"auth_req_id":"..."} to your endpoint with Authorization: Bearer and your token. Answer 204, then make one token request.

Why it matters: the ping says only that a result is ready. Tokens still come only from the token endpoint, after client authentication.

  1. A ping for a declined request: the token request returns access_denied.

  2. Discovery lists backchannel_token_delivery_modes_supported as ["poll","ping"], and registering push is refused, the FAPI position the lesson describes.

Do today

  1. Recompute at_hash, the check push mode relies on. A normal lab-collage token response carries an access token and an ID token whose at_hash covers it:

signin
redeem '<code>'
COMPUTED=$(printf %s "$TOKEN" | openssl dgst -sha256 -binary | head -c 16 | openssl base64 -A | tr '+/' '-_' | tr -d '=')
[ "$COMPUTED" = "$(part "$ID_TOKEN" | jq -r .at_hash)" ] && echo "at_hash matches" || echo "at_hash mismatch: do not use the access token"

at_hash matches: the left half of the SHA-256 hash, Base64url-encoded, because the ID token is signed with RS256.

Why it matters: in push mode the tokens arrive at an endpoint the client exposes, and the signed ID token's at_hash is what vouches for the access token in that unauthenticated delivery.

  1. Build the notification endpoint's one rule: accept only the bearer value you issued for that request. Save as notify.mjs and run it:

import { createServer } from 'node:http';
import { randomBytes, timingSafeEqual } from 'node:crypto';
const issued = new Map([['demo-auth-req-21', randomBytes(24).toString('base64url')]]);
console.log('Notification token for demo-auth-req-21:', issued.get('demo-auth-req-21'));
createServer(async (req, res) => {
  if (req.method !== 'POST' || req.url !== '/ciba/notify') return res.writeHead(404).end();
  let body = '';
  for await (const chunk of req) { body += chunk; if (body.length > 4096) return res.writeHead(413).end(); }
  let id = null;
  try { id = JSON.parse(body).auth_req_id; } catch { id = null; }
  const expected = issued.get(id), given = Buffer.from((req.headers.authorization ?? '').replace(/^Bearer /, ''));
  const ok = Boolean(expected) && given.length === expected.length && timingSafeEqual(given, Buffer.from(expected));
  console.log(JSON.stringify({stage: 'ciba_ping', known_request: Boolean(expected), result: ok ? 'accepted' : 'refused'}));
  res.writeHead(ok ? 204 : 401).end();
}).listen(8767, '127.0.0.1');
  1. Call it as the provider would, once with the value it printed and once with a value it never issued.

Simulation. no tenant sends pings yet (G32), so you play the provider's notification with curl. Your endpoint's decision is real code you would deploy.

read -rs NOTIFY_TOKEN
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:8767/ciba/notify -H "Authorization: Bearer $NOTIFY_TOKEN" -H 'Content-Type: application/json' -d '{"auth_req_id":"demo-auth-req-21"}'
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:8767/ciba/notify -H "Authorization: Bearer not-the-issued-value" -H 'Content-Type: application/json' -d '{"auth_req_id":"demo-auth-req-21"}'

204, then 401, and the log lines contain no token.

Why it matters: the client issues this token and the provider presents it, the reverse of the usual direction. A forged ping does little in ping mode, and in push mode this check is the first gate before any token is used.

Break it

Planned, once G32 exists: your notification endpoint receives a ping whose bearer value it did not issue. It answers 401, makes no token request, and records the refusal.

Check your work

Today: at_hash matches for a real token response, and your endpoint answering 204 for the issued value and 401 for any other.

Once G32 exists, Audit and Logs show each ping delivered with its status, and tokens issued only at the token endpoint.

Cleanup

Stop notify.mjs. Once G32 exists, remove the notification endpoint from lab-tmp-kiosk.

Missing infrastructure

  • G32 (CIBA). Poll and ping delivery, the notification token, and delivery events.

  • G4 (hosted lab receiver). A hosted notification receiver for learners without public HTTPS, since the tenant cannot reach 127.0.0.1.

Back to all labs

We value your privacy

We use cookies and similar technologies to enhance your browsing experience, and analytics to understand our traffic. By clicking "Allow All", you consent to optional analytics. Cookie Policy

The Lab