Skip to content

feat(catalog): oauth/oidc-client with its proof suite - #17

Merged
dgesteves merged 3 commits into
mainfrom
feat/catalog-infra-oauth
Nov 2, 2026
Merged

dgesteves merged 3 commits into
mainfrom
feat/catalog-infra-oauth

Conversation

@dgesteves

Copy link
Copy Markdown
Owner

Adds oauth/oidc-client: secure OIDC sign-in, JWT access-token validation and rotating refresh tokens in Node.js with openid-client 6, jose 6 and oauth4webapi 3. References cover openid-client v6, resource servers and the token lifecycle, with dated sources.

Suite oauth-oidc-client (3 tasks): api-access-tokens, oidc-login, refresh-token-rotation.

Each suite passed node tasks/_tools/check-task.mjs tasks/<suite> --runs 3 in Docker today: the hidden verifier fails on the untouched repo and passes on repo + reference solution three times in a row, with the verifier run under --network none. sigil validate catalog and sigil scan catalog --fail-on high pass on the branch (no new findings).

Not done yet, on purpose: the calibration probe (a short baseline run in the lab) that checks whether today's models already pass these tasks without the skill. The first lab runs hit ceiling effects on zod-4 and express-5, so every suite gets probed before its first published proof. Until then these capabilities show as submitted and untested, which is what they are.

@vercel

vercel Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
sigil-lab Ignored Ignored Preview Nov 2, 2026 9:50am UTC

