Skip to content

Add JWT Bearer authentication backed by AWS Cognito#140

Open
jodavis wants to merge 6 commits intofeature/ADR-161-cusrtomization-servicefrom
claude/implement-adr-168-layout-sxfA9
Open

Add JWT Bearer authentication backed by AWS Cognito#140
jodavis wants to merge 6 commits intofeature/ADR-161-cusrtomization-servicefrom
claude/implement-adr-168-layout-sxfA9

Conversation

@jodavis
Copy link
Copy Markdown
Owner

@jodavis jodavis commented Apr 8, 2026

Summary

Implements JWT Bearer token authentication for the CompiledLayoutService backend API using AWS Cognito as the identity provider. The service now validates bearer tokens on protected endpoints and extracts the sub claim as the user ID. Includes a local test JWT authority for API integration tests to validate authentication flows end-to-end without external dependencies.

Key Changes

Backend Service (CompiledLayoutService)

  • Added JWT Bearer authentication middleware configured to validate tokens against a Cognito user pool
  • Created CognitoSettings configuration class to manage Authority and Audience settings
  • Protected the /layouts/compiled/active endpoint with [Authorize]; /health remains unauthenticated
  • Extracted userId from the JWT sub claim and passed it to the layout repository
  • Added configuration support for Cognito__Authority and Cognito__Audience environment variables

Client Application (AdaptiveRemote)

  • Created CognitoTokenService to acquire and cache OAuth2 access tokens using the Client Credentials flow
  • Implemented lazy token refresh: cached tokens are reused until within 60 seconds of expiry
  • Added BackendSettings and CognitoClientSettings configuration classes for backend connectivity
  • Integrated backend support into the app's host builder via BackendHostBuilderExtensions
  • Added appsettings.json and appsettings.Development.json with Cognito configuration placeholders

API Integration Tests

  • Created TestJwtAuthority: a minimal local OIDC/JWKS server that issues and validates test JWTs
    • Exposes /.well-known/openid-configuration and /.well-known/jwks.json endpoints
    • Dynamically assigns a localhost port to avoid conflicts
  • Refactored ServiceFixture to start the test JWT authority before the service
  • Added helper methods: CreateToken(), CreateExpiredToken(), CreateAnonymousHttpClient(), CreateBearerHttpClient()
  • Created ServiceContext for Reqnroll scenario-scoped dependency injection
  • Added AuthenticationSteps with step definitions for testing authenticated/unauthenticated/expired token scenarios
  • Added AuthenticationEndpoints.feature with four test scenarios covering the authentication matrix

Documentation

  • Added _doc_Auth.md with comprehensive authentication architecture, Cognito setup instructions, and configuration guidance for both client and backend

Notable Implementation Details

  • Token validation runs end-to-end in tests using a real (but local) JWT authority, exercising the full OIDC discovery and JWKS validation flow
  • The test JWT authority uses RSA-2048 key pairs and RS256 signatures, matching production Cognito behavior
  • HTTP metadata endpoints are allowed in non-production environments to support local development and tests
  • Audience validation is optional: if Cognito__Audience is empty, validation is skipped
  • The client application discovers the token endpoint dynamically from the OIDC configuration document, supporting flexible Cognito deployments

https://claude.ai/code/session_01LLWQBraEp7n7uLVy7M4PFp

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 8, 2026

Test Results

407 tests  +4   407 ✅ +4   2m 5s ⏱️ -18s
 12 suites ±0     0 💤 ±0 
 12 files   ±0     0 ❌ ±0 

Results for commit ae59af0. ± Comparison against base commit 72495aa.

♻️ This comment has been updated with latest results.

claude and others added 2 commits April 10, 2026 14:48
…nd auth API tests

- CompiledLayoutService: add JWT Bearer authentication backed by AWS Cognito.
  GET /layouts/compiled/active now requires a valid bearer token; GET /health
  remains unauthenticated. The sub claim is extracted as userId.
  CognitoSettings reads Authority and Audience from appsettings/env vars.

- Client app: add BackendSettings, ICognitoTokenService, CognitoTokenService.
  CognitoTokenService acquires and caches tokens via the OAuth2 Client Credentials
  flow (lazy acquire, 60-second expiry buffer). Log messages in range 1600-1699.

- API tests: add TestJwtAuthority (local OIDC/JWKS server) so auth can be tested
  end-to-end without a real Cognito user pool. ServiceFixture now starts the
  authority first and configures Cognito__Authority on the service process.
  CommonSteps updated to use Reqnroll context injection (ServiceContext) for
  shared fixture state. AuthenticationEndpoints.feature covers 401 for no-auth
  and expired tokens, 200 for valid JWT, and unauthenticated /health access.

