[ADR-183] OAuth2 client credentials token provider for cloud assets#179
Conversation
Introduce OAuth2 client credentials authentication to the cloud asset infrastructure. CognitoTokenProvider acquires tokens from AWS Cognito, caches them in memory, and transparently refreshes them within 60 seconds of expiry. A SemaphoreSlim prevents concurrent refreshes. CloudSettings gains ClientId, ClientSecret, and CognitoTokenEndpointUrl (all default to empty string — supply via user secrets or env vars). MessageLogger gains CognitoTokenProvider_* log methods (Event IDs 1010-1012). Unit tests cover: first-call fetch, cached-token reuse, near-expiry refresh, cancellation, and HTTP failure propagation. https://claude.ai/code/session_016PwpXFt999Q5Xk52qFukfu
Update CognitoTokenProvider_GetTokenAsync_HttpFailure_PropagatesExceptionAsync to assert the exact exception message that EnsureSuccessStatusCode() throws (includes the 401 status code) rather than a default HttpRequestException. https://claude.ai/code/session_016PwpXFt999Q5Xk52qFukfu
|
Re: Near-duplicate of CognitoTokenService The spec (ADR-183) explicitly calls for a new Generated by Claude Code |
|
Re: This is consistent with the existing Generated by Claude Code |
- Move CognitoTokenProvider log event IDs from conflicting 1010-1012 range to 1800-1899 - Add ICloudAuthTokenProvider/CognitoTokenProvider DI registration to AddCloudAssetServices - Add MockLogger.VerifyMessages assertion to SecondCallBeforeExpiry test - Replace Queue<T> with ConcurrentQueue<T> in FakeHttpMessageHandler for thread safety https://claude.ai/code/session_016PwpXFt999Q5Xk52qFukfu
…letion Add three behaviours to the team-task skill: - Phase 4 creates the PR as a draft (draft: true) using the MCP tool - Completion marks the draft ready for review (draft: false) - Completion requests a review from @jodavis
| // ───────────────────────────────────────────────────────────────── | ||
|
|
||
| [TestMethod] | ||
| public async Task CognitoTokenProvider_GetTokenAsync_FirstCall_FetchesAndReturnsTokenAsync() |
There was a problem hiding this comment.
Don't create async unit tests. Use TaskCompletionSource to simulate asynchronous operations, and have tests to cover all edge cases and error conditions (e.g. any call to an async method could block, throw, be cancelled, or succeed. All async calls that accept CancellationToken should respond to the CancellationToken passed in to GetTokenAsync).
| // FakeHttpMessageHandler — returns pre-configured responses in order | ||
| // ───────────────────────────────────────────────────────────────── | ||
|
|
||
| private sealed class FakeHttpMessageHandler : HttpMessageHandler |
There was a problem hiding this comment.
Move this to the TestUtilities namespace. It will probably be useful for other tests that need to make HTTP calls, e.g. the REST API calls that the real CloudAssetDownloader will eventually make.
|
|
||
| // Assert | ||
| token.Should().Be("first-token"); | ||
| handler.CallCount.Should().Be(1); |
There was a problem hiding this comment.
Should also verify that the correct URL/content was passed, not just that a call was made.
| new("client_id", _settings.ClientId), | ||
| new("client_secret", _settings.ClientSecret), |
There was a problem hiding this comment.
If ClientId and ClientSecret are not set, this should fail gracefully. I'm not sure what that means for the API. Maybe it returns null, and the caller will know not to attach an Authorization header if the token is null?
| { | ||
| internal static IServiceCollection AddCloudAssetServices(this IServiceCollection services, IConfiguration configuration) | ||
| => services | ||
| .AddSingleton<ICloudAuthTokenProvider, CognitoTokenProvider>() |
There was a problem hiding this comment.
Nothing is calling this yet, so we have no idea whether the code actually works.
Let's add a call to ICloudAuthTokenProvider.GetTokenAsync in the stub implementation of ICloudAssetDownloader. It doesn't need a token, but it can try to acquire one anyway.
There should be E2E tests with ClientId/ClientSecret set and not set, so we validate that the application can start without credentials (no cloud downloads) and with credentials (cloud downloads attempted). I think we created a mock OAuth server to test authentication in ApiTests. These scenarios should be added to the CloudAsset E2E scenarios.
Jira: https://jodasoft.atlassian.net/browse/ADR-183
What changed
ICloudAuthTokenProviderinterface withGetTokenAsync(CancellationToken)inServices/CloudAssets/CognitoTokenProvidersingleton: POSTs to Cognito token endpoint with client credentials, caches token in-memory, proactively refreshes 60s before expiry, usesSemaphoreSlimfor thread safetyClientId,ClientSecret,CognitoTokenEndpointUrltoCloudSettingsMessageLogger.csfor token acquisition events_doc_CloudAssets.mdwith OAuth2 token provider sectionTest plan
CognitoTokenProviderTests)🤖 Generated with Claude Code
Generated by Claude Code