Skip to content

fix(security): claim verified OIDC replay before token mint - #83

Draft
seonghobae wants to merge 8 commits into
docs/architecture-trust-boundariesfrom
fix/replay-before-token-mint
Draft

fix(security): claim verified OIDC replay before token mint#83
seonghobae wants to merge 8 commits into
docs/architecture-trust-boundariesfrom
fix/replay-before-token-mint

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Purpose

Test-first repair for #81: a replayed but otherwise valid GitHub Actions OIDC credential must be rejected by the distributed single-use claim after cryptographic/target authorization but before createInstallationToken(), so duplicate requests cannot cross the privileged GitHub token-mint boundary. Unverified jti values must never consume replay state.

Intended security ordering

distributed rate limit
→ exact workflow ref/SHA prefilter
→ replay binding existence
→ cryptographic OIDC verification
→ target repository authorization
→ atomic distributed replay claim
→ GitHub App installation-token creation
→ response

Missing replay binding, replay conflict, unavailable replay state, missing bounded verified replay claims, or malformed proof fails closed before token mint.

Fresh stack identity

Protected main is now c85d710804139c0697d7ef8fa47d02b1389e6d84; #76 is integrated and the old repository-wide nanoid audit failure is historical. That does not make this stale feature-base head current or transfer its old checks.

Test-first lineage

  • ed1e7c529712028c4f6e5c5bf928fb07bba48a18: RED valid-signed-replay regression requiring 401 ERR_AUTH_REPLAY and zero installation-token POSTs.
  • 12c95f033437cfc7efbdfd61036775921267ebf2: exposes verified jti/exp only after target authorization and before createInstallationToken().
  • 1e893dda8ba063ffaaa694521f4cfac80d1f86dc: wrapper requires distributed replay binding and consumes pre-mint proof.
  • 5acd2f0f3c69a9e80f3878ab9359dd1829c061b1: first-use ordering requires replay_claim before token_mint.
  • 9dfbe1007c43e5c3ca1381ad2a57d748776f0c88: current stale head; defensive replay-unavailable/unexpected-guard coverage.

Historical exact-head evidence

For 9dfbe1007c43e5c3ca1381ad2a57d748776f0c88, application CI 31316088354 reached 679 tests and configured statements/branches/functions/lines 100% before the then-inherited nanoid audit; reviewer-ci 31316088356 was terminal success; central Security Scan was absent because the PR targets a feature base and therefore remained non-passing defer_until_trigger. Current inline review threads were 0 at the last exact read.

The old audit failure no longer describes protected main, but all evidence remains bound to this stale head and does not transfer after convergence.

Current dependency / ownership rule

Do not refresh or patch #83 while #71 is a moving/diverged canonical shared-source owner. The safe path is:

  1. converge fix(security): bind workflow source and document MSA boundaries #71 onto current protected main without losing fix(security): update transitive nanoid for CVE-2026-67213 #76/current successor truth, reacquire exact-head documentation/application evidence, and integrate it under live governance;
  2. from the then-current protected head, rebuild only fix(security): claim verified OIDC replay before token mint #83's six-path replay-order delta rather than reusing stale ancestry;
  3. establish RED/GREEN again where the protected source changed materially, run focused replay tests and full application verification, and retain exact 100% owned production coverage without broad exclusions;
  4. require fresh eligible protected-base Security Scan plus current automated/formal review and live chore(governance): protect main and enforce release checks #27/chore(operations): provision and activate hourly maintainer App #29 governance;
  5. update integrated Architecture/UML/TRD/Traceability so pre-mint replay rejection becomes protected truth;
  6. perform protected-main operational proof that a replayed valid OIDC credential causes zero installation-token POSTs;
  7. close issue security(replay): claim verified OIDC use before GitHub token creation #81 only after protected integration and operational proof.

No repair workflow, self-modifying Action, broader App permission, audit waiver, protection bypass, synthetic approval, merge/release/deployment authority is introduced.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

OIDC 교환 흐름이 서명·권한 검증 후 replay claim을 등록하고, 그 다음 GitHub 설치 토큰을 발급하도록 변경되었습니다. replay 충돌과 보호 기능 불가 상태는 각각 401과 503으로 처리됩니다.

