Skip to content

Commit 319af3c

Browse files
authored
Merge branch 'main' into main
2 parents be1fb7c + 522d9ce commit 319af3c

File tree

12 files changed

+94
-198
lines changed

12 files changed

+94
-198
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
4.79.2
2+
======
3+
4+
### Bug fixes
5+
* Bump winsdk dependency [#5575](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/5575)
6+
* ImdsV2 probe does not fire when .WithMtlsProofOfPossesstion is not used [#5579](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/5579)
7+
* Downgrade System.Formats.Asn1 to match ID web [#5583](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/5583)
8+
19
4.79.0
210
======
311

@@ -6,13 +14,15 @@
614
* Bearer Requests should Fallback to IMDS in Preview in https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/5562
715
* Updating MSAL to send client info = 2 on client credential flow in https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/5529
816
* Make `IMsalMtlsHttpClientFactory` interface public in https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/5559* Adjust WithExtraQueryParameters APIs and cache key behavior https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/5536
17+
* Adjust WithExtraQueryParameters APIs and cache key behavior [#5536](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/5536)
918

1019
### Bug fixes
1120
* Fix instance discovery bug in Fr cloud [#5549](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/5549)
1221
* Mark WithClientAssertion API as experimental [#5551](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/5551)
1322

1423

1524

25+
1626
4.78.0
1727
======
1828
### Changes

docs/msi_v2/cert_handling.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Certificate Handling on Windows for MSI v2 / PoP
2+
3+
## Overview
4+
In the MSI v2 flow, the client must present a certificate over mTLS.
5+
The certificate itself is the **credential** for this flow.
6+
7+
Currently, the certificate is stored only in memory.
8+
To reduce repeated calls to IMDS and enable reuse across requests,
9+
the certificate should be **rooted (persisted)** in the Windows certificate store.
10+
11+
## 1. Certificate Store Location
12+
- Use the **Windows Certificate Store**.
13+
- Scope: **CurrentUser\My** store (per-user, not global).
14+
- This provides:
15+
- Secure key isolation (KeyGuard).
16+
- OS-enforced ACLs instead of manual file permissions.
17+
18+
---
19+
20+
## 2. Certificate Lifecycle
21+
22+
1. **Check Cert Store**
23+
- Look in `CurrentUser\My` for an MSI certificate.
24+
- Identify by subject and issuer.
25+
- If found and not expired → load it.
26+
27+
2. **No Valid Certificate**
28+
- Generate a key pair (RSA) in KeyGuard (for POP).
29+
- Get an attestation token for the key.
30+
- Create CSR in memory.
31+
- Call **IMDS issuecredential** endpoint.
32+
- Receive certificate from IMDS.
33+
34+
3. **Persist the Certificate**
35+
- Store the new certificate (with private key) in `CurrentUser\My`.
36+
- Mark the key as **non-exportable**. (uses KeyGuard keys - already non-exportable)
37+
38+
4. **Use Certificate**
39+
- Load cert from store as needed.
40+
- Use it for mTLS handshake.
41+
42+
---
43+
44+
## 3. Expiration and Renewal
45+
- Certificates are short-lived (7 days).
46+
- Always check expiration before use.
47+
- If expired or close to expiry:
48+
- Remove stale cert from store.
49+
- Acquire and persist a new cert from IMDS.
50+
51+
### Proactive Renewal: Start renewal at half the certificate lifetime (typically, 3.5 days) to avoid expiry during active sessions.
52+
53+
**This is calculated based on the certificate’s NotAfter property (expiration date).**
54+
---
55+
56+
## 4. Error Handling
57+
- If store entry is corrupt → remove it and fetch new cert.
58+
- If IMDS call fails → bubble up error to caller.
59+
60+
---
61+
62+
# Certificate Store Location in Linux
63+
- Rely on .NET’s X509Store abstractions for per-user certificate storage. The framework handles platform-specific details.”
64+
65+
---
66+
67+
## 5. Security Considerations
68+
- Private keys must be generated in **CNG/KeyGuard** with `ExportPolicy = None`.
69+
- Keys should **never** be exported or persisted outside the Windows certificate store.
70+
- `CurrentUser\My` is scoped to the signed-in user.
71+
72+
---
73+
74+
## ✅ Summary
75+
- Always **check Windows cert store first** (`CurrentUser\My`).
76+
- If no valid cert, **request one from IMDS**.
77+
- **Persist it** back into the store securely.
78+
- **Refresh on expiration**.

tests/Microsoft.Identity.Test.LabInfrastructure/CertificateHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public static X509Certificate2 FindCertificateByName(string subjectName)
3636
/// <param subjectName="location"><see cref="StoreLocation"/> in which to search for a matching certificate</param>
3737
/// <param subjectName="name"><see cref="StoreName"/> in which to search for a matching certificate</param>
3838
/// <returns><see cref="X509Certificate2"/> with <paramref subjectName="certName"/>, or null if no matching certificate was found</returns>
39-
public static X509Certificate2 FindCertificateByName(string certName, StoreLocation location, StoreName name)
39+
private static X509Certificate2 FindCertificateByName(string certName, StoreLocation location, StoreName name)
4040
{
4141
// Unix LocalMachine X509Store is limited to the Root and CertificateAuthority stores
4242
if (SharedUtilities.IsLinuxPlatform())

tests/Microsoft.Identity.Test.LabInfrastructure/KeyVaultConfiguration.cs

Lines changed: 0 additions & 38 deletions
This file was deleted.

tests/Microsoft.Identity.Test.LabInfrastructure/KeyVaultSecretsProvider.cs

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,33 +29,7 @@ public class KeyVaultSecretsProvider : IDisposable
2929
private CertificateClient _certificateClient;
3030
private SecretClient _secretClient;
3131

32-
/// <summary>Initialize the secrets provider with the "keyVault" configuration section.</summary>
33-
/// <remarks>
34-
/// <para>
35-
/// Authentication using <see cref="LabAccessAuthenticationType.ClientCertificate"/>
36-
/// 1. Register Azure AD application of "Web app / API" type.
37-
/// To set up certificate based access to the application PowerShell should be used.
38-
/// 2. Add an access policy entry to target Key Vault instance for this application.
39-
///
40-
/// The "keyVault" configuration section should define:
41-
/// "authType": "ClientCertificate"
42-
/// "clientId": [client ID]
43-
/// "certThumbprint": [certificate thumbprint]
44-
/// </para>
45-
/// <para>
46-
/// Authentication using <see cref="LabAccessAuthenticationType.UserCredential"/>
47-
/// 1. Register Azure AD application of "Native" type.
48-
/// 2. Add to 'Required permissions' access to 'Azure Key Vault (AzureKeyVault)' API.
49-
/// 3. When you run your native client application, it will automatically prompt user to enter Azure AD credentials.
50-
/// 4. To successfully access keys/secrets in the Key Vault, the user must have specific permissions to perform those operations.
51-
/// This could be achieved by directly adding an access policy entry to target Key Vault instance for this user
52-
/// or an access policy entry for an Azure AD security group of which this user is a member of.
53-
///
54-
/// The "keyVault" configuration section should define:
55-
/// "authType": "UserCredential"
56-
/// "clientId": [client ID]
57-
/// </para>
58-
/// </remarks>
32+
5933
public KeyVaultSecretsProvider(string keyVaultAddress = KeyVaultInstance.MSIDLab)
6034
{
6135
var credentials = GetKeyVaultCredentialAsync().GetAwaiter().GetResult();

tests/Microsoft.Identity.Test.LabInfrastructure/LabAuthenticationHelper.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,4 @@ public static async Task<AccessToken> GetLabAccessTokenAsync(string authority, s
7070
}
7171
}
7272

73-
public enum LabAccessAuthenticationType
74-
{
75-
ClientCertificate,
76-
ClientSecret,
77-
UserCredential
78-
}
7973
}

tests/Microsoft.Identity.Test.LabInfrastructure/LabResponse.cs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,12 @@ public class LabApp
2626
[JsonProperty("redirecturi")]
2727
public string RedirectUri { get; set; }
2828

29-
[JsonProperty("signinaudience")]
30-
public string Audience { get; set; }
31-
3229
// TODO: this is a list, but lab sends a string. Not used today, discuss with lab to return a list
3330
[JsonProperty("authority")]
3431
public string Authority { get; set; }
3532

3633
[JsonProperty("defaultscopes")]
3734
public string DefaultScopes { get; set; }
38-
3935
}
4036

4137
public class Lab
@@ -46,16 +42,7 @@ public class Lab
4642
[JsonProperty("federationprovider")]
4743
public FederationProvider FederationProvider { get; set; }
4844

49-
[JsonProperty("credentialvaultkeyname")]
50-
public string CredentialVaultkeyName { get; set; }
51-
5245
[JsonProperty("authority")]
5346
public string Authority { get; set; }
5447
}
55-
56-
public class LabCredentialResponse
57-
{
58-
[JsonProperty("Value")]
59-
public string Secret { get; set; }
60-
}
6148
}

tests/Microsoft.Identity.Test.LabInfrastructure/LabServiceApi.cs

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -75,35 +75,13 @@ private Task<string> RunQueryAsync(UserQuery query)
7575
if (string.IsNullOrEmpty(query.Upn))
7676
{
7777
//Building user query
78-
//Required parameters will be set to default if not supplied by the test code
79-
80-
queryDict.Add(
81-
LabApiConstants.MultiFactorAuthentication,
82-
query.MFA != null ?
83-
query.MFA.ToString() :
84-
MFA.None.ToString());
85-
86-
queryDict.Add(
87-
LabApiConstants.ProtectionPolicy,
88-
query.ProtectionPolicy != null ?
89-
query.ProtectionPolicy.ToString() :
90-
ProtectionPolicy.None.ToString());
91-
78+
//Required parameters will be set to default if not supplied by the test code
79+
9280
if (query.UserType != null)
9381
{
9482
queryDict.Add(LabApiConstants.UserType, query.UserType.ToString());
9583
}
96-
97-
if (query.HomeDomain != null)
98-
{
99-
queryDict.Add(LabApiConstants.HomeDomain, query.HomeDomain.ToString());
100-
}
101-
102-
if (query.HomeUPN != null)
103-
{
104-
queryDict.Add(LabApiConstants.HomeUPN, query.HomeUPN.ToString());
105-
}
106-
84+
10785
if (query.B2CIdentityProvider != null)
10886
{
10987
queryDict.Add(LabApiConstants.B2CProvider, query.B2CIdentityProvider.ToString());
@@ -168,17 +146,6 @@ internal async Task<string> GetLabResponseAsync(string address)
168146
}
169147
}
170148

171-
public async Task<string> GetUserSecretAsync(string lab)
172-
{
173-
Dictionary<string, string> queryDict = new Dictionary<string, string>
174-
{
175-
{ "secret", lab }
176-
};
177-
178-
string result = await SendLabRequestAsync(LabApiConstants.LabUserCredentialEndpoint, queryDict).ConfigureAwait(false);
179-
return JsonConvert.DeserializeObject<LabCredentialResponse>(result).Secret;
180-
}
181-
182149
public async Task<string> GetMSIHelperServiceTokenAsync()
183150
{
184151
if (_msiHelperApiAccessToken == null)

tests/Microsoft.Identity.Test.LabInfrastructure/LabUser.cs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,6 @@ public class LabUser
1818
[JsonProperty("upn")]
1919
public string Upn { get; set; }
2020

21-
[JsonProperty("displayname")]
22-
public string DisplayName { get; set; }
23-
24-
[JsonProperty("mfa")]
25-
public MFA Mfa { get; set; }
26-
27-
[JsonProperty("protectionpolicy")]
28-
public ProtectionPolicy ProtectionPolicy { get; set; }
29-
30-
[JsonProperty("homedomain")]
31-
public HomeDomain HomeDomain { get; set; }
32-
3321
[JsonProperty("homeupn")]
3422
public string HomeUPN { get; set; }
3523

@@ -41,8 +29,6 @@ public class LabUser
4129

4230
public FederationProvider FederationProvider { get; set; }
4331

44-
public string Credential { get; set; }
45-
4632
public string TenantId { get; set; }
4733

4834
private string _password = null;

tests/Microsoft.Identity.Test.LabInfrastructure/LabUserHelper.cs

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,7 @@ private static LabResponse MergeLabResponses(LabResponse primary, LabResponse se
136136

137137
return primaryJson.ToObject<LabResponse>();
138138
}
139-
140-
[Obsolete("Use GetSpecificUserAsync instead", true)]
141-
public static Task<LabResponse> GetLabUserDataForSpecificUserAsync(string upn)
142-
{
143-
throw new NotSupportedException();
144-
}
145-
139+
146140
public static async Task<string> GetMSIEnvironmentVariablesAsync(string uri)
147141
{
148142
string result = await s_labService.GetLabResponseAsync(uri).ConfigureAwait(false);
@@ -187,33 +181,6 @@ public static Task<LabResponse> GetB2CLocalAccountAsync()
187181
return GetLabUserDataAsync(UserQuery.B2CLocalAccountUserQuery);
188182
}
189183

190-
public static Task<LabResponse> GetB2CFacebookAccountAsync()
191-
{
192-
return GetLabUserDataAsync(UserQuery.B2CFacebookUserQuery);
193-
}
194-
195-
public static Task<LabResponse> GetB2CGoogleAccountAsync()
196-
{
197-
return GetLabUserDataAsync(UserQuery.B2CGoogleUserQuery);
198-
}
199-
200-
public static async Task<LabResponse> GetB2CMSAAccountAsync()
201-
{
202-
var response = await GetLabUserDataAsync(UserQuery.B2CMSAUserQuery).ConfigureAwait(false);
203-
if (string.IsNullOrEmpty(response.User.HomeUPN) ||
204-
string.Equals("None", response.User.HomeUPN, StringComparison.OrdinalIgnoreCase))
205-
{
206-
Debug.WriteLine($"B2C MSA HomeUPN set to UPN: {response.User.Upn}");
207-
response.User.HomeUPN = response.User.Upn;
208-
}
209-
return response;
210-
}
211-
212-
public static Task<LabResponse> GetSpecificUserAsync(string upn)
213-
{
214-
return GetLabUserDataAsync(new UserQuery() { Upn = upn });
215-
}
216-
217184
public static Task<LabResponse> GetArlingtonUserAsync()
218185
{
219186
var response = GetLabUserDataAsync(UserQuery.ArlingtonUserQuery);

0 commit comments

Comments
 (0)