Skip to content

Commit 235984a

Browse files
committed
add new article
1 parent b30517f commit 235984a

1 file changed

Lines changed: 232 additions & 40 deletions

File tree

Lines changed: 232 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,270 @@
11
---
2-
title: Microsoft Entra Applications Using TLS 1.0/1.1 Fail to Authenticate
3-
description: Provides solutions to authentication errors that occur with Microsoft Entra applications using TLS version 1.0 or 1.1.
4-
ms.reviewer: bachoang, v-weizhu
2+
title: Enable Bundled Consent for Multiple Application Registrations in Azure AD
3+
description: Describes how to bundle consent for application registrations
4+
ms.reviewer: willfid
55
ms.service: entra-id
66
ms.date: 05/09/2025
77
ms.custom: sap:Developing or Registering apps with Microsoft identity platform
88
---
9-
# Microsoft Entra applications using TLS 1.0/1.1 fail to authenticate
9+
# How bundle consent for multiple application registrations
1010

11-
This article provides solutions to authentication errors that occur with Microsoft Entra-integrated applications targeting versions earlier than Microsoft .NET Framework 4.7.
11+
In scenarios that you have a custom client application and a custom API. Each registered as separate applications in Microsoft Entra ID. You may want to streamline the user experience by allowing users to consent to both applications at once. This article explains how to configure bundled consent so that users can grant permissions to multiple apps in a single step.
1212

13-
## Symptoms
13+
## Step 1: Configure knownClientApplications for the API app registration
1414