Changes

OIDC replay 보호

Layer / File(s) Summary
검증된 claim과 토큰 발급 경계
src/index.ts, test/replay-before-token-mint.test.ts
jtiexp를 검증한 뒤 단일 사용 claim을 등록합니다. replay 충돌이나 저장소 불가 상태에서는 GitHub 토큰을 발급하지 않습니다.
Worker 사전 차단과 응답 변환
src/worker.ts, src/index.ts
replay guard가 없으면 /exchange를 503으로 중단합니다. 선행 보호 응답은 single-use 헤더와 상태로 변환됩니다.
순서 검증과 설계 문서
test/replay-before-token-mint.test.ts, docs/doctoring/replay-before-token-mint.md
RS256 토큰과 Durable Object mock으로 replay 거부 및 claim-before-mint 순서를 검증합니다. 설계와 fail-closed 조건을 문서화합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Worker
  participant ReplayGuard
  participant GitHubAPI
  Client->>Worker: POST /exchange
  Worker->>ReplayGuard: claim verified jti and exp
  ReplayGuard-->>Worker: claim accepted
  Worker->>GitHubAPI: create installation token
  GitHubAPI-->>Worker: token response
  Worker-->>Client: 200 single-use response
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning 구현은 핵심 순서를 충족하지만, #81의 동시성·저장소 장애 테스트와 일부 필수 문서 갱신은 확인되지 않습니다. 동시성 및 replay-storage 장애 회귀 테스트를 추가하고 docs/oidc-replay-protection.md, CHANGELOG.md 및 아키텍처 문서를 갱신하십시오.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed 변경 사항은 OIDC replay claim 순서, 오류 처리, 회귀 테스트 및 관련 보안 문서에 집중되어 있어 #81 범위를 벗어난 변경이 확인되지 않습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 검증된 OIDC replay 상태를 토큰 발급 전에 등록하는 핵심 변경을 정확하고 간결하게 설명합니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/replay-before-token-mint

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please perform a substantive security review of exact head 89f1d736c04a259363e566b2d269ad37fc6f8194. Verify that unverified JWT payload data still cannot reserve replay state; the distributed replay binding is required before the production wrapper enters the credential-bearing core; cryptographic OIDC verification and target-repository authorization occur before replay mutation; a replay conflict/unavailable state returns before createInstallationToken(); successful first-use ordering is replay claim before GitHub access-token mint; the legacy outer post-success claim is not taken on the real core proof-header path; and no credential/authority boundary was widened. Treat your verdict as diagnostic model evidence only, not formal GitHub approval or merge authority.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected='89f1d736c04a259363e566b2d269ad37fc6f8194'
echo '=== checked-out commit ==='
git rev-parse HEAD
git show -s --format='%H%n%P%n%s%n%D' HEAD
echo '=== expected commit available ==='
git cat-file -t "$expected"
echo '=== expected commit summary ==='
git show -s --format='%H%n%P%n%s%n%D' "$expected"

echo '=== changed files from first parent ==='
git diff-tree --no-commit-id --name-status -r "$expected"

echo '=== source outlines ==='
ast-grep outline src/index.ts --items all --view expanded || true
ast-grep outline src/worker.ts --items all --view expanded || true
ast-grep outline test/replay-before-token-mint.test.ts --items all --view expanded || true

echo '=== security-relevant source references ==='
rg -n -C 6 -e 'verifyGithubOidcJwt|createInstallationToken|replay|REPLAY|x-oidc-replay-protection|targetRepository|repository' src/index.ts src/worker.ts test/replay-before-token-mint.test.ts

echo '=== exact-head patch ==='
git diff "${expected}^" "$expected" -- src/index.ts src/worker.ts test/replay-before-token-mint.test.ts

echo '=== package scripts and dependency authority surface ==='
if [ -f package.json ]; then cat package.json; fi
if [ -f wrangler.toml ]; then cat wrangler.toml; fi
if [ -f wrangler.jsonc ]; then cat wrangler.jsonc; fi

echo '=== PR metadata and exact-head checks ==='
gh pr view 83 --repo ContextualWisdomLab/noema --json number,state,isDraft,headRefOid,baseRefName,baseRefOid,mergeStateStatus,statusCheckRollup,files \
  --jq '{number,state,isDraft,headRefOid,baseRefName,baseRefOid,mergeStateStatus,checks:[.statusCheckRollup[]|{name,status,conclusion,detailsUrl}],files:[.files[]|{path,additions,deletions]}]}' || true

Length of output: 52580


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='89f1d736c04a259363e566b2d269ad37fc6f8194'
base=$(git merge-base "$head" "origin/docs/architecture-trust-boundaries" 2>/dev/null || git merge-base "$head" "$head^")
printf '=== merge base ===\n%s\n' "$base"

echo '=== exact verification and core exchange path ==='
sed -n '352,410p' src/index.ts
sed -n '511,612p' src/index.ts

echo '=== complete production wrapper path ==='
sed -n '59,270p' src/worker.ts
sed -n '380,555p' src/worker.ts

echo '=== replay guard implementation ==='
replay_file=$(fd -t f '^oidc-replay\.ts$' src | head -n 1)
printf '%s\n' "$replay_file"
ast-grep outline "$replay_file" --items all --view expanded
cat -n "$replay_file"

echo '=== runtime entrypoint and bindings ==='
runtime_file=$(fd -t f '^runtime-entrypoint\.ts$' src | head -n 1)
printf '%s\n' "$runtime_file"
cat -n "$runtime_file"

echo '=== focused regression test bodies ==='
sed -n '163,255p' test/replay-before-token-mint.test.ts

echo '=== replay-related tests and direct-core tests ==='
rg -n -C 4 -e 'NOEMA_OIDC_REPLAY_GUARD|verified-before-mint|claimOidcTokenUsage|createInstallationToken|ERR_AUTH_REPLAY' test src -g '*.test.ts' -g '*.ts'

echo '=== cumulative branch diff from merge base ==='
git diff --stat "$base" "$head"
git diff --find-renames --find-copies "$base" "$head" -- src/index.ts src/worker.ts src/oidc-replay.ts src/runtime-entrypoint.ts wrangler.toml package.json test/replay-before-token-mint.test.ts docs/doctoring/replay-before-token-mint.md

echo '=== requested-commit documentation ==='
cat -n docs/doctoring/replay-before-token-mint.md

echo '=== exact-head PR and check metadata ==='
gh pr view 83 --repo ContextualWisdomLab/noema --json number,state,isDraft,headRefOid,baseRefName,baseRefOid,mergeStateStatus,statusCheckRollup,files

Length of output: 92060


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='89f1d736c04a259363e566b2d269ad37fc6f8194'
pr_base='52c6f2bce0f89760367807df4c316f2137ad5a97'

echo '=== actual PR-base availability and ancestry ==='
git cat-file -t "$pr_base"
git merge-base --is-ancestor "$pr_base" "$head" && echo 'PR base is an ancestor of head'
git log --oneline --decorate --ancestry-path "${pr_base}..${head}"

echo '=== complete actual PR diff ==='
git diff --stat "$pr_base" "$head"
git diff --name-status "$pr_base" "$head"
git diff --find-renames --find-copies "$pr_base" "$head" -- \
  src/index.ts src/worker.ts src/oidc-replay.ts src/runtime-entrypoint.ts \
  wrangler.toml package.json \
  '.github/**' test/replay-before-token-mint.test.ts docs/doctoring/replay-before-token-mint.md

echo '=== production wrapper dispatch, including route gate ==='
sed -n '356,555p' src/worker.ts

echo '=== all exported fetch entrypoints and exchange delegations ==='
rg -n -C 4 -P '(export default|async fetch\s*\(|baseWorker\.fetch\s*\(|handleExchange\s*\(|createInstallationToken\s*\()' src -g '*.ts'

echo '=== authority-related changes in actual PR ==='
git diff -U4 "$pr_base" "$head" -- \
  wrangler.toml package.json \
  ':!src/index.ts' ':!src/worker.ts' ':!test/replay-before-token-mint.test.ts' ':!docs/doctoring/replay-before-token-mint.md'

