Skip to content

auth-service: delegate NATS user permissions to scoped signing key#437

Open
Joey0538 wants to merge 1 commit into
mainfrom
claude/understand-present-content-3xk7ea
Open

auth-service: delegate NATS user permissions to scoped signing key#437
Joey0538 wants to merge 1 commit into
mainfrom
claude/understand-present-content-3xk7ea

Conversation

@Joey0538

@Joey0538 Joey0538 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Why

The account signing key the auth service uses to mint NATS user JWTs is being rotated to an account scoped signing key. The scope's role template declares the per-user subject grants centrally on the key itself (chat.user.{{tag(account)}}.>, chat.room.>, _INBOX.>, presence read + query), and NATS enforces those grants uniformly at connect time — regardless of what a JWT tries to assert.

Motivation for the rotation:

  • Single source of truth for permissions. Subject-shape changes now flow through one key config instead of every service that mints or embeds NATS permissions.
  • Uniform enforcement. NATS applies the scope template at auth time; a rogue or misconfigured issuer cannot widen permissions past what the scope declares.
  • Cleaner separation. The auth service authenticates a user (SSO → account) and stamps the account onto the JWT; permission policy lives with the key, not in application code.

Once the key was rotated, the auth service kept issuing JWTs (no server-side error) but frontend NATS connections failed with Authorization Violation because the JWT the auth service produced isn't compatible with a scoped signing key.

What changed

signNATSJWT no longer inlines Pub/Sub allow lists. Two adjustments make it compatible with the scoped signing key:

  1. Add the account:<account> tag so {{tag(account)}} in the scope template resolves per user (e.g. chat.user.alice.> for account alice).
  2. Zero UserPermissionLimits. jwt.NewUserClaims populates NatsLimits with -1 ("unlimited") defaults. Scope enforcement rejects any non-zero per-user limit — even one that encodes the same effective cap — because the scope owns limits. Clearing the struct produces the same wire format nsc emits under a scoped signing key.
func (h *AuthHandler) signNATSJWT(userPubKey, account string) (string, error) {
    uc := jwt.NewUserClaims(userPubKey)
    uc.Expires = h.jwtExpiryAt().Unix()
    uc.Tags.Add("account:" + account)
    uc.UserPermissionLimits = jwt.UserPermissionLimits{}
    return uc.Encode(h.signingKey)
}

Client-facing impact

None. The effective subject grants documented in docs/client-api.md are identical — same chat.user.{account}.>, chat.room.>, _INBOX.>, presence read/query. They're now supplied by the scope template instead of being inlined per JWT. The POST /auth request and response schemas are unchanged.

Backend services

backend.creds (loaded by message-worker, broadcast-worker, room-service, etc.) is unaffected: it's a separate long-lived credential signed by a different key path from AUTH_SIGNING_KEY.

Test plan

  • make lint — clean
  • make test SERVICE=auth-service — all unit tests updated and passing
  • TestHandleAuth_ValidToken — asserts Tags contains account:<account>, Pub.Allow and Sub.Allow are empty, UserPermissionLimits is zero
  • TestHandleAuth_PermissionsPerUser — asserts per-account tag isolation
  • TestHandleAuth_DevMode_ValidRequest — asserts tag for dev-mode path
  • Deploy to staging with the new scoped signing key seed set as AUTH_SIGNING_KEY; frontend connects and can publish/subscribe under its own chat.user.{account}.> namespace
  • Confirm tag key on the scope template is account (platform team)

Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Updated authentication token generation to use account-based tagging and scoped claims instead of embedding explicit publish/subscribe allow lists.
    • Tokens now align with the configured permission template, improving consistency for chat and presence/query access.
  • Chores
    • Improved local development setup to generate and use a dedicated scoped signing key for authentication.
  • Tests
    • Updated auth handler tests to validate the new tagged/scoped token permission model.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Joey0538, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 53 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1d9bebcd-289d-417e-820f-66322ce9dea2

📥 Commits

Reviewing files that changed from the base of the PR and between fd387a6 and ddf722b.

📒 Files selected for processing (8)
  • auth-service/deploy/.env.example
  • auth-service/handler.go
  • auth-service/handler_test.go
  • auth-service/integration_test.go
  • auth-service/main.go
  • docker-local/setup.sh
  • docs/specs/botplatform/auth.md
  • docs/superpowers/spec.md
📝 Walkthrough

Walkthrough

The auth-service JWT signer now uses account-tagged scoped claims instead of inline publish/subscribe allow lists. Tests were updated for the new claim shape, and the local setup script now provisions and exports a dedicated signing key seed for auth-service.

Changes

Scoped NATS auth flow

Layer / File(s) Summary
Scoped JWT claims
auth-service/handler.go
signNATSJWT adds an account:<account> tag, enables scoped claims, and removes explicit publish/subscribe allow-list construction.
Claim assertions updated
auth-service/handler_test.go
The auth handler tests assert account tags and empty allow lists across valid-token, per-user, and dev-mode cases.
Scoped signing key setup
docker-local/setup.sh
The local setup script creates a dedicated chatapp signing key, writes its seed to auth_sk_seed.txt, updates the displayed output, and points AUTH_SIGNING_KEY at the new seed file.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • hmchangw/chat#33: Introduced the inline claims.Pub.Allow/claims.Sub.Allow permission logic that this PR replaces with tag-based scoped claims.
  • hmchangw/chat#55: Also changed auth identity handling in auth-service/handler.go, touching the same JWT-generation path.
  • hmchangw/chat#97: Also updates local NATS/auth setup scripts to generate and export credentials for local development.
🚥 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 accurately summarizes the main change: auth-service now delegates user permissions to a scoped NATS signing key.
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
  • Commit unit tests in branch claude/understand-present-content-3xk7ea

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.

@Joey0538 Joey0538 force-pushed the claude/understand-present-content-3xk7ea branch 3 times, most recently from fd387a6 to 5605f50 Compare July 2, 2026 01:48
The account signing key used to mint NATS user JWTs is being rotated to a
scoped signing key whose role template declares the per-user subject
grants centrally (chat.user.{{tag(account)}}.>, chat.room.>, _INBOX.>,
presence read + query). Centralizing grants on the key means changes to
the allowed subject shape flow through one key config instead of every
consumer of this signing key, and NATS enforces the scope uniformly.

signNATSJWT drops the inline Pub/Sub allow lists and instead:

- Adds an `account:<account>` tag so {{tag(account)}} in the scope
  template resolves per user.
- Calls SetScoped(true) so per-user NatsLimits are cleared — a scoped SK
  rejects any per-user limits on the JWT even when they'd resolve to the
  same effective cap, because the scope owns limits.

Renamed AUTH_SIGNING_KEY → AUTH_SCOPED_SIGNING_KEY to make the required
value's type explicit at every callsite. The variable is not yet set in
any deployed environment (still pre-staging), so the rename is a
zero-risk clarity win.

docker-local/setup.sh mirrors the prod key layout so local dev enforces
the same isolation: the chatapp account gets a scoped signing key with
the auth-service scope template, and AUTH_SCOPED_SIGNING_KEY in the
generated .env is the scoped SK seed (not the account root seed).
Without this, local user JWTs — signed by an unscoped root key with no
inline perms — would silently receive unrestricted pub/sub, masking
scope violations that would fail in prod.

Client-facing behavior is unchanged: the effective subject grants
documented in client-api.md still apply, sourced from the scope template
instead of the JWT's inline allow lists.
@Joey0538 Joey0538 force-pushed the claude/understand-present-content-3xk7ea branch from 5605f50 to ddf722b Compare July 2, 2026 01:52
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