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 stacked security repair for #81. The production ordering on the stack could mint a GitHub installation token before the distributed single-use replay claim rejected a duplicate valid OIDC credential. This PR moves the atomic replay decision to the verified-and-authorized boundary before privileged token minting, without allowing unverified jti values to consume replay state.

This PR is stacked on #71 (docs/architecture-trust-boundaries) because it changes the same credential-exchange source surface. It remains Draft while #71 is unintegrated and until the repair is refreshed and revalidated against the protected lineage.

Test-first lineage

  • ed1e7c529712028c4f6e5c5bf928fb07bba48a18: RED contract requiring a valid signed replay to return 401 ERR_AUTH_REPLAY with zero POSTs to /app/installations/{id}/access_tokens.
  • 12c95f033437cfc7efbdfd61036775921267ebf2: core exposes cryptographically verified jti/exp after target-repository authorization and before createInstallationToken().
  • 1e893dda8ba063ffaaa694521f4cfac80d1f86dc: wrapper requires the distributed replay binding before credential-bearing core execution and consumes internal pre-mint replay proof.
  • 5acd2f0f3c69a9e80f3878ab9359dd1829c061b1: first-use regression requires replay_claim before token_mint.
  • 89f1d736c04a259363e566b2d269ad37fc6f8194: focused APA 7 doctoring for the security-ordering rationale.
  • 82506bf4697664e94b27b0312347f0a82e5fd15d: localizes the operational/security doctoring to Korean while preserving technical semantics, identifiers, URLs and citations.
  • 1a8317867713c003cafe3f6a46fd8490ba0bf789: repairs the stale distributed-rate-limit fixture exposed by exact-head CI.
  • 9dfbe1007c43e5c3ca1381ad2a57d748776f0c88: current exact head; adds defensive wrapper coverage for bounded replay-unavailable diagnostics and unexpected replay-guard failures. The one-commit comparison from 1a831... changes only test/worker-defensive-replay.test.ts (+37/-2), not production source.

Security boundary after the change

Intended production 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

The unverified payload prefilter cannot reserve replay state. The claim occurs only after signature, issuer, audience, repository/workflow, time and target-repository authorization succeed. Missing replay binding, replay conflict, unavailable replay state, missing bounded verified replay claims, or malformed proof fails closed before token mint.

Current stack identity

Freshly revalidated:

Refreshing this branch while #71 remains unintegrated would invalidate exact-head evidence without creating independent integration value and could race the shared credential-exchange surface. The live-base divergence is therefore retained as explicit stack evidence rather than hidden behind stale PR base_sha metadata.

Exact-head CI evidence and RCA

For exact head 9dfbe1007c43e5c3ca1381ad2a57d748776f0c88:

  • ci run 31316088354: terminal failure. The full application suite passed 679/679 tests with configured production statement, branch, function, and line coverage all 100%. The only failing acceptance boundary is the repository-wide npm audit --audit-level=high gate on inherited nanoid@3.3.16 (GHSA-2v37-7h3g-55p8). The root remediation is isolated in PR fix(security): update transitive nanoid for CVE-2026-67213 #76; duplicating that lockfile change into this security stack is rejected as a wrong-layer fix.
  • reviewer-ci run 31316088356: terminal success.
  • Central Security Scan: absent on this feature-base stack; this is defer_until_trigger, not passing evidence.
  • Formal reviews: CodeRabbit COMMENTED predecessor-range evidence only; no eligible independent non-author APPROVED review.
  • Current inline review threads: 0.

The earlier fixture defect is no longer the failing boundary: current CI shows test/distributed-rate-limit.test.ts and the replay-order regression suite passing. No predecessor-head CI, review, model, or synthetic-merge evidence is promoted to exact-head approval or merge authority.

Remediation feasibility classification

Required acceptance before merge

  1. fix(security): update transitive nanoid for CVE-2026-67213 #76 integrates under actual live governance and removes the inherited dependency-security blocker.
  2. fix(security): bind workflow source and document MSA boundaries #71 reaches a stable accepted exact head and protected-integrates.
  3. Rebuild or refresh only fix(security): claim verified OIDC replay before token mint #83's unique repair onto the then-current protected base without destructive history shortcuts.
  4. Re-run focused replay-order tests and the full application suite on that refreshed exact head, retaining 100% owned production coverage without exclusions or waivers.
  5. Make central Security Scan eligible and require terminal success on the refreshed exact head.
  6. Re-run current automated review and satisfy actual live ruleset and qualifying independent-approval requirements.
  7. Update canonical Architecture/UML/TRD/traceability/gap-audit on the integrated lineage so they describe pre-mint replay rejection as protected behavior, not merely proposed stack behavior.
  8. Perform protected-main operational proof that replayed valid OIDC credentials produce zero installation-token POSTs.

No repair workflow, self-modifying Action, branch-patching contents:write, broader App permission, audit waiver, protection bypass, merge authority, release authority or deployment authority is introduced.

Closes #81 only after protected integration and exact-head/operational verification.

@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