Comment on lines +28 to +44
router.get('/login', async (req, res) => {
const oidc = await getOidc()
const codeVerifier = client.randomPKCECodeVerifier()
const state = client.randomState()
const nonce = client.randomNonce()
req.session.pendingLogin = { codeVerifier, state, nonce, startedAt: Date.now() }
const url = client.buildAuthorizationUrl(oidc, {
redirect_uri: config.oidc.redirectUri,
scope: 'openid email profile',
code_challenge: await client.calculatePKCECodeChallenge(codeVerifier),
code_challenge_method: 'S256',
state,
nonce,
})
await promisify((cb) => req.session.save(cb))()
res.redirect(url.href)
})
Comment on lines +46 to +85
router.get('/auth/callback', async (req, res) => {
// Single use: whatever happens next, this login attempt is consumed.
const pending = req.session.pendingLogin
delete req.session.pendingLogin
if (!pending || Date.now() - pending.startedAt > LOGIN_TIMEOUT_MS) {
res.status(400).send('This sign-in link has expired. Please sign in again.')
return
}

let tokens: Awaited<ReturnType<typeof client.authorizationCodeGrant>>
try {
tokens = await client.authorizationCodeGrant(await getOidc(), callbackUrl(config, req), {
pkceCodeVerifier: pending.codeVerifier,
expectedState: pending.state,
expectedNonce: pending.nonce,
idTokenExpected: true,
})
} catch (err) {
console.warn('sign-in failed:', err instanceof Error ? err.message : err)
res.status(400).send('Sign-in failed. Please try again.')
return
}

const claims = tokens.claims()
if (!claims) {
res.status(400).send('Sign-in failed. Please try again.')
return
}

// A new session id after login, so a session id planted before login is worthless.
await promisify((cb) => req.session.regenerate(cb))()
req.session.user = {
sub: claims.sub,
email: typeof claims.email === 'string' ? claims.email : undefined,
name: typeof claims.name === 'string' ? claims.name : undefined,
}
req.session.idToken = tokens.id_token
await promisify((cb) => req.session.save(cb))()
res.redirect('/')
})
Comment on lines +30 to +41
return session({
name: SESSION_COOKIE,
secret: config.sessionSecret,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
sameSite: 'lax',
secure: new URL(config.oidc.redirectUri).protocol === 'https:',
maxAge: 8 * 60 * 60 * 1000,
},
})
Comment on lines +25 to +44
router.get('/login', async (_req, res) => {
const attempt = randomBytes(32).toString('base64url')
const login: PendingLogin = {
codeVerifier: client.randomPKCECodeVerifier(),
state: client.randomState(),
nonce: client.randomNonce(),
expiresAt: Date.now() + LOGIN_TTL_MS,
}
pending.set(attempt, login)
setCookie(res, LOGIN_COOKIE, attempt, LOGIN_TTL_MS)
const url = client.buildAuthorizationUrl(await oidc(), {
redirect_uri: config.oidc.redirectUri,
scope: 'openid email profile offline_access',
code_challenge: await client.calculatePKCECodeChallenge(login.codeVerifier),
code_challenge_method: 'S256',
state: login.state,
nonce: login.nonce,
})
res.redirect(url.href)
})
Comment on lines +46 to +86
router.get('/auth/callback', async (req, res) => {
const attempt = readCookie(req, LOGIN_COOKIE) ?? ''
const login = pending.get(attempt)
pending.delete(attempt)
res.clearCookie(LOGIN_COOKIE, { path: '/' })
if (!login || login.expiresAt < Date.now()) {
res.status(400).send('This sign-in link has expired. Please sign in again.')
return
}
const currentUrl = new URL(config.oidc.redirectUri)
currentUrl.search = new URL(req.originalUrl, currentUrl).search
try {
const tokens = await client.authorizationCodeGrant(await oidc(), currentUrl, {
pkceCodeVerifier: login.codeVerifier,
expectedState: login.state,
expectedNonce: login.nonce,
})
const claims = tokens.claims()
if (!claims || !tokens.refresh_token) throw new Error('the IdP did not return the expected tokens')
const sid = randomBytes(32).toString('base64url')
await sessions.set(sid, {
user: {
sub: claims.sub,
email: typeof claims.email === 'string' ? claims.email : undefined,
name: typeof claims.name === 'string' ? claims.name : undefined,
},
tokens: {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
idToken: tokens.id_token,
expiresAt: Date.now() + (tokens.expiresIn() ?? 300) * 1000,
},
createdAt: Date.now(),
})
setCookie(res, SESSION_COOKIE, sid, SESSION_TTL_MS)
res.redirect('/')
} catch (err) {
console.warn('sign-in failed:', err instanceof Error ? err.message : err)
res.status(400).send('Sign-in failed. Please try again.')
}
})
this.#server = createServer((req, res) => {
this.#handle(req, res).catch((err: unknown) => {
res.statusCode = 500
res.end(String(err))
this.#server = createServer((req, res) => {
this.#handle(req, res).catch((err: unknown) => {
res.statusCode = 500
res.end(String(err))
this.#server = createServer((req, res) => {
this.#handle(req, res).catch((err: unknown) => {
res.statusCode = 500
res.end(String(err))
this.#server = createServer((req, res) => {
this.#handle(req, res).catch((err: unknown) => {
res.statusCode = 500
res.end(String(err))
this.#server = createServer((req, res) => {
this.#handle(req, res).catch((err: unknown) => {
res.statusCode = 500
res.end(String(err))
dgesteves added a commit that referenced this pull request Nov 2, 2026
…e it stands (#22)

The README was a 12-line "under construction" stub. This replaces it
with the front door: the problem, what Sigil does, a quick start, how a
proof works, trust levels, an honest status and the repo layout.

How it was written:

- **Every number checked against its source today.** SkillsBench's
abstract has been revised since we first read it: +16.6 points on
average (33.9% to 50.5%), ranging from +4.1 to +25.7 across 18
model-harness setups. SWE-Skills-Bench: 92 of 98 skills had no
statistically significant effect (76.6% to 76.9%). Scan numbers come
from `packages/sigil/src/scan/BENCHMARK.md` (47 benign skills with zero
high or critical findings, all 14 malicious samples flagged). Agent
counts come from `core/ids.ts` (22 agents plus `generic`).
- **Only commands that exist,** with their real flags. Sigil isn't on
npm yet, so the quick start runs from a clone and says so.
- **Honest status:** all capabilities are `submitted` and `untested`,
the first local lab runs hit ceiling effects, and no number is published
until a run produced it. The header image is the site's OG image,
rendered from the live registry, so it can't drift.
- **No named-competitor comparison table.** The claims about other tools
can't be re-verified today, so they stay out.

Also corrects `spec/METHODOLOGY.md`, which said evidence "are published
with GitHub artifact attestations" in the present tense. The CI proving
workflow isn't live yet, so it now says CI-produced evidence is attested
and local runs are labelled local until then.

The catalog line lists what is on main at this commit. When #17 and #19
land, it gains Next.js, Svelte, Vue, Angular and OAuth.
dgesteves added a commit that referenced this pull request Nov 2, 2026
…es (#21)

CodeQL default setup cannot ignore paths. Every proof suite adds
deliberately imperfect starting repos, test harnesses and reference
solutions that only ever run inside the lab sandbox, and CodeQL flags
them. #17 (OAuth) and #19 (web) brought 25 such alerts, all under
`tasks/`: HTML-stripping helpers in Next.js test harnesses,
prototype-pollution warnings on the toy database of a starting repo, and
rate-limit and CSRF heuristics on a demo OIDC login app.

This moves CodeQL to an advanced workflow with the same default query
suite and the same languages (`actions`, `javascript-typescript`). It
runs on PRs, on main and weekly, is SHA-pinned like the rest of CI, and
has least-privilege permissions. `.github/codeql/codeql-config.yml`
ignores `tasks/`, `site/fixtures/` and `packages/sigil/test/fixtures/`.
Capability content keeps its own scanner job ("Scan the catalog").

**Before merging:** default setup has to be switched off, or GitHub
rejects the advanced upload. Settings → Code security → CodeQL analysis
→ Switch to advanced, or:

```sh
gh api -X PATCH repos/dgesteves/sigil/code-scanning/default-setup -f state=not-configured
```

Then re-run this PR's checks, merge it, and re-run CodeQL on #17 and
#19.
@dgesteves
dgesteves merged commit 2430299 into main Nov 2, 2026
8 checks passed
@dgesteves
dgesteves deleted the feat/catalog-infra-oauth branch November 2, 2026 09:55
dgesteves added a commit that referenced this pull request Nov 2, 2026
Updates the Status line after #17 and #19: adds Next.js 16, Vue 3.5,
Svelte 5, Angular 22 and OAuth/OIDC. Counts come from
`catalog/skills/**/sigil.yaml` (16) and `tasks/*/*/task.yaml` (48) on
main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants