Skip to content

docs(oagw): design for OAuth2 authorization-code auth plugin (DRAFT)#4225

Draft
striped-zebra-dev wants to merge 1 commit into
constructorfabric:mainfrom
striped-zebra-dev:oagw-oauth2-authcode-design
Draft

docs(oagw): design for OAuth2 authorization-code auth plugin (DRAFT)#4225
striped-zebra-dev wants to merge 1 commit into
constructorfabric:mainfrom
striped-zebra-dev:oagw-oauth2-authcode-design

Conversation

@striped-zebra-dev

@striped-zebra-dev striped-zebra-dev commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Design only - no implementation. This is a DRAFT for discussion and will need another round of re-thinking before it is built.

Proposes per-user OAuth2 Authorization Code (RFC 6749 §4.1 + PKCE) support in OAGW as a new built-in auth plugin (oauth2_auth_code), a peer of the existing client-credentials plugin. Adds an ADR and five scenarios; changes no code.

What's in here

  • docs/adr-oauth2-authorization-code-auth-plugin.md (new) - the design.
  • scenarios/proxy-api/authentication/positive-9.11..negative-9.15 (new) - enrollment, refresh, authorization-required, refresh-rejection, cross-subject isolation.
  • docs/adr-oauth2-client-credentials-auth-plugin.md, scenarios/INDEX.md (edited) - cross-links / index entries.

Design principles it commits to

  • Standard, not bespoke. OAGW is an ordinary OAuth 2.1 client; token flow, the callback, and the re-auth signal (WWW-Authenticate) follow published RFCs. Aligns with the MCP authorization profile and the IETF BFF pattern.
  • No growth of the cross-gear SDK. ServiceGatewayClientV1 is unchanged; re-authorization rides the existing CanonicalError / reason channel. The consumer surface is "call proxy, handle one typed error."
  • Topology-agnostic callback. One callback handler works whether OAGW is exposed (AS redirects directly) or internal (BFF reverse-proxies the callback). Nothing depends on exposing OAGW - that can be deferred or permanently refused.
  • Design-consistent storage. Durable per-user token in CredStore (Private sharing, opaque value) via its already-designed API; transient PKCE/CSRF state stays out of CredStore (it has no expiry by design) in an ephemeral TTL store.

⚠️ Not final - open questions to resolve before implementation

This is a first coherent cut, not a settled design. See the ADR's Open Questions section. Known blockers:

  1. Multi-instance pending store - shared ephemeral store vs. state-routed callbacks. Unresolved.
  2. Callback exposure policy - we may decide never to expose OAGW's callback publicly; the BFF-relay contract is not specified here.
  3. Stateless-state variant - pre-registered client + self-contained encrypted state would remove the pending store and multi-instance constraint entirely. Evaluate first.
  4. Durable CredStore backend - assumed prerequisite (write-capable, Secrets:Write); not part of this design.
  5. toolkit-auth additions - authorization-code primitives named but not designed in detail.
  6. Re-auth challenge shape - exact WWW-Authenticate parameters still to be pinned and round-trip-tested.

Please review the direction and trade-offs rather than treating this as implementation-ready. Follow-up ADRs likely for (1)/(2)/(3).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a draft design for OAuth2 authorization-code authentication with PKCE, including enrollment, callback, token refresh, status, and revocation flows.
    • Defined interactive user authorization, bearer-token injection, transparent refresh, and re-authorization behavior.
  • Documentation

    • Added comprehensive scenarios covering enrollment, refresh, missing or rejected authorization, token revocation, and cross-subject token isolation.
    • Updated OAuth2 client-credentials documentation to reference the separate authorization-code design.

Add a design-only ADR and scenarios for per-user OAuth2 authorization-code
(RFC 6749 §4.1 + PKCE) support in OAGW, as a peer of the existing
client-credentials auth plugin. No implementation.

- OAGW acts as a standard OAuth 2.1 client: discovery (RFC 8414/9728), DCR
  (RFC 7591), PKCE, code/refresh exchange live in toolkit-auth; the plugin
  orchestrates them.
- Standard OAuth callback endpoint (no custom "complete" step), reachable
  either directly (OAGW exposed) or reverse-proxied by the BFF (OAGW internal);
  the handler is identical in both topologies.
- Re-authorization surfaces through the existing CanonicalError/reason channel
  (WWW-Authenticate challenge, AUTHORIZATION_REQUIRED); the cross-gear
  ServiceGatewayClientV1 SDK is unchanged.
- Per-user token stored in CredStore (Private sharing, opaque value) using its
  already-designed put/get/delete; transient PKCE/CSRF state stays out of
  CredStore in an ephemeral TTL store.

DRAFT / not final: open questions remain (multi-instance pending store,
callback-exposure policy, stateless-state variant, durable CredStore backend,
toolkit-auth additions, WWW-Authenticate challenge shape). See the ADR's
Open Questions section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a draft ADR for a built-in OAuth2 authorization-code PKCE plugin and documents enrollment, token refresh, authorization-required failures, revocation, and subject-isolation scenarios.

Changes

OAuth2 authorization-code plugin design

Layer / File(s) Summary
Plugin architecture and contracts
gears/system/oagw/docs/adr-oauth2-authorization-code-auth-plugin.md
Defines the plugin, configuration, REST endpoints, typed authorization challenge, deployment models, and pending and durable credential records.
Enrollment and proxy token flows
gears/system/oagw/docs/adr-oauth2-authorization-code-auth-plugin.md, gears/system/oagw/docs/adr-oauth2-client-credentials-auth-plugin.md
Describes callback enrollment, token exchange and storage, bearer injection, refresh, revocation handling, registration, and related ADR scope.
Enrollment and refresh scenarios
gears/system/oagw/scenarios/INDEX.md, gears/system/oagw/scenarios/proxy-api/authentication/positive-9.11-oauth2-authorization-code-enrollment.md, gears/system/oagw/scenarios/proxy-api/authentication/positive-9.12-oauth2-authorization-code-refresh.md
Adds indexed scenarios for interactive enrollment, bearer injection, and transparent refresh.
Failure and subject-isolation scenarios
gears/system/oagw/scenarios/proxy-api/authentication/negative-9.13-oauth2-authorization-code-authorization-required.md, gears/system/oagw/scenarios/proxy-api/authentication/negative-9.14-oauth2-authorization-code-refresh-rejected.md, gears/system/oagw/scenarios/proxy-api/authentication/negative-9.15-oauth2-authorization-code-cross-subject-isolation.md
Documents missing-token failures, refresh rejection and revocation, and cross-subject and cross-tenant credential isolation.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the ADR and scenario docs for the OAuth2 authorization-code auth plugin.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@striped-zebra-dev
striped-zebra-dev marked this pull request as draft July 17, 2026 15:20

@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.

Actionable comments posted: 5

🤖 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.

Inline comments:
In `@gears/system/oagw/docs/adr-oauth2-authorization-code-auth-plugin.md`:
- Around line 347-353: Update the refresh flow around UserTokenStore.load,
refresh_token, UserTokenStore.save, and UserTokenStore.delete to coordinate
concurrent refreshes for each (subject, upstream_id), using per-key
single-flight locking or compare-and-swap. Ensure only one request refreshes a
given record at a time, and on rejection delete only the version that failed
rather than a newer rotated record.

In
`@gears/system/oagw/scenarios/proxy-api/authentication/negative-9.13-oauth2-authorization-code-authorization-required.md`:
- Around line 39-40: Update the OAuth2 authorization-required scenario assertion
around FailureReason::AuthorizationRequired to match the canonical consumer
contract, using upstream_id and authorize instead of
authorization_url/authorization_uri; if the ADR defines the alternate shape as
canonical, update FailureReason::AuthorizationRequired and its projection
accordingly, then keep the scenario and contract consistent.

In
`@gears/system/oagw/scenarios/proxy-api/authentication/positive-9.11-oauth2-authorization-code-enrollment.md`:
- Around line 29-30: Validate the caller-controlled return_to value in the OAuth
authorization flow before persisting it or using it for redirects. Update the
POST /oagw/v1/upstreams/{id}/oauth/authorize handling and corresponding callback
path to accept only same-origin or server-side allowlisted destinations,
rejecting all other URLs.
- Around line 44-49: The OAuth callback flow must consume or atomically mark the
pending state as used before exchanging the authorization code. Update the state
lookup/consumption behavior described in the callback flow so any later callback
is rejected, including when token exchange fails, while preserving the existing
matching, verifier, redirect URI, and token-storage behavior.

In
`@gears/system/oagw/scenarios/proxy-api/authentication/positive-9.12-oauth2-authorization-code-refresh.md`:
- Around line 39-44: Update the refresh-token flow documented in the expected
behavior to serialize writes and deletes per subject, using single-flight or
conditional update/delete semantics. Ensure a stale refresh attempt cannot
delete credentials rotated by another request, and preserve writing rotated
tokens under the same Private reference before continuing with the refreshed
bearer token.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8146871f-426e-4df7-98ae-41fb22999251

📥 Commits

Reviewing files that changed from the base of the PR and between e7361e7 and 9c2897d.

📒 Files selected for processing (8)
  • gears/system/oagw/docs/adr-oauth2-authorization-code-auth-plugin.md
  • gears/system/oagw/docs/adr-oauth2-client-credentials-auth-plugin.md
  • gears/system/oagw/scenarios/INDEX.md
  • gears/system/oagw/scenarios/proxy-api/authentication/negative-9.13-oauth2-authorization-code-authorization-required.md
  • gears/system/oagw/scenarios/proxy-api/authentication/negative-9.14-oauth2-authorization-code-refresh-rejected.md
  • gears/system/oagw/scenarios/proxy-api/authentication/negative-9.15-oauth2-authorization-code-cross-subject-isolation.md
  • gears/system/oagw/scenarios/proxy-api/authentication/positive-9.11-oauth2-authorization-code-enrollment.md
  • gears/system/oagw/scenarios/proxy-api/authentication/positive-9.12-oauth2-authorization-code-refresh.md

Comment on lines +347 to +353
UserTokenStore.load(ctx, upstream_id)
None -> Err(AuthorizationRequired(resource))
valid (> now + margin) -> inject Authorization: Bearer; Ok
expired, has refresh -> refresh_token(...)
ok -> UserTokenStore.save(rotated); inject Bearer; Ok
rejected -> UserTokenStore.delete(); Err(AuthorizationRequired(resource))
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Serialize refreshes per (subject, upstream_id) or use compare-and-swap.

Two concurrent proxy requests can load the same expiring record. If refresh-token rotation is enabled, one request can save the rotated record while the other refreshes with the old token, receives rejection, and deletes the newly saved record. Use single-flight/per-key locking or conditional writes, and only delete the version that failed.

🤖 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 `@gears/system/oagw/docs/adr-oauth2-authorization-code-auth-plugin.md` around
lines 347 - 353, Update the refresh flow around UserTokenStore.load,
refresh_token, UserTokenStore.save, and UserTokenStore.delete to coordinate
concurrent refreshes for each (subject, upstream_id), using per-key
single-flight locking or compare-and-swap. Ensure only one request refreshes a
given record at a time, and on rejection delete only the version that failed
rather than a newer rotated record.

Comment on lines +39 to +40
- `oagw_sdk::reason::auth::FailureReason::from(&err)` projects to
`AuthorizationRequired { authorization_url }`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

printf 'FILES:\n'
git ls-files 'gears/system/oagw/scenarios/proxy-api/authentication/negative-9.13-oauth2-authorization-code-authorization-required.md' \
  'gears/system/oagw' | sed -n '1,200p'

printf '\nSEARCH AUTH FAILURE SHAPES:\n'
rg -n --hidden --glob '!**/.git/**' \
  'AuthorizationRequired|authorization_url|upstream_id|authorize' \
  gears/system/oagw | sed -n '1,240p'

printf '\nSCENARIO CONTEXT:\n'
cat -n gears/system/oagw/scenarios/proxy-api/authentication/negative-9.13-oauth2-authorization-code-authorization-required.md | sed -n '1,120p'

Repository: constructorfabric/gears-rust

Length of output: 47087


🏁 Script executed:

set -euo pipefail
pwd
git ls-files | rg '^gears/system/oagw/scenarios/proxy-api/authentication/negative-9.13-oauth2-authorization-code-authorization-required\.md$|^gears/system/oagw/'

Repository: constructorfabric/gears-rust

Length of output: 20426


🏁 Script executed:

set -euo pipefail

printf 'ADR EXCERPT:\n'
sed -n '180,220p;300,315p' gears/system/oagw/docs/adr-oauth2-authorization-code-auth-plugin.md | cat -n

printf '\nSDK REASON EXCERPT:\n'
sed -n '1,260p' gears/system/oagw/oagw-sdk/src/reason.rs | cat -n | sed -n '1,260p'

printf '\nSCENARIO INDEX EXCERPT:\n'
sed -n '200,214p' gears/system/oagw/scenarios/INDEX.md | cat -n

Repository: constructorfabric/gears-rust

Length of output: 14902


🏁 Script executed:

set -euo pipefail

rg -n 'AuthorizationRequired|authorization_url|authorize\b|upstream_id' gears/system/oagw/oagw-sdk/src gears/system/oagw/oagw/src | sed -n '1,240p'

Repository: constructorfabric/gears-rust

Length of output: 12861


Align FailureReason::AuthorizationRequired with the ADR. The scenario currently documents authorization_url/authorization_uri, but the consumer contract projects AuthorizationRequired { upstream_id, authorize }. Update the scenario or the canonical contract so tests don’t lock in the wrong shape.

🤖 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
`@gears/system/oagw/scenarios/proxy-api/authentication/negative-9.13-oauth2-authorization-code-authorization-required.md`
around lines 39 - 40, Update the OAuth2 authorization-required scenario
assertion around FailureReason::AuthorizationRequired to match the canonical
consumer contract, using upstream_id and authorize instead of
authorization_url/authorization_uri; if the ADR defines the alternate shape as
canonical, update FailureReason::AuthorizationRequired and its projection
accordingly, then keep the scenario and contract consistent.

Comment on lines +29 to +30
1. `POST /oagw/v1/upstreams/{id}/oauth/authorize` with
`{ "scope": "profile email", "return_to": "https://app.example/connected" }`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Constrain return_to before persisting and redirecting.

This accepts a caller-controlled URL and later redirects the browser to it. Require a same-origin or server-side allowlisted destination; otherwise the callback becomes an open-redirect/phishing primitive.

Also applies to: 49-50

🤖 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
`@gears/system/oagw/scenarios/proxy-api/authentication/positive-9.11-oauth2-authorization-code-enrollment.md`
around lines 29 - 30, Validate the caller-controlled return_to value in the
OAuth authorization flow before persisting it or using it for redirects. Update
the POST /oagw/v1/upstreams/{id}/oauth/authorize handling and corresponding
callback path to accept only same-origin or server-side allowlisted
destinations, rejecting all other URLs.

Comment on lines +44 to +49
- The callback matches the pending record by `state` (single-use) and exchanges
the `code` with the `code_verifier` and the same `redirect_uri` from the
pending record (so exposed and relayed paths behave identically).
- The resulting token is stored in `cred_store` with `SharingMode::Private`,
scoped to the calling subject; the stored value is opaque (never logged).
- The pending record is removed after a successful exchange, and the callback

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Consume OAuth state before exchanging the code.

The checklist says state is single-use but also says the pending record is removed only after successful exchange. Atomically consume or mark the state used before token exchange, and reject all subsequent callbacks even if the exchange fails.

🤖 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
`@gears/system/oagw/scenarios/proxy-api/authentication/positive-9.11-oauth2-authorization-code-enrollment.md`
around lines 44 - 49, The OAuth callback flow must consume or atomically mark
the pending state as used before exchanging the authorization code. Update the
state lookup/consumption behavior described in the callback flow so any later
callback is rejected, including when token exchange fails, while preserving the
existing matching, verifier, redirect URI, and token-storage behavior.

Comment on lines +39 to +44
Expected:
- The plugin exchanges the refresh token at the token endpoint.
- The rotated record (new access token, and rotated refresh token when the
server issues one) is written back to `cred_store` under the same
`Private` reference.
- The request proceeds with the refreshed bearer token.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the scenario file and related index around the referenced behavior.
printf '\n== scenario file ==\n'
sed -n '1,120p' gears/system/oagw/scenarios/proxy-api/authentication/positive-9.12-oauth2-authorization-code-refresh.md

printf '\n== scenario index matches ==\n'
rg -n "refresh|rejected refresh|delete|cred_store|Private reference|rotate" gears/system/oagw/scenarios/INDEX.md gears/system/oagw/scenarios/proxy-api/authentication -g '*.md'

printf '\n== nearby auth scenarios ==\n'
fd -a 'positive-9.12-oauth2-authorization-code-refresh.md|INDEX.md' gears/system/oagw/scenarios

Repository: constructorfabric/gears-rust

Length of output: 8720


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== negative scenario ==\n'
sed -n '1,140p' gears/system/oagw/scenarios/proxy-api/authentication/negative-9.14-oauth2-authorization-code-refresh-rejected.md

printf '\n== auth ADR mentions ==\n'
rg -n "single-flight|compare-and-swap|CAS|conditional delete|refresh token|rotate|mutex|lock|serialization|version" gears/system/oagw/docs gears/system/oagw/scenarios -g '*.md'

printf '\n== auth plugin related scenario titles ==\n'
rg -n "^#|^## Scenario|Mechanism|Expected:" gears/system/oagw/scenarios/proxy-api/authentication/*oauth2* -g '*.md'

Repository: constructorfabric/gears-rust

Length of output: 44018


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== auth ADR refresh flow ==\n'
sed -n '300,390p' gears/system/oagw/docs/adr-oauth2-authorization-code-auth-plugin.md

printf '\n== search for auth plugin implementation references ==\n'
rg -n "oauth2_auth_code|UserTokenStore|refresh token|invalid_grant|AUTHORIZATION_REQUIRED|cred_store|Private" gears -g '*.{rs,md,ts,tsx,py,json,yml,yaml}'

printf '\n== candidate source files ==\n'
fd -a 'oauth2|auth.*code|token.*store|cred_store' gears -t f

Repository: constructorfabric/gears-rust

Length of output: 48955


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== ADR around token store semantics ==\n'
sed -n '250,290p' gears/system/oagw/docs/adr-oauth2-authorization-code-auth-plugin.md

printf '\n== token-store related lines ==\n'
rg -n "UserTokenStore|single-use|single-flight|save\\(|delete\\(|refreshes|rotated|same `Private`" gears/system/oagw/docs/adr-oauth2-authorization-code-auth-plugin.md gears/system/oagw/scenarios/proxy-api/authentication -g '*.md'

Repository: constructorfabric/gears-rust

Length of output: 6625


Serialize refresh-token writes and deletes. Concurrent refreshes can race: one request may rotate the token while another gets invalid_grant and deletes the same Private record, wiping the fresh credentials and forcing re-authorization. Use per-subject single-flight or conditional update/delete semantics so only the still-current token can be removed.

🤖 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
`@gears/system/oagw/scenarios/proxy-api/authentication/positive-9.12-oauth2-authorization-code-refresh.md`
around lines 39 - 44, Update the refresh-token flow documented in the expected
behavior to serialize writes and deletes per subject, using single-flight or
conditional update/delete semantics. Ensure a stale refresh attempt cannot
delete credentials rotated by another request, and preserve writing rotated
tokens under the same Private reference before continuing with the refreshed
bearer token.

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