- Docs: _doc_Auth.md documents Cognito setup for local dev, client credentials
  config, editor Authorization Code flow, and the test JWT authority pattern.

https://claude.ai/code/session_01LLWQBraEp7n7uLVy7M4PFp
@jodavis jodavis force-pushed the claude/implement-adr-168-layout-sxfA9 branch from 327624c to 787b0df Compare April 11, 2026 02:41
@jodavis
Copy link
Copy Markdown
Owner Author

jodavis commented Apr 11, 2026

@claude -- The API tests are hanging with these changes. Debug the problem, describe what's happening, and try to fix it. When you're done, the solution should build without warnings or errors and all tests should pass, especially the Headless Host E2E tests and API tests.

claude added 2 commits April 11, 2026 02:53
…nd auth API tests

- CompiledLayoutService: add JWT Bearer authentication backed by AWS Cognito.
  GET /layouts/compiled/active now requires a valid bearer token; GET /health
  remains unauthenticated. The sub claim is extracted as userId.
  CognitoSettings reads Authority and Audience from appsettings/env vars.

- Client app: add BackendSettings, ICognitoTokenService, CognitoTokenService.
  CognitoTokenService acquires and caches tokens via the OAuth2 Client Credentials
  flow (lazy acquire, 60-second expiry buffer). Log messages in range 1600-1699.

- API tests: add TestJwtAuthority (local OIDC/JWKS server) so auth can be tested
  end-to-end without a real Cognito user pool. ServiceFixture now starts the
  authority first and configures Cognito__Authority on the service process.
  CommonSteps updated to use Reqnroll context injection (ServiceContext) for
  shared fixture state. AuthenticationEndpoints.feature covers 401 for no-auth
  and expired tokens, 200 for valid JWT, and unauthenticated /health access.

- Docs: _doc_Auth.md documents Cognito setup for local dev, client credentials
  config, editor Authorization Code flow, and the test JWT authority pattern.

https://claude.ai/code/session_01LLWQBraEp7n7uLVy7M4PFp
Three categories of fixes:

1. API tests hanging: ServiceFixture.StartService() was blocking the async
   test thread with Thread.Sleep() and HttpClient.GetAsync().Result, risking
   a deadlock when called from Reqnroll's async step dispatcher. Renamed to
   StartServiceAsync(), switched to await/Task.Delay, added a 5-second per-
   request timeout on the health-check client, and made the [Given] step
   binding async. The service URL is now allocated on a dynamic free port
   instead of hardcoded :5000, eliminating port-conflict failures between
   consecutive scenarios.

2. Build errors:
   - Removed 'using System.Net.Http;' from CognitoTokenService.cs — the
     namespace is already a global implicit using for Microsoft.NET.Sdk.Razor
     and the redundant directive triggers a warning/error with /warnaserror.
   - Changed <Content Include="appsettings*.json"> to <Content Update=...> in
     AdaptiveRemote.csproj so we only set CopyToOutputDirectory metadata on
     items the SDK already includes automatically, instead of adding duplicate
     Content items.

3. Missing package: Added Microsoft.NET.Test.Sdk to AdaptiveRemote.Backend.
   ApiTests.csproj — required by Reqnroll.MSTest for proper test-host
   infrastructure (consistent with all other test projects in the solution).

https://claude.ai/code/session_01LLWQBraEp7n7uLVy7M4PFp
@jodavis
Copy link
Copy Markdown
Owner Author

jodavis commented Apr 11, 2026

@copilot There are two problems with the build in this branch. One is a package mismatch error that is causing the build to fail. The other is that we're running E2E tests even when there's a build failure. The latter problem is because of an if: always in the action definition, which is there because we want to run E2E tests if unit tests fail, but we don't want to if the build fails.

Please fix these issues and any others until the build passes without warnings or errors and all tests pass, especially the E2E and API tests.

… gating

Three build fixes:

1. Package mismatch (NU1608): Replace MSTest meta-package with
   MSTest.TestAdapter + MSTest.TestFramework in ApiTests project.
   MSTest 3.1.1 meta-package requires Microsoft.NET.Test.Sdk = 17.6.0
   exactly, which conflicts with the 18.0.1 version in Directory.Packages.props.
   Other test projects already use the individual packages to avoid this.

