|
1 | 1 | --- |
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 |
| 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 |
5 | 5 | ms.service: entra-id |
6 | 6 | ms.date: 05/09/2025 |
7 | 7 | ms.custom: sap:Developing or Registering apps with Microsoft identity platform |
8 | 8 | --- |
9 | | -# How bundle consent for multiple application registrations |
| 9 | +# Microsoft Entra applications using TLS 1.0/1.1 fail to authenticate |
10 | 10 |
|
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. |
| 11 | +This article provides solutions to authentication errors that occur with Microsoft Entra-integrated applications targeting versions earlier than Microsoft .NET Framework 4.7. |
12 | 12 |
|
13 | | -## Step 1: Configure knownClientApplications for the API app registration |
| 13 | +## Symptoms |
14 | 14 |
|
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). |
| 15 | +Applications using an older version of the .NET Framework might encounter authentication failures with one of the following error messages: |
16 | 16 |
|
17 | | -## Step 2: Configure API permissions |
| 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 |
18 | 18 |
|
19 | | -Make sure that: |
| 19 | +- > IDX20804: Unable to retrieve document from: '[PII is hidden]' |
20 | 20 |
|
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. |
| 21 | +- > IDX20803: Unable to obtain configuration from: '[PII is hidden]' |
23 | 22 |
|
24 | | -## Step 3: The sign-in request |
| 23 | +- > IDX10803: Unable to create to obtain configuration from: 'https://login.microsoftonline.com/{Tenant-ID}/.well-known/openid-configuration' |
25 | 24 |
|
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. |
| 25 | +- > IDX20807: Unable to retrieve document from: 'System.String' |
27 | 26 |
|
28 | | -### Example Request for Microsoft accounts and Work or school accounts |
| 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 |
29 | 28 |
|
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 |
| 29 | +## Cause |
42 | 30 |
|
43 | | -If you are not supporting Microsoft Accounts: |
| 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) |
44 | 32 |
|
45 | | -```http |
| 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. |
46 | 34 |
|
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 | | -``` |
| 35 | +## Solution 1: Upgrade the .NET Framework |
65 | 36 |
|
66 | | -Consent propagation for new service principals and permissions may take time. Your application should handle this delay. |
| 37 | +Upgrade the application to use .NET Framework 4.7 or later, where TLS 1.2 is enabled by default. |
67 | 38 |
|
68 | | -### Acquire Tokens for Multiple Resources |
| 39 | +## Solution 2: Enable TLS 1.2 programmatically |
69 | 40 |
|
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 |
| 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: |
79 | 42 |
|
80 | 43 | ```csharp |
81 | | - public static async Task<AuthenticationResult> GetTokenAfterConsentAsync(string[] resourceScopes) |
82 | | - { |
83 | | - AuthenticationResult result = null; |
84 | | - int retryCount = 0; |
85 | | - |
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 | | - } |
| 44 | +using System.Net; |
99 | 45 |
|
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 | | - } |
| 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 | +} |
139 | 51 | ``` |
140 | 52 |
|
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++; |
| 53 | +## Solution 3: Change web.config to enable TLS 1.2 |
157 | 54 |
|
158 | | - if (result == null) |
159 | | - { |
160 | | - Thread.Sleep(1000 * retryCount * 2); |
161 | | - } |
162 | | - } |
| 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: |
163 | 56 |
|
164 | | -If (result==null) return new HttpStatusCodeResult(HttpStatusCode.Forbidden, "Need Consent"); |
| 57 | +```json |
| 58 | +<system.web> |
| 59 | + <httpRuntime targetFramework="4.7.2" /> |
| 60 | +</system.web> |
165 | 61 | ``` |
166 | 62 |
|
167 | | -If all retries fail, return an error and throw an error and instruct the client to initial a full consent process. |
| 63 | +> [!NOTE] |
| 64 | +> If using .NET Framework 4.7.2 causes breaking changes to your app, this solution might not work. |
168 | 65 |
|
169 | | -**Example of client code that assumes your API throws a 403** |
| 66 | +## Solution 4: Enable TLS 1.2 before running PowerShell commands |
170 | 67 |
|
171 | | -``` |
172 | | -HttpResponseMessage apiResult = null; |
173 | | -apiResult = await MockApiCall(result.AccessToken); |
| 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: |
174 | 69 |
|
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 | | -} |
| 70 | +```powershell |
| 71 | +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 |
186 | 72 | ``` |
187 | 73 |
|
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 | | - |
| 74 | +## References |
198 | 75 |
|
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 | | -``` |
| 76 | +[Transport Layer Security (TLS) best practices with .NET Framework](/dotnet/framework/network-programming/tls) |
269 | 77 |
|
270 | 78 | [!INCLUDE [Azure Help Support](../../../includes/azure-help-support.md)] |
0 commit comments