Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions TECHNICAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,9 @@ All runtimes acquire provider credentials on demand for each live request:
another token exchange. Managed-identity assertions are obtained only when the SDK needs them.

There are no application-owned provider credential caches, startup warmup, background refresh
loops, or failure cooldowns. Evaluation skips provider credential work. Acquisition retains its
2.5-second budget. A failure returns `502` with a request-correlated `credential_unavailable`
loops, or failure cooldowns. Evaluation skips provider credential work. Acquisition has a
5-second budget in all runtimes, separate from the provider
HTTP timeout. A failure returns `502` with a request-correlated `credential_unavailable`
log; the next live request can attempt acquisition again.

`EPP_KEY_VAULT_CACHE_ENABLED` and `EPP_ACCESS_TOKEN_CACHE_ENABLED` are obsolete and ignored by
Expand Down
2 changes: 1 addition & 1 deletion dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ mapping. Each provider maps its statuses to `ProviderResult`. Request-aware rout
accounts require a separate trust, configuration, credential, and test design.

`CredentialTokenService` invokes the selected provider's credential retrieval for each live
request with a 2.5-second cancellation budget. API-key secrets are read from Key Vault each time.
request with a 5-second cancellation budget. API-key secrets are read from Key Vault each time.
The OAuth providers share `OAuthCredentialSource` and reuse Azure Identity SDK credential instances,
leaving token caching and renewal to the SDK. There is no custom credential cache, hosted warmup service, polling, or
retry cooldown. Evaluation does no provider credential work. See
Expand Down
2 changes: 1 addition & 1 deletion dotnet/Src/CredentialTokenService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Epp.Otp;

public sealed class CredentialTokenService
{
internal static readonly TimeSpan AcquisitionTimeout = TimeSpan.FromSeconds(2.5);
internal static readonly TimeSpan AcquisitionTimeout = TimeSpan.FromSeconds(5);

public async Task<ProviderCredentials> GetCredentialsAsync(
PhoneProviderBase provider,
Expand Down
16 changes: 15 additions & 1 deletion dotnet/tests/CredentialAcquisitionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ public async Task CallerCancellationCancelsOnlyItsCredentialFetch()
Assert.True(observed.IsCancellationRequested);
}

[Fact]
public async Task AcquisitionAllowsCredentialsThatTakeLongerThanThePreviousBudget()
{
Assert.Equal(TimeSpan.FromSeconds(5), CredentialTokenService.AcquisitionTimeout);
var provider = new TestProvider(async cancellation =>
{
await Task.Delay(TimeSpan.FromSeconds(3), cancellation);
return new("apiKey", Secret: "synthetic-secret");
});

var credentials = await new CredentialTokenService().GetCredentialsAsync(provider, new AppConfig());
Assert.Equal("synthetic-secret", credentials.Secret);
}

[Fact]
public async Task AcquisitionDeadlineRejectsLateResultsWithoutBlockingTheNextRequest()
{
Expand All @@ -88,7 +102,7 @@ public async Task AcquisitionDeadlineRejectsLateResultsWithoutBlockingTheNextReq
var error = await Assert.ThrowsAsync<InvalidOperationException>(() =>
service.GetCredentialsAsync(provider, new AppConfig()));
Assert.Equal("provider credential unavailable", error.Message);
Assert.InRange(elapsed.Elapsed.TotalSeconds, 2, 8);
Assert.InRange(elapsed.Elapsed.TotalSeconds, 4.5, 10);
Assert.True(observed.IsCancellationRequested);
}
finally { late.TrySetResult(new("apiKey", Secret: "late")); }
Expand Down
2 changes: 1 addition & 1 deletion javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ For diagnostics, use [Application Insights](../docs/APPLICATION-INSIGHTS.md) and
Each live request reads API-key secrets from Key Vault or calls the reusable Azure Identity
credential's token API. Only the SDK caches tokens and renews them as needed; there is no custom
credential cache, startup warmup, polling, or retry cooldown. Evaluation does no provider credential
work. Acquisition is bounded to 2.5 seconds, and shutdown aborts pending requests. See
work. Acquisition is bounded to 5 seconds, and shutdown aborts pending requests. See
[credential acquisition](../TECHNICAL.md#provider-settings-and-credential-acquisition).

| Source | Purpose |
Expand Down
2 changes: 1 addition & 1 deletion javascript/src/functions/credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const { AzureLogger } = require('@azure/logger');
const API_KEY_MODE = 'apiKey';
const OAUTH_MODE = 'oauth';
const TOKEN_EXCHANGE_SCOPE = 'api://AzureADTokenExchange/.default';
const ACQUISITION_TIMEOUT_MS = 2500;
const ACQUISITION_TIMEOUT_MS = 5000;
const TOKEN_SKEW_MS = 30000;
const unavailable = (error) => new Error(aadstsCode(error) || 'provider credential unavailable');
/** @type {AsyncLocalStorage<AbortSignal>} */
Expand Down
14 changes: 14 additions & 0 deletions javascript/test/credentials.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ test('concurrent secret requests are independent and shutdown aborts all waiters
} finally { release(); manager.close(); }
});

test('credential acquisition allows a three-second response within a five-second budget', async (t) => {
const setTimer = global.setTimeout;
const timers = t.mock.method(global, 'setTimeout', setTimer);
t.mock.method(SecretClient.prototype, 'getSecret', async () => {
await new Promise((resolve) => setTimer(resolve, 3000));
return { value: 'synthetic-secret' };
});
const manager = new CredentialTokenService();
try {
assert.equal((await manager.getCredentials(auth, config)).secret, 'synthetic-secret');
assert.equal(timers.mock.calls[0].arguments[1], 5000);
} finally { manager.close(); }
});

test('empty, disabled, future, or expired secrets fail instead of returning old credentials', async (t) => {
const vault = t.mock.method(SecretClient.prototype, 'getSecret', async () => ({ value: 'valid' }));
for (const invalid of [
Expand Down
2 changes: 1 addition & 1 deletion python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Verify which Python logging properties the host/export pipeline preserves.
Each live request reads API-key secrets from Key Vault or calls the reusable Azure Identity
credential's token API. Only the SDK caches tokens and renews them as needed; there is no custom
credential cache, startup warmup, polling, or retry cooldown. Evaluation does no provider credential
work. Request-scoped daemon threads bound the caller's acquisition wait to 2.5 seconds without
work. Request-scoped daemon threads bound the caller's acquisition wait to 5 seconds without
blocking process shutdown. Timed-out results are discarded; SDK network timeouts bound the
underlying calls. See [credential acquisition](../TECHNICAL.md#provider-settings-and-credential-acquisition).

Expand Down
2 changes: 1 addition & 1 deletion python/src/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
OAUTH_MODE: Final = "oauth"
TOKEN_EXCHANGE_SCOPE = "api://AzureADTokenExchange/.default"
CREDENTIAL_ERROR = "provider credential unavailable"
ACQUISITION_TIMEOUT_SECONDS = 2.5
ACQUISITION_TIMEOUT_SECONDS = 5
TOKEN_SKEW_SECONDS = 30
_acquiring = ContextVar("epp_credential_acquisition", default=False)
T = TypeVar("T")
Expand Down
14 changes: 14 additions & 0 deletions python/tests/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,20 @@ def test_acquisition_timeout_discards_late_values_and_does_not_delay_the_next_re
manager.close()


def test_default_acquisition_budget_allows_a_three_second_response():
assert credentials_module.ACQUISITION_TIMEOUT_SECONDS == 5

def read(_):
time.sleep(3)
return "synthetic-secret"

manager = CredentialTokenService(Mock(resolve=read))
try:
assert manager.resolve(AUTH, CONFIG)["secret"] == "synthetic-secret"
finally:
manager.close()


@pytest.mark.parametrize("empty_secret", ["key", "id"])
def test_empty_secrets_do_not_return_a_previous_or_partial_bundle(empty_secret):
secrets = Mock(resolve=Mock(return_value="valid"))
Expand Down
4 changes: 4 additions & 0 deletions setup/docs/Troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ federation, provider tenant/scope, and provider permissions using the
Match diagnostics to the deployed package version. Regardless of event names, verify permissions
and identity configuration.

Credential acquisition is bounded to 5 seconds in all runtimes.
`EPP_PROVIDER_TIMEOUT_MS` controls the later provider HTTP call, not credential acquisition.
Deploy the updated runtime package to use the longer budget; changing app settings alone is not enough.

Current packages acquire credentials only during live requests, retain SDK token caching, and
have no custom failure cooldown. The next live request can retry acquisition. Confirm the deployed
package version before comparing event sequences. If moving to current behavior, upgrade through
Expand Down
Loading