Skip to content

Conversation

@gladjohn
Copy link
Contributor

@gladjohn gladjohn commented Oct 28, 2025

commit 1 - caches MSI v2 in-memory + tests (ready)
commit 2 - stores cert to store and if there is a cache miss, checks store (in-progress)
commit 3 - adds mutex (in-progress)

This PR adds a minimal, internal in‑memory certificate cache for IMDS v2 mTLS PoP:

  • One entry per cache key (the key is exactly what the caller passes: client_id, object_id, or resource_id), by design.
  • Entries are returned only if valid ≥ 24 hours, otherwise they are ignored and evicted (mTLS tokens are valid up to 24h and must be bound to a cert valid for the full token lifetime).
  • The cache returns a clone of the X509Certificate2 so callers can dispose independently. The cache owns its copy and disposes on eviction/clear.
  • Normalized to UTC for time comparisons.
  • Thread‑safe: ConcurrentDictionary + an external per‑key SemaphoreSlim (unchanged) prevents double‑mints.

Why 24 hours?

mTLS PoP tokens can be valid up to 24h; reusing a cert that expires sooner would create a token that cannot be used by the resource throughout its validity. The cache therefore refuses to store certs with < 24h remaining and evicts entries that have hit their NotAfter.

internal static readonly ICertificateCache s_mtlsCertificateCache = new InMemoryCertificateCache();

// Per-key async de-duplication so concurrent callers don’t double-mint.
internal static readonly ConcurrentDictionary<string, SemaphoreSlim> s_perKeyGates =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This design introduces unnecessary coupling and breaks encapsulation, leading to class envy. The responsibility should either reside where it’s used or be abstracted into a dedicated component.

Additionally, usage should avoid direct field access. Instead, expose functionality through well-defined internal or public methods behind a thoughtful interface.

For instance, why isn’t s_perKeyGates treated as an implementation detail of the imdsv2 component, given that it appears to be used only there? The same applies to the cache instance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

udpated

Endpoint = endpoint ?? throw new MsalClientException(nameof(endpoint));
ClientId = clientId ?? throw new MsalClientException(nameof(clientId));
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldnt we need comments for public members?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are internal

/// </summary>
bool TryGet(
string cacheKey,
out X509Certificate2 certificate,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very classic way of doing it, wouldnt it be better to use a tuple, valuetype result or even a record/structure here so that we have some flexibility with adding fields without breaking the contract?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

/// Process-local cache for an mTLS certificate and its endpoint.
/// Expiration is based solely on certificate.NotAfter.
/// </summary>
internal interface ICertificateCache : IDisposable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say that it shouldnt inerit form IDisposable, but the implementation has.

/// certificate+endpoint+clientId cache stored in process memory.
/// </summary>
internal sealed class InMemoryCertificateCache : ICertificateCache
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should implement IDisposable, not the interface, imho

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, updated

}

/// <summary>
/// used for logging cache keys without exposing full values.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Start from capital letter

/// <summary>
/// used for logging cache keys without exposing full values.
/// </summary>
/// <param name="s"></param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we fill those up as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated but this is internal

{
if (string.IsNullOrEmpty(s))
return "<empty>";
var take = Math.Min(8, s.Length);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so it would expose full value if it is less than 8? Is that ok?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

using (var httpManager = new MockHttpManager())
{
// Start clean across tests
ManagedIdentityClient.ResetSourceForTest();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you ensure that those tests dont race? Because we are using static state here, this could cause instabilities.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed this - and each test before initializing will always call ResetSourceForTest, so the static caches are cleared in PRODUCT.


internal static void ResetSourceForTest()
{
s_sourceName = ManagedIdentitySource.None;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we leak the dependency to test, this utility metod can belong in test itself....
A better approach would be to expose appropriate public methods on well encapsulated components.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved the one in IMDS V2 to it's own method and call it from here

…_cert

- Resolve conflicts in ManagedIdentityClient.cs and ImdsV2ManagedIdentitySource.cs
- Update tests (ImdsV2Tests, MockHelpers)
return; // already disposed
}

try
Copy link
Contributor

@MZOLN MZOLN Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try/catch is expensive. You won't double dispose with that atomic switch you got on line 70, would you?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

/// </summary>
void Set(
string cacheKey,
X509Certificate2 certificate,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we adjust the set as well? This looks better actually ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree - missed updating this

@gladjohn gladjohn marked this pull request as ready for review October 29, 2025 03:52
@gladjohn gladjohn requested a review from a team as a code owner October 29, 2025 03:52
@gladjohn gladjohn requested a review from MZOLN October 29, 2025 03:52
@MZOLN MZOLN merged commit 4b284c3 into rginsburg/msiv2_feature_branch Oct 29, 2025
3 checks passed
@MZOLN MZOLN deleted the gladjohn/cahed_msiv2_cert branch October 29, 2025 17:59
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.

3 participants