2. Code analysis error (CA1816): Add GC.SuppressFinalize(this) to
   ServiceContext.Dispose() as required by CA1816.

3. Workflow: Add id: build to the Build step and change E2E Tests from
   if: always() to if: always() && steps.build.outcome == 'success'.
   This ensures E2E tests skip when the build fails, but still run when
   only unit tests fail (as was the intent of always()).

Agent-Logs-Url: https://github.com/jodavis/AdaptiveRemote/sessions/d7976913-3a8a-4c5f-b6ce-5f024fe07d01

Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 11, 2026

@copilot There are two problems with the build in this branch. One is a package mismatch error that is causing the build to fail. The other is that we're running E2E tests even when there's a build fa...

Fixed in commit 8d6f0e5. Three issues addressed:

  1. Package mismatch (NU1608): The MSTest meta-package (3.1.1) requires Microsoft.NET.Test.Sdk = 17.6.0 exactly, but Directory.Packages.props pins it to 18.0.1. Replaced MSTest with MSTest.TestAdapter + MSTest.TestFramework in AdaptiveRemote.Backend.ApiTests.csproj — matching how all other test projects reference MSTest to avoid this constraint.

  2. CA1816 error: Added GC.SuppressFinalize(this) to ServiceContext.Dispose().

  3. Workflow gating: Added id: build to the Build step and changed E2E Tests from if: always() to if: always() && steps.build.outcome == 'success'. E2E tests now skip when the build fails, but still run when only unit tests fail.

@claude -- The API tests are hanging with these changes. Debug the problem, describe what's happening, and try to fix it. When you're done, the solution should build without warnings or errors and all...

The API tests were hanging due to a build failure — once the build fails, the E2E test runner (if: always()) was trying to run tests against non-existent binaries. The root cause was a package version conflict: MSTest 3.1.1 meta-package requires Microsoft.NET.Test.Sdk = 17.6.0 exactly, but the solution pins it to 18.0.1, which caused error NU1608 at build time. Commit 8d6f0e5 fixes this along with the workflow gating and a CA1816 code analysis error.

@jodavis
Copy link
Copy Markdown
Owner Author

jodavis commented Apr 11, 2026

@copilot The API tests are still failing in CI runs. I'm not sure why. It's reporting that the service didn't start within 30s, but the process logs look like it was started and waiting for requests. Investigate and see if you can fix those as well. Add logging to the test if needed to understand why the test thinks the server isn't started yet.

…ettings from overriding ASPNETCORE_URLS

Root cause: dotnet run reads launchSettings.json and overwrites ASPNETCORE_URLS
with applicationUrl (https://localhost:54433;http://localhost:54434), ignoring
the dynamic port set in ProcessStartInfo.Environment. The health check polls the
dynamic port but the service listens on 54434, so it always times out.

Changes:
1. ServiceFixture.cs: Added --no-launch-profile to dotnet run so ASPNETCORE_URLS
   from the test fixture's environment is respected.
2. ServiceFixture.cs: Added per-attempt diagnostic logging in the health check
   loop showing the URL polled and the status/exception per attempt.
3. Program.cs: Fixed misleading ServiceStarted log — app.Urls is always empty
   before Run(), so read ASPNETCORE_URLS from IConfiguration instead.

Agent-Logs-Url: https://github.com/jodavis/AdaptiveRemote/sessions/ba6750ee-d018-4f28-b4d0-7d4e309d6f02

Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 11, 2026

@copilot The API tests are still failing in CI runs. I'm not sure why. It's reporting that the service didn't start within 30s, but the process logs look like it was started and waiting for requests. ...

Fixed in commit ae59af0. Root cause: dotnet run reads launchSettings.json and overwrites ASPNETCORE_URLS with applicationUrl (https://localhost:54433;http://localhost:54434), ignoring the dynamic port set in the test fixture's ProcessStartInfo.Environment. The service was listening on port 54434 while the health check polled the dynamic port — so the 30-second timeout was always reached.

Three changes:

  1. ServiceFixture.cs: Added --no-launch-profile to dotnet run to prevent launchSettings.json from overriding ASPNETCORE_URLS.
  2. ServiceFixture.cs: Added per-attempt diagnostic logging in the health check loop — each attempt now records the status code or exception and the exact URL being polled, making future failures immediately diagnosable.
  3. Program.cs: Fixed the misleading "started successfully" log message — app.Urls is empty before app.Run(), so it always fell back to 5000. Now reads ASPNETCORE_URLS from IConfiguration to log the actual configured address.

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.

4 participants