15-
Applications using an older version of the .NET Framework might encounter authentication failures with one of the following error messages:
15+
Add the custom client app ID to the custom APIs app registration `knownClientApplications` property. For more information, see [knownClientApplications attribute](/entra/identity-platform/reference-app-manifest#knownclientapplications-attribute).
1616

17-
- > AADSTS1002016: You are using TLS version 1.0, 1.1 and/or 3DES cipher which are deprecated to improve the security posture of Azure AD
17+
## Step 2: Configure API permissions
1818

19-
- > IDX20804: Unable to retrieve document from: '[PII is hidden]'
19+
Make sure that:
2020

21-
- > IDX20803: Unable to obtain configuration from: '[PII is hidden]'
21+
- All required API permissions are correctly configured on both the custom client and custom API app registrations.
22+
- The custom client app registration includes the API permissions defined in the custom API app registration.
2223

23-
- > IDX10803: Unable to create to obtain configuration from: 'https://login.microsoftonline.com/{Tenant-ID}/.well-known/openid-configuration'
24+
## Step 3: The sign-in request
2425

25-
- > IDX20807: Unable to retrieve document from: 'System.String'
26+
Your authentication request must use the `.default` scope for Microsoft Graph. For Microsoft accounts, the scope must be for the custom API. This also works for school and work accounts.
2627

27-
- > System.Net.Http.Headers.HttpResponseHeaders RequestMessage {Method: POST, RequestUri: '\<request-uri>', Version: 1.1, Content: System.Net.Http.FormUrlEncodedContent, Headers: { Content-Type: application/x-www-form-urlencoded Content-Length: 970 }} System.Net.Http.HttpRequestMessage StatusCode UpgradeRequired This service requires use of the TLS-1.2 protocol
28+
### Example Request for Microsoft accounts and Work or school accounts
2829

29-
## Cause
30+
```HTTP
31+
https://login.microsoftonline.com/common/oauth2/v2.0/authorize
32+
?response_type=code
33+
&Client_id=72333f42-5078-4212-abb2-e4f9521ec76a
34+
&redirect_uri=https://localhost
35+
&scope=openid profile offline_access app_uri_id1/.default
36+
&prompt=consent
37+
```
38+
> [NOTE!]
39+
> The client will not appear as having permission for the API. This is expected because the client is listed as a knownClientApplication.
40+
41+
### Example request for Work or school accounts only
3042

31-
Starting January 31, 2022, Microsoft enforced the use of the TLS 1.2 protocol for client applications connecting to Microsoft Entra services on the Microsoft Identity Platform to ensure compliance with security and industry standards. For more information about this change, see [Enable support for TLS 1.2 in your environment for Microsoft Entra TLS 1.1 and 1.0 deprecation](../ad-dmn-services/enable-support-tls-environment.md) and [Act fast to secure your infrastructure by moving to TLS 1.2!](https://techcommunity.microsoft.com/blog/microsoft-entra-blog/act-fast-to-secure-your-infrastructure-by-moving-to-tls-1-2/2967457)
43+
If you are not supporting Microsoft Accounts:
3244

33-
Applications running on older platforms or using older .NET Framework versions might not have TLS 1.2 enabled. Therefore, they can't retrieve the OpenID Connect metadata document, resulting in failed authentication.
45+
```http
3446
35-
## Solution 1: Upgrade the .NET Framework
47+
GET https://login.microsoftonline.com/common/oauth2/v2.0/authorize
48+
?response_type=code
49+
&client_id=72333f42-5078-4212-abb2-e4f9521ec76a
50+
&redirect_uri=https://localhost
51+
&scope=openid profile offline_access User.Read https://graph.microsoft.com/.default
52+
&prompt=consent
53+
54+
```
55+
56+
### Implementation with MSAL.NET
57+
58+
```http
59+
String[] consentScope = { "api://ae5a0bbe-d6b3-4a20-867b-c8d9fd442160/.default" };
60+
var loginResult = await clientApp.AcquireTokenInteractive(consentScope)
61+
.WithAccount(account)
62+
.WithPrompt(Prompt.Consent)
63+
.ExecuteAsync();
64+
```
3665

37-
Upgrade the application to use .NET Framework 4.7 or later, where TLS 1.2 is enabled by default.
66+
Consent propagation for new service principals and permissions may take time. Your application should handle this delay.
3867

39-
## Solution 2: Enable TLS 1.2 programmatically
68+
### Acquire Tokens for Multiple Resources
4069

41-
If upgrading the .NET Framework isn't feasible, you can enable TLS 1.2 by adding the following code to the **Global.asax.cs** file in your application:
70+
If your client app needs to acquire tokens for another resource such as Microsoft Graph, you must implement logic to handle potential delays after user consent. Here are some recommendations:
71+
72+
- Use the `.default` scope when requesting tokens.
73+
- Track acquired scopes until the required one is returned
74+
- Add a delay if the result still does not have the required scope.
75+
76+
Currently, if `acquireTokenSilent` fails, MSAL will force you to perform a successful interaction before it will allow you to use `AcquireTokenSilent` again, even if you have a valid refresh token to use.
77+
78+
Here is some sample code of retry logic
4279

4380
```csharp
44-
using System.Net;
81+
public static async Task<AuthenticationResult> GetTokenAfterConsentAsync(string[] resourceScopes)
82+
{
83+
AuthenticationResult result = null;
84+
int retryCount = 0;
4585

46-
protected void Application_Start()
47-
{
48-
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Ssl3; // only allow TLS 1.2 and SSL 3
49-
// The rest of your startup code goes here
50-
}
86+
int index = resourceScopes[0].LastIndexOf("/");
87+
88+
string resource = String.Empty;
89+
90+
// Determine resource of scope
91+
if (index < 0)
92+
{
93+
resource = "https://graph.microsoft.com";
94+
}
95+
else
96+
{
97+
resource = resourceScopes[0].Substring(0, index);
98+
}
99+
100+
string[] defaultScope = { $"{resource}/.default" };
101+
102+
string[] acquiredScopes = { "" };
103+
string[] scopes = defaultScope;
104+
105+
while (!acquiredScopes.Contains(resourceScopes[0]) && retryCount <= 15)
106+
{
107+
try
108+
{
109+
result = await clientApp.AcquireTokenSilent(scopes, CurrentAccount).WithForceRefresh(true).ExecuteAsync();
110+
acquiredScopes = result.Scopes.ToArray();
111+
if (acquiredScopes.Contains(resourceScopes[0])) continue;
112+
}
113+
catch (Exception e)
114+
{ }
115+
116+
// Switch scopes to pass to MSAL on next loop. This tricks MSAL to force AcquireTokenSilent after failure. This also resolves intermittent cachine issue in ESTS
117+
scopes = scopes == resourceScopes ? defaultScope : resourceScopes;
118+
retryCount++;
119+
120+
// Obvisouly something went wrong
121+
if(retryCount==15)
122+
{
123+
throw new Exception();
124+
}
125+
126+
// MSA tokens do not return scope in expected format when .default is used
127+
int i = 0;
128+
foreach(var acquiredScope in acquiredScopes)
129+
{
130+
if(acquiredScope.IndexOf('/')==0) acquiredScopes[i].Replace("/", $"{resource}/");
131+
i++;
132+
}
133+
134+
Thread.Sleep(2000);
135+
}
136+
137+
return result;
138+
}
51139
```
52140

53-
## Solution 3: Change web.config to enable TLS 1.2
141+
### On the custom API using the On-behalf-of flow
142+
143+
In the same way the client app does, when your custom API tries to acquire tokens for another resource using the On-Behalf-Of (OBO) flow, it may fail immediately after consent. To resolve this issue, you can implement retry logic and scope tracking as the following sample:
144+
145+
```csharp
146+
while (result == null && retryCount >= 6)
147+
{
148+
UserAssertion assertion = new UserAssertion(accessToken);
149+
try
150+
{
151+
result = await apiMsalClient.AcquireTokenOnBehalfOf(scopes, assertion).ExecuteAsync();
152+
153+
}
154+
catch { }
155+
156+
retryCount++;
54157

55-
If .NET Framework 4.7.2 is available, you can enable TLS 1.2 by adding the following configuration to the **web.config** file:
158+
if (result == null)
159+
{
160+
Thread.Sleep(1000 * retryCount * 2);
161+
}
162+
}
56163

57-
```json
58-
<system.web>
59-
    <httpRuntime targetFramework="4.7.2" />
60-
</system.web>
164+
If (result==null) return new HttpStatusCodeResult(HttpStatusCode.Forbidden, "Need Consent");
61165
```
62166

63-
> [!NOTE]
64-
> If using .NET Framework 4.7.2 causes breaking changes to your app, this solution might not work.
167+
If all retries fail, return an error and throw an error and instruct the client to initial a full consent process.
65168

66-
## Solution 4: Enable TLS 1.2 before running PowerShell commands
169+
**Example of client code that assumes your API throws a 403**
67170

68-
If you encounter the AADSTS1002016 error while running the PowerShell command `Connect-MSolService`, `Connect-AzureAD`, or `Connect-MSGraph` (from the Microsoft Intune PowerShell SDK module), set the security protocol to TLS 1.2 before executing the commands:
171+
```
172+
HttpResponseMessage apiResult = null;
173+
apiResult = await MockApiCall(result.AccessToken);
69174
70-
```powershell
71-
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
175+
if(apiResult.StatusCode==HttpStatusCode.Forbidden)
176+
{
177+
var authResult = await clientApp.AcquireTokenInteractive(apiDefaultScope)
178+
.WithAccount(account)
179+
.WithPrompt(Prompt.Consent)
180+
.ExecuteAsync();
181+
CurrentAccount = authResult.Account;
182+
183+
// Retry API call
184+
apiResult = await MockApiCall(result.AccessToken);
185+
}
72186
```
73187

74-
## References
188+
## Recommendations and expected behavior
189+
190+
Building an app for handling bundled consent is not as straight forward. Preferably you have a separate process you can walk your users through to perform this bundled consent, provision your app and API within their tenant or on their Microsoft Account and only get the consent experience once. (Separate from actually signing into the app.) If you don’t have this process and trying to build it into your app and your sign in experience, it gets messy and your users will have multiple consent prompts. I would recommend that you build a experience within your app that warns users they may get prompted to consent (multiple times).
191+
192+
For Microsoft Accounts, I would expect minimum of two consent prompts. One for the application, and one for the API.
193+
194+
For work and school accounts, I would expect only one consent prompt. Azure AD handles bundled consent much better than Microsoft Accounts.
195+
196+
Here is a end to end example sample of code. This has a pretty good user experience considering trying to support all account types and only prompting consent if required. Its not perfect as perfect is virtually non-existent.
197+
75198

76-
[Transport Layer Security (TLS) best practices with .NET Framework](/dotnet/framework/network-programming/tls)
199+
200+
```csharp
201+
string[] msGraphScopes = { "User.Read", "Mail.Send", "Calendar.Read" }
202+
String[] apiScopes = { "api://ae5a0bbe-d6b3-4a20-867b-c8d9fd442160/access_as_user" };
203+
String[] msGraphDefaultScope = { "https://graph.microsoft.com/.default" };
204+
String[] apiDefaultScope = { "api://ae5a0bbe-d6b3-4a20-867b-c8d9fd442160/.default" };
205+
206+
var accounts = await clientApp.GetAccountsAsync();
207+
IAccount account = accounts.FirstOrDefault();
208+
209+
AuthenticationResult msGraphTokenResult = null;
210+
AuthenticationResult apiTokenResult = null;
211+
212+
try
213+
{
214+
msGraphTokenResult = await clientApp.AcquireTokenSilent(msGraphScopes, account).ExecuteAsync();
215+
apiTokenResult = await clientApp.AcquireTokenSilent(apiScopes, account).ExecuteAsync();
216+
}
217+
catch (Exception e1)
218+
{
219+
220+
string catch1Message = e1.Message;
221+
string catch2Message = String.Empty;
222+
223+
try
224+
{
225+
// First possible consent experience
226+
var result = await clientApp.AcquireTokenInteractive(apiScopes)
227+
.WithExtraScopesToConsent(msGraphScopes)
228+
.WithAccount(account)
229+
.ExecuteAsync();
230+
CurrentAccount = result.Account;
231+
msGraphTokenResult = await clientApp.AcquireTokenSilent(msGraphScopes, CurrentAccount).ExecuteAsync();
232+
apiTokenResult = await clientApp.AcquireTokenSilent(apiScopes, CurrentAccount).ExecuteAsync();
233+
}
234+
catch(Exception e2)
235+
{
236+
catch2Message = e2.Message;
237+
};
238+
239+
if(catch1Message.Contains("AADSTS650052") || catch2Message.Contains("AADSTS650052") || catch1Message.Contains("AADSTS70000") || catch2Message.Contains("AADSTS70000"))
240+
{
241+
// Second possible consent experience
242+
var result = await clientApp.AcquireTokenInteractive(apiDefaultScope)
243+
.WithAccount(account)
244+
.WithPrompt(Prompt.Consent)
245+
.ExecuteAsync();
246+
CurrentAccount = result.Account;
247+
msGraphTokenResult = await GetTokenAfterConsentAsync(msGraphScopes);
248+
apiTokenResult = await GetTokenAfterConsentAsync(apiScopes);
249+
}
250+
}
251+
252+
// Call API
253+
254+
apiResult = await MockApiCall(apiTokenResult.AccessToken);
255+
var contentMessage = await apiResult.Content.ReadAsStringAsync();
256+
257+
if(apiResult.StatusCode==HttpStatusCode.Forbidden)
258+
{
259+
var result = await clientApp.AcquireTokenInteractive(apiDefaultScope)
260+
.WithAccount(account)
261+
.WithPrompt(Prompt.Consent)
262+
.ExecuteAsync();
263+
CurrentAccount = result.Account;
264+
265+
// Retry API call
266+
apiResult = await MockApiCall(result.AccessToken);
267+
}
268+
```
77269

78270
[!INCLUDE [Azure Help Support](../../../includes/azure-help-support.md)]

0 commit comments

Comments
 (0)