Length of output: 38305


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== entrypoint imports and /exchange delegation ==='
sed -n '1,55p' src/entrypoint.ts
sed -n '300,365p' src/entrypoint.ts

echo '=== worker import and runtime route-chain references ==='
rg -n -C 3 -P '(from "./worker"|worker\.fetch\s*\(|from "./entrypoint"|entrypoint\.fetch\s*\()' src -g '*.ts'

Length of output: 6145


@seonghobae I reviewed exact head 89f1d736c04a259363e566b2d269ad37fc6f8194.

Diagnostic verdict: no security finding identified in the requested scope.

  • Unverified JWT payload data cannot reserve replay state. src/worker.ts decodes payload claims only for bounded workflow prefilter checks. It does not call claimOidcTokenUsage() before core verification.
  • The production /exchange route requires NOEMA_OIDC_REPLAY_GUARD before src/worker.ts calls baseWorker.fetch(). A missing binding returns 503 ERR_AUTH_REPLAY.
  • The effective production route is src/runtime-entrypoint.tssrc/entrypoint.tssrc/worker.tssrc/index.ts. The credential exchange does not bypass the wrapper.
  • src/index.ts performs verifyGithubOidcJwt() before target-repository authorization. It calls claimVerifiedOidcUsage() only after target authorization. It calls createInstallationToken() only after an accepted claim.
  • Replay conflict maps to 401 ERR_AUTH_REPLAY. Replay unavailability, invalid replay claims, and missing replay state map to 503 ERR_AUTH_REPLAY. These paths exit before createInstallationToken().
  • The focused regression uses an RS256-signed OIDC fixture. It asserts zero requests to /app/installations/{id}/access_tokens on replay. It also asserts the first-use order replay_claim before token_mint.
  • A real core success sets x-oidc-replay-protection: verified-before-mint. The wrapper changes this to single-use and returns before the legacy post-success claim path. The legacy claim remains reachable only for a success response without the internal proof header.
  • The actual PR diff changes only src/index.ts, src/worker.ts, the focused test, and documentation. It does not change GitHub App token permissions, workflow files, deployment bindings, secrets, dependencies, or outbound authority configuration.

Limitations

  • This is static source, route-chain, test, and diff evidence. I did not execute tests in the sandbox.
  • Exact-head ci / verify and reviewer-ci / reviewer were queued at inspection time. They are not passing evidence.
  • The PR remains Draft and is stacked on #71. This review is not formal GitHub approval or merge authority.
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
docs/doctoring/replay-before-token-mint.md (1)

1-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

운영 보안 문서를 한국어 중심으로 작성하세요.

이 문서는 replay 보호의 운영 및 보안 설계를 설명합니다. 본문과 제목을 한국어로 전환하세요. 코드, HTTP 헤더, 오류 코드, API 식별자, 인용 원문은 필요한 경우 영어로 유지하세요.

As per coding guidelines, operational and sales/acquisition documentation is largely Korean.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/doctoring/replay-before-token-mint.md` around lines 1 - 77, 이 문서의 제목과 설명
문단을 한국어 중심으로 번역해 운영·보안 설계 내용을 한국어로 제공하세요. 코드 블록, HTTP 헤더, 오류 코드, API 식별자, URL 및
필요한 인용 원문은 영어로 유지하고, 기술적 의미와 보안 순서·검증 조건은 변경하지 마세요.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@docs/doctoring/replay-before-token-mint.md`:
- Around line 1-77: 이 문서의 제목과 설명 문단을 한국어 중심으로 번역해 운영·보안 설계 내용을 한국어로 제공하세요. 코드
블록, HTTP 헤더, 오류 코드, API 식별자, URL 및 필요한 인용 원문은 영어로 유지하고, 기술적 의미와 보안 순서·검증 조건은
변경하지 마세요.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 028bd60b-bad8-48bc-b52b-c1f1cd744422

📥 Commits

Reviewing files that changed from the base of the PR and between 52c6f2b and 89f1d73.

📒 Files selected for processing (4)
  • docs/doctoring/replay-before-token-mint.md
  • src/index.ts
  • src/worker.ts
  • test/replay-before-token-mint.test.ts

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.

1 participant