Skip to content

fix(auth): wire Keycloak authority from injected keys + restore sub/aud claims (closes #147)#149

Merged
emeraldleaf merged 5 commits into
mainfrom
fix/keycloak-auth-wiring
Jun 13, 2026
Merged

fix(auth): wire Keycloak authority from injected keys + restore sub/aud claims (closes #147)#149
emeraldleaf merged 5 commits into
mainfrom
fix/keycloak-auth-wiring

Conversation

@emeraldleaf

@emeraldleaf emeraldleaf commented Jun 13, 2026

Copy link
Copy Markdown
Owner

What

Authenticated endpoints returned 409 (not 401) for every request when running the full stack locally — System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. Three layers, all fixed:

  1. Authority wiring (code). AddDefaultAuthentication read Keycloak:Url, but the Keycloak.AuthServices.Aspire.Hosting integration injects Keycloak:AuthServerUrl + Keycloak:Realm separately. Authority resolved to null → no JWT bearer scheme registered → no default challenge scheme → 409. Now builds authority = {AuthServerUrl}/realms/{Realm} from the injected keys (reads the keys, doesn't hardcode the URL).
  2. Missing sub (realm). Keycloak 25+ moved sub into the basic client scope; the storefront client listed a bogus openid scope (not a real client scope) and omitted basic. Fixed defaultClientScopes[basic, profile, email, roles]. (profile/email already resolved, proving the built-ins are seeded — so basic is too.)
  3. Missing aud: nextaurora-api (realm). ValidateAudience=true requires it; added an oidc-audience-mapper to the storefront client.

Verification (live, against the AppHost)

Check Result
Token claims sub ✓, aud: nextaurora-api ✓, iss: localhost:8080 (matches authority) ✓
Authed endpoint, no token 401 (was 409)
Authed endpoint, valid bearer (own buyer) 200
IDOR — buyer1 token for another buyer's id 403
Valid token, random order id 404 (owned-by predicate; no existence leak)

Build clean (analyzers-as-errors). Issuer matches because both the SPA (VITE_KEYCLOAK_URL=localhost:8080) and the API resolve to the same Keycloak base.

Notes

Found + fixed during the frontend epic #130 live verification. Companion local-run fixes already merged in #146. The remaining local-run item is #148 (Wolverine internal emulator queues — non-fatal, degrades saga). Closes #147.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Configuration Updates

    • Adjusted storefront OIDC client: tightened allowed redirect origins, added post-logout redirect, expanded default scopes to include roles, and added an audience mapper for access tokens targeting the API.
    • Improved authentication authority resolution to derive Keycloak authority from configured auth server URL and realm when not explicitly set.
  • Bug Fixes

    • Prevented duplicate token-exchange runs during sign-in callback to avoid double-processing in development.

…ud claims (#147)

Three-layer fix for authed endpoints returning 409 (not 401) locally:

1. AddDefaultAuthentication read 'Keycloak:Url', but the Keycloak.AuthServices
   Aspire hosting integration injects 'Keycloak:AuthServerUrl' + 'Keycloak:Realm'
   separately. Authority was therefore null → no JWT bearer scheme registered →
   '.RequireAuthorization()' had no DefaultChallengeScheme → InvalidOperationException
   → mapped to 409. Now builds authority = {AuthServerUrl}/realms/{Realm} from the
   keys Aspire actually injects (issuer then matches the token: localhost:8080).

2. Token had no 'sub' claim. In Keycloak 25+ 'sub' moved into the 'basic' client
   scope; storefront's defaultClientScopes listed a bogus 'openid' (not a real
   client scope) and omitted 'basic'. Fixed to [basic, profile, email, roles].
   (profile/email already resolved — proof built-in scopes are seeded — so basic
   is too.)

3. Token had no 'aud: nextaurora-api' (ValidateAudience=true requires it). Added
   an oidc-audience-mapper to the storefront client.

Verified: token issuer already matches; this restores the scheme + sub + aud so
the buyer flow validates. Closes the auth half of the local full-stack run (#147).

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

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@emeraldleaf, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 56 minutes and 50 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 52fc60d1-c0c8-49fa-9ecf-dd1a9ac4f7ff

📥 Commits

Reviewing files that changed from the base of the PR and between 26c3b2c and 6634828.

📒 Files selected for processing (1)
  • docs/architecture.md

Walkthrough

Authority resolution now builds a Keycloak realm-based authority from Keycloak:AuthServerUrl and Keycloak:Realm when Authentication:Authority is unset. The storefront OIDC client narrows redirect/web origins to localhost, adds post-logout redirect URIs, updates default scopes, and adds an audience protocol mapper for access tokens. The frontend callback adds a StrictMode guard to avoid double token exchange.

Changes

OIDC Authentication Configuration

Layer / File(s) Summary
Authority resolution from configuration
NextAurora.ServiceDefaults/Extensions.cs, tests/OrderService.Tests.Unit/Application/ServiceDefaultsJwtTests.cs
AddDefaultAuthentication derives JWT bearer authority from Keycloak:AuthServerUrl + Keycloak:Realm when Authentication:Authority is not set; corresponding unit test in-memory config updated.
Storefront OIDC client configuration
NextAurora.AppHost/realms/nextaurora-realm.json
storefront client: narrowed redirectUris and webOrigins to http://localhost:5173, added post.logout.redirect.uris, changed defaultClientScopes to include basic and roles, and added a nextaurora-api-audience protocol mapper that emits the nextaurora-api audience in access tokens only.
Frontend AuthCallback StrictMode guard
frontend/src/features/auth/AuthCallback.tsx
Adds useRef exchanged guard to short-circuit repeated signinRedirectCallback executions (addresses React StrictMode double-invoke).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • emeraldleaf/NextAurora#25: Touches ServiceDefaults authority resolution and related JWT test setup; changes appear related to the same authentication configuration adjustments.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main changes: fixing Keycloak authority wiring and restoring authentication claims (sub/aud), matching the core objectives of the PR.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/keycloak-auth-wiring

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 and usage tips.

@codecov

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.00000% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
NextAurora.ServiceDefaults/Extensions.cs 70.00% 0 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

…ak accepts (#147)

The storefront client's redirectUris were 'http://localhost:*/*' — but Keycloak
only allows wildcards at the END of a URI, so a wildcard in the port position
never matches and the SPA's 'http://localhost:5173/auth/callback' was rejected
with 'Invalid parameter: redirect_uri' at the login redirect.

Fixed to ['http://localhost:5173/*', 'http://localhost:5173'] (Vite's stable dev
port) + matching webOrigins (for the token-endpoint CORS) + post.logout.redirect.uris.
Applied the same change to the running instance via the admin API for the live
session. Part of the #147 local auth fix.

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

@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: 2

🤖 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 `@NextAurora.ServiceDefaults/Extensions.cs`:
- Around line 217-225: The change removed the old Keycloak:Url and broke tests
that expect the JwtBearer branch to run; restore compatibility by falling back
to the legacy key or update tests: modify the authority resolution in the code
that sets authority (the block referencing
builder.Configuration["Keycloak:AuthServerUrl"] and
builder.Configuration["Keycloak:Realm"]) to also check
builder.Configuration["Keycloak:Url"] (trimmed) when AuthServerUrl+Realm are not
present, or alternatively update the unit test setup to provide both
Keycloak:AuthServerUrl and Keycloak:Realm so the JwtBearer branch is exercised;
additionally add/adjust unit tests to cover the changed authority-resolution
logic (the lines that compute authority) so the new behavior is tested.
- Around line 207-225: Extensions.cs changes the authentication contract
(authority resolution logic inside AddDefaultAuthentication) but the PR omits
the required paired documentation updates; update docs/architecture.md and the
service-request-flow artifacts (docs/service-request-flow.svg and
docs/service-request-flow.excalidraw) to reflect the new authority resolution
behavior (Authentication:Authority override vs Keycloak:AuthServerUrl +
Keycloak:Realm), or if you cannot update them in this PR create a tracked
deferred issue and reference it in the PR description; ensure the doc change
explains the new resolution priority and shows the updated middleware/auth flow
so the repo’s canonical artifacts remain in sync with AddDefaultAuthentication.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 75372934-fb05-4673-b5ce-f2b20522d1f2

📥 Commits

Reviewing files that changed from the base of the PR and between bc5c997 and c551a17.

📒 Files selected for processing (2)
  • NextAurora.AppHost/realms/nextaurora-realm.json
  • NextAurora.ServiceDefaults/Extensions.cs

Comment thread NextAurora.ServiceDefaults/Extensions.cs
Comment thread NextAurora.ServiceDefaults/Extensions.cs
…ange (#147)

React 19 StrictMode double-invokes effects in dev, so signinRedirectCallback ran
twice → the single-use authorization code was consumed by the first exchange and
the second failed with invalid_code (a 400 at the token endpoint, surfaced during
#147 live verification). A useRef guard ensures the exchange runs exactly once.
Dev-only symptom (StrictMode), but the guard is correct regardless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
emeraldleaf and others added 2 commits June 13, 2026 11:05
…eys (#147)

The ServiceDefaultsJwtTests configured the authority via 'Keycloak:Url' — the
dead key this PR replaced. AddDefaultAuthentication now builds the authority from
'Keycloak:AuthServerUrl' + 'Keycloak:Realm' (what Aspire actually injects), so the
test setup must use those keys or no JWT scheme registers and the option
assertions fail. Resolves to the same authority value; the assertions are unchanged.

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

Pairs with the AddDefaultAuthentication change (CodeRabbit doc-discipline catch):
- authority now resolves from Authentication:Authority, else built from the
  Aspire-injected Keycloak:AuthServerUrl + Keycloak:Realm (was the dead Keycloak:Url)
- no-op fallback note now explains the 401-challenge→409 symptom (#147)
- fixed stale method name AddJwtBearerAuthentication → AddDefaultAuthentication
The request-flow diagram is unchanged — middleware order + authenticate→authorize
are identical; only the authority's config source changed.

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

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/OrderService.Tests.Unit/Application/ServiceDefaultsJwtTests.cs (1)

18-30: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Restructure test methods with explicit AAA phase markers and narrative comments.

Per coding guidelines (CLAUDE.md "Testing"), all tests must follow the Arrange → Act → Assert structure with // ARRANGE, // ACT, // ASSERT phase markers (all caps) and narrative comments explaining what's being set up and why. This makes the test contract immediately readable for junior developers without requiring them to read the SUT.

Currently, the three test methods lack explicit phase markers; comments exist but are not tied to marked phases. Restructure each test to make the phases explicit and move narrative comments to align with each phase.

📋 Proposed refactoring for test method structure (example: Test 1)
     [Fact]
     public void AddServiceDefaults_WhenAuthorityConfigured_SetsExplicitSigningKeyValidation()
     {
+        // ARRANGE — build a host with authority configured via Aspire Keycloak injected keys,
+        // triggering AddDefaultAuthentication to wire JWT Bearer options.
         using var host = BuildHostWithAuthority();
+
+        // ACT — retrieve the configured JwtBearerOptions from the DI container.
         var options = host.Services
             .GetRequiredService<IOptionsMonitor<JwtBearerOptions>>()
             .Get(JwtBearerDefaults.AuthenticationScheme);
+
+        // ASSERT — ValidateIssuerSigningKey must be true. Without this explicit flag,
+        // JWT Bearer's implicit default still validates (via JWKS), but making it explicit
+        // makes the security posture auditable and prevents a future config refactor
+        // from accidentally disabling signature validation.
         options.TokenValidationParameters.ValidateIssuerSigningKey.Should().BeTrue();
     }

Apply the same structure to the other two test methods (SetsTightClockSkew and RetainsCoreValidations), moving existing comments into their corresponding ASSERT phases and adding narrative to each phase.

Also applies to: 32-45, 47-60

🤖 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 `@tests/OrderService.Tests.Unit/Application/ServiceDefaultsJwtTests.cs` around
lines 18 - 30, The test methods
AddServiceDefaults_WhenAuthorityConfigured_SetsExplicitSigningKeyValidation,
SetsTightClockSkew, and RetainsCoreValidations should be refactored to an
explicit Arrange–Act–Assert layout: add // ARRANGE, // ACT, and // ASSERT
comment markers (all caps) in each test, move existing narrative comments into
the phase they describe, and ensure each phase has a brief narrative explaining
what is being set up, the action performed, and the assertion being checked; for
example in
AddServiceDefaults_WhenAuthorityConfigured_SetsExplicitSigningKeyValidation put
host creation and options retrieval under // ARRANGE with a short narrative,
leave retrieving the JwtBearerOptions as the // ACT, and move the existing
explanatory comment and the Should().BeTrue() check under // ASSERT with a
narrative explaining why explicit signing-key validation is required.

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.

Outside diff comments:
In `@tests/OrderService.Tests.Unit/Application/ServiceDefaultsJwtTests.cs`:
- Around line 18-30: The test methods
AddServiceDefaults_WhenAuthorityConfigured_SetsExplicitSigningKeyValidation,
SetsTightClockSkew, and RetainsCoreValidations should be refactored to an
explicit Arrange–Act–Assert layout: add // ARRANGE, // ACT, and // ASSERT
comment markers (all caps) in each test, move existing narrative comments into
the phase they describe, and ensure each phase has a brief narrative explaining
what is being set up, the action performed, and the assertion being checked; for
example in
AddServiceDefaults_WhenAuthorityConfigured_SetsExplicitSigningKeyValidation put
host creation and options retrieval under // ARRANGE with a short narrative,
leave retrieving the JwtBearerOptions as the // ACT, and move the existing
explanatory comment and the Should().BeTrue() check under // ASSERT with a
narrative explaining why explicit signing-key validation is required.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a7d2ad94-785c-41b4-9bd0-54c65ff28850

📥 Commits

Reviewing files that changed from the base of the PR and between c551a17 and 26c3b2c.

📒 Files selected for processing (2)
  • frontend/src/features/auth/AuthCallback.tsx
  • tests/OrderService.Tests.Unit/Application/ServiceDefaultsJwtTests.cs

@emeraldleaf
emeraldleaf merged commit 19e3afe into main Jun 13, 2026
8 checks passed
@emeraldleaf
emeraldleaf deleted the fix/keycloak-auth-wiring branch June 13, 2026 17:24
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.

Keycloak authority config not reaching AddDefaultAuthentication → authed endpoints 409 locally

1 participant