Skip to content

Commit 57e6ba8

Browse files
authored
Merge pull request #225 from microsoft/release/update/220107073544
Sync up from Mainline branch for CommitId 6636c16f03f6b9276362d2c2b29…
2 parents bb98317 + bb0e24d commit 57e6ba8

24 files changed

Lines changed: 25027 additions & 111 deletions

src/Build.Common.core.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</PropertyGroup>
66

77
<PropertyGroup Condition="'$(ProjectSpecificFx)' == ''">
8-
<TargetFrameworks>net462;net472;net48;netcoreapp3.0;netcoreapp3.1</TargetFrameworks>
8+
<TargetFrameworks>net462;net472;net48;netcoreapp3.0;netcoreapp3.1;netstandard2.0</TargetFrameworks>
99
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
1010
</PropertyGroup>
1111

src/GeneralTools/DataverseClient/Client/Auth/AuthProcessor.cs

Lines changed: 8 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Microsoft.Extensions.DependencyInjection;
1+
using Microsoft.Extensions.DependencyInjection;
22
using Microsoft.Identity.Client;
33
using Microsoft.PowerPlatform.Dataverse.Client.Auth.TokenCache;
44
using Microsoft.PowerPlatform.Dataverse.Client.Utils;
@@ -90,11 +90,11 @@ internal async static Task<ExecuteAuthenticationResults> ExecuteAuthenticateServ
9090
}
9191
else
9292
{
93-
var rslt = GetAuthorityFromTargetServiceAsync(ClientServiceProviders.Instance.GetService<IHttpClientFactory>(), processResult.TargetServiceUrl, logSink).ConfigureAwait(false).GetAwaiter().GetResult();
94-
if (!string.IsNullOrEmpty(rslt.Authority))
93+
var details = GetAuthorityFromTargetServiceAsync(ClientServiceProviders.Instance.GetService<IHttpClientFactory>(), processResult.TargetServiceUrl, logSink).ConfigureAwait(false).GetAwaiter().GetResult();
94+
if (details.Success)
9595
{
96-
Authority = rslt.Authority;
97-
Resource = rslt.Resource;
96+
Authority = details.Authority.AbsoluteUri;
97+
Resource = details.Resource.AbsoluteUri;
9898
}
9999
else
100100
throw new ArgumentNullException("Authority", "Need a non-empty authority");
@@ -446,85 +446,18 @@ internal static UriBuilder GetUriBuilderWithVersion(Uri discoveryServiceUri)
446446
return versionTaggedUriBuilder;
447447
}
448448

449-
450-
private const string AuthenticateHeader = "WWW-Authenticate";
451-
private const string Bearer = "bearer";
452-
private const string AuthorityKey = "authorization_uri";
453-
private const string ResourceKey = "resource_id";
454-
455-
internal class AuthRoutingProperties
456-
{
457-
public string Authority { get; set; }
458-
public string Resource { get; set; }
459-
}
460-
461449
/// <summary>
462450
/// Get authority and resource for this instance.
463451
/// </summary>
464452
/// <param name="targetServiceUrl">URI to query</param>
465453
/// <param name="logger">Logger to write info too</param>
466454
/// <param name="clientFactory">HTTP Client factory to use for this request.</param>
467455
/// <returns></returns>
468-
private static async Task<AuthRoutingProperties> GetAuthorityFromTargetServiceAsync(IHttpClientFactory clientFactory, Uri targetServiceUrl, DataverseTraceLogger logger)
456+
private static async Task<AuthenticationDetails> GetAuthorityFromTargetServiceAsync(IHttpClientFactory clientFactory, Uri targetServiceUrl, DataverseTraceLogger logger)
469457
{
470-
AuthRoutingProperties authRoutingProperties = new AuthRoutingProperties();
471458
var client = clientFactory.CreateClient("DataverseHttpClientFactory");
472-
var rslt = await client.GetAsync(targetServiceUrl).ConfigureAwait(false);
473-
474-
if (rslt.StatusCode == System.Net.HttpStatusCode.NotFound || rslt.StatusCode == System.Net.HttpStatusCode.BadRequest)
475-
{
476-
// didn't find endpoint.
477-
logger.Log($"Failed to get Authority and Resource error. Attempt to Access Endpoint {targetServiceUrl.ToString()} resulted in {rslt.StatusCode}.", TraceEventType.Error);
478-
return authRoutingProperties;
479-
}
480-
481-
if (rslt.Headers.Contains("WWW-Authenticate"))
482-
{
483-
var authenticateHeader = rslt.Headers.GetValues("WWW-Authenticate").FirstOrDefault();
484-
authenticateHeader = authenticateHeader.Trim();
485-
486-
// This also checks for cases like "BearerXXXX authorization_uri=...." and "Bearer" and "Bearer "
487-
if (!authenticateHeader.StartsWith(Bearer, StringComparison.OrdinalIgnoreCase)
488-
|| authenticateHeader.Length < Bearer.Length + 2
489-
|| !char.IsWhiteSpace(authenticateHeader[Bearer.Length]))
490-
{
491-
//var ex = new ArgumentException(AdalErrorMessage.InvalidAuthenticateHeaderFormat,
492-
// nameof(authenticateHeader));
493-
//CoreLoggerBase.Default.Error(AdalErrorMessage.InvalidAuthenticateHeaderFormat);
494-
//CoreLoggerBase.Default.ErrorPii(ex);
495-
//throw ex;
496-
}
497-
498-
authenticateHeader = authenticateHeader.Substring(Bearer.Length).Trim();
499-
500-
IDictionary<string, string> authenticateHeaderItems = null;
501-
try
502-
{
503-
authenticateHeaderItems =
504-
EncodingHelper.ParseKeyValueListStrict(authenticateHeader, ',', false, true);
505-
}
506-
catch //(ArgumentException ex)
507-
{
508-
//var newEx = new ArgumentException(AdalErrorMessage.InvalidAuthenticateHeaderFormat,
509-
// nameof(authenticateHeader), ex);
510-
//CoreLoggerBase.Default.Error(AdalErrorMessage.InvalidAuthenticateHeaderFormat);
511-
//CoreLoggerBase.Default.ErrorPii(newEx);
512-
//throw newEx;
513-
}
514-
515-
if (authenticateHeaderItems != null)
516-
{
517-
string param;
518-
authenticateHeaderItems.TryGetValue(AuthorityKey, out param);
519-
authRoutingProperties.Authority =
520-
param.Replace("oauth2/authorize", "") // swap out the old oAuth pattern.
521-
.Replace("common", "organizations"); // swap common for organizations because MSAL reasons.
522-
authenticateHeaderItems.TryGetValue(ResourceKey, out param);
523-
authRoutingProperties.Resource = param;
524-
}
525-
}
526-
527-
return authRoutingProperties;
459+
var resolver = new AuthorityResolver(client, (t, msg) => logger.Log(msg, t));
460+
return await resolver.ProbeForExpectedAuthentication(targetServiceUrl);
528461
}
529462

530463
/// <summary>
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Linq;
5+
using System.Net;
6+
using System.Net.Http;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.PowerPlatform.Dataverse.Client.Auth
10+
{
11+
/// <summary>
12+
/// Details of expected authentication.
13+
/// </summary>
14+
public sealed class AuthenticationDetails
15+
{
16+
/// <summary>
17+
/// True if probing returned a WWW-Authenticate header.
18+
/// </summary>
19+
public bool Success { get; internal set; }
20+
21+
/// <summary>
22+
/// Authority to initiate OAuth flow with.
23+
/// </summary>
24+
// TODO: the 2 Uris here should be nullable: Uri? but that requires to update C# used for this solution from current 7.x to C# 9 or 10
25+
public Uri Authority { get; internal set; }
26+
27+
/// <summary>
28+
/// OAuth resource to request authentication for.
29+
/// </summary>
30+
public Uri Resource { get; internal set; }
31+
}
32+
33+
/// <summary>
34+
/// Probes API endpoint to elicit a 401 response with the WWW-Authenticate header and processes the found information
35+
/// </summary>
36+
public sealed class AuthorityResolver
37+
{
38+
private const string AuthenticateHeader = "WWW-Authenticate";
39+
private const string Bearer = "bearer";
40+
private const string AuthorityKey = "authorization_uri";
41+
private const string ResourceKey = "resource_id";
42+
43+
private readonly HttpClient _httpClient;
44+
private readonly Action<TraceEventType, string> _logger;
45+
46+
/// <summary>
47+
/// instantiate resolver, using specified HttpClient to be used.
48+
/// </summary>
49+
/// <param name="httpClient"></param>
50+
/// <param name="logger"></param>
51+
public AuthorityResolver(HttpClient httpClient, Action<TraceEventType, string> logger = null)
52+
{
53+
_ = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
54+
55+
_httpClient = httpClient;
56+
_logger = logger;
57+
}
58+
59+
/// <summary>
60+
/// Attemtps to solicit a WWW-Authenticate reply using an unauthenticated GET call to the given endpoint.
61+
/// Parses returned header for details
62+
/// </summary>
63+
/// <param name="endpoint"></param>
64+
/// <returns></returns>
65+
/// <exception cref="ArgumentNullException"></exception>
66+
public async Task<AuthenticationDetails> ProbeForExpectedAuthentication(Uri endpoint)
67+
{
68+
_ = endpoint ?? throw new ArgumentNullException(nameof(endpoint));
69+
var details = new AuthenticationDetails();
70+
71+
HttpResponseMessage response;
72+
try
73+
{
74+
response = await _httpClient.GetAsync(endpoint).ConfigureAwait(false);
75+
}
76+
catch (HttpRequestException ex)
77+
{
78+
var errDetails = string.Empty;
79+
if (ex.InnerException is WebException wex)
80+
{
81+
errDetails = $"; details: {wex.Message} ({wex.Status})";
82+
}
83+
LogError($"Failed to get response from: {endpoint}; error: {ex.Message}{errDetails}");
84+
return details;
85+
}
86+
87+
88+
if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.BadRequest)
89+
{
90+
// didn't find endpoint.
91+
LogError($"Failed to get Authority and Resource error. Attempt to Access Endpoint {endpoint} resulted in {response.StatusCode}.");
92+
return details;
93+
}
94+
95+
if (response.Headers.Contains(AuthenticateHeader))
96+
{
97+
var authenticateHeader = response.Headers.GetValues(AuthenticateHeader).FirstOrDefault();
98+
authenticateHeader = authenticateHeader.Trim();
99+
100+
// This also checks for cases like "BearerXXXX authorization_uri=...." and "Bearer" and "Bearer "
101+
if (!authenticateHeader.StartsWith(Bearer, StringComparison.OrdinalIgnoreCase)
102+
|| authenticateHeader.Length < Bearer.Length + 2
103+
|| !char.IsWhiteSpace(authenticateHeader[Bearer.Length]))
104+
{
105+
LogError($"Malformed 'Bearer' format: {authenticateHeader}");
106+
return details;
107+
}
108+
109+
authenticateHeader = authenticateHeader.Substring(Bearer.Length).Trim();
110+
111+
IDictionary<string, string> authenticateHeaderItems = null;
112+
try
113+
{
114+
authenticateHeaderItems =
115+
EncodingHelper.ParseKeyValueListStrict(authenticateHeader, ',', false, true);
116+
}
117+
catch (ArgumentException)
118+
{
119+
LogError($"Malformed arguments in '{AuthenticateHeader}: {authenticateHeader}");
120+
return details;
121+
}
122+
123+
if (authenticateHeaderItems != null)
124+
{
125+
if (!authenticateHeaderItems.TryGetValue(AuthorityKey, out var auth))
126+
{
127+
LogError($"Response header from {endpoint} is missing expected key/value for {AuthorityKey}");
128+
return details;
129+
}
130+
details.Authority = new Uri(
131+
auth.Replace("oauth2/authorize", "") // swap out the old oAuth pattern.
132+
.Replace("common", "organizations")); // swap common for organizations because MSAL reasons.
133+
134+
if (!authenticateHeaderItems.TryGetValue(ResourceKey, out var res))
135+
{
136+
LogError($"Response header from {endpoint} is missing expected key/value for {ResourceKey}");
137+
return details;
138+
}
139+
details.Resource = new Uri(res);
140+
details.Success = true;
141+
}
142+
}
143+
144+
return details;
145+
}
146+
147+
private void LogError(string message)
148+
{
149+
if (_logger != null)
150+
{
151+
_logger(TraceEventType.Error, message);
152+
}
153+
}
154+
}
155+
}

src/GeneralTools/DataverseClient/Client/ConnectionService.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ internal bool EnableCookieRelay
591591
/// Cookies that are being passed though clients, when cookies are used
592592
/// </summary>
593593
internal Dictionary<string, string> CurrentCookieCollection { get; set; } = null;
594-
594+
595595
/// <summary>
596596
/// Server Hint for the number of concurrent threads that would provbide optimal processing.
597597
/// </summary>
@@ -1581,7 +1581,9 @@ internal async Task<OrganizationResponse> Command_WebAPIProcess_ExecuteAsync(Org
15811581
}
15821582
else if (req.Parameters.ContainsKey("Target") && req.Parameters["Target"] is EntityReference entRef) // this should cover things that have targets.
15831583
{
1584-
cReq = new Entity(entRef.LogicalName, entRef.Id);
1584+
cReq = entRef.KeyAttributes.Any()
1585+
? new Entity(entRef.LogicalName, entRef.KeyAttributes)
1586+
: new Entity(entRef.LogicalName, entRef.Id);
15851587
}
15861588

15871589
EntityMetadata entityMetadata = null;

src/GeneralTools/DataverseClient/ConnectControl/Microsoft.PowerPlatform.Dataverse.ConnectControl.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<ComponentAreaName>DataverseConnectControl</ComponentAreaName>
3+
<ComponentAreaName>DataverseClient</ComponentAreaName>
44
<OutputType>Library</OutputType>
55
<AppDesignerFolder>Properties</AppDesignerFolder>
66
<RootNamespace>Microsoft.PowerPlatform.Dataverse.ConnectControl</RootNamespace>
@@ -11,7 +11,7 @@
1111
<Import Project="..\..\..\Build.Common.StandardAndLegacy.props" />
1212
<PropertyGroup>
1313
<TargetFrameworks>$(DotNetClassicTargetFrameworks)</TargetFrameworks>
14-
<DocumentationFile>$(OutDir)\Microsoft.PowerPlatform.Dataverse.CrmConnectControl.xml</DocumentationFile>
14+
<DocumentationFile>$(OutDir)\Microsoft.PowerPlatform.Dataverse.ConnectControl.xml</DocumentationFile>
1515
</PropertyGroup>
1616
<ItemGroup>
1717
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="$(PackageVersion_Adal)" />

src/GeneralTools/DataverseClient/DataverseClient_PackageTestSolution.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.31729.503
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LivePackageTestsConsole", "UnitTests\LivePackageTestsConsole\LivePackageTestsConsole.csproj", "{AD21CFD4-EDA7-4F31-9A07-CC90C03B4C27}"
77
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LivePackageRunUnitTests", "UnitTests\LivePackageRunUnitTests\LivePackageRunUnitTests.csproj", "{F90838B9-F2D2-499B-8C37-4F8389380743}"
9+
EndProject
810
Global
911
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1012
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
1517
{AD21CFD4-EDA7-4F31-9A07-CC90C03B4C27}.Debug|Any CPU.Build.0 = Debug|Any CPU
1618
{AD21CFD4-EDA7-4F31-9A07-CC90C03B4C27}.Release|Any CPU.ActiveCfg = Release|Any CPU
1719
{AD21CFD4-EDA7-4F31-9A07-CC90C03B4C27}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{F90838B9-F2D2-499B-8C37-4F8389380743}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{F90838B9-F2D2-499B-8C37-4F8389380743}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{F90838B9-F2D2-499B-8C37-4F8389380743}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{F90838B9-F2D2-499B-8C37-4F8389380743}.Release|Any CPU.Build.0 = Release|Any CPU
1824
EndGlobalSection
1925
GlobalSection(SolutionProperties) = preSolution
2026
HideSolutionNode = FALSE

src/GeneralTools/DataverseClient/Testers/LoginControlTester/LoginControlTester.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<ComponentAreaName>DataverseConnectControl</ComponentAreaName>
3+
<ComponentAreaName>DataverseClient</ComponentAreaName>
44
<OutputType>WinExe</OutputType>
55
<AppDesignerFolder>Properties</AppDesignerFolder>
66
<RootNamespace>LoginControlTester</RootNamespace>

src/GeneralTools/DataverseClient/UIStyles/Microsoft.PowerPlatform.Dataverse.Ui.Styles.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<ComponentAreaName>DataverseConnectControl</ComponentAreaName>
3+
<ComponentAreaName>DataverseClient</ComponentAreaName>
44
<SignAssembly>true</SignAssembly>
55
<AppDesignerFolder>Properties</AppDesignerFolder>
66
<RootNamespace>Microsoft.PowerPlatform.Dataverse.Ui.Styles</RootNamespace>

0 commit comments

Comments
 (0)