Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Build.Common.core.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</PropertyGroup>

<PropertyGroup Condition="'$(ProjectSpecificFx)' == ''">
<TargetFrameworks>net462;net472;net48;netcoreapp3.0;netcoreapp3.1</TargetFrameworks>
<TargetFrameworks>net462;net472;net48;netcoreapp3.0;netcoreapp3.1;netstandard2.0</TargetFrameworks>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>

Expand Down
83 changes: 8 additions & 75 deletions src/GeneralTools/DataverseClient/Client/Auth/AuthProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Client;
using Microsoft.PowerPlatform.Dataverse.Client.Auth.TokenCache;
using Microsoft.PowerPlatform.Dataverse.Client.Utils;
Expand Down Expand Up @@ -90,11 +90,11 @@ internal async static Task<ExecuteAuthenticationResults> ExecuteAuthenticateServ
}
else
{
var rslt = GetAuthorityFromTargetServiceAsync(ClientServiceProviders.Instance.GetService<IHttpClientFactory>(), processResult.TargetServiceUrl, logSink).ConfigureAwait(false).GetAwaiter().GetResult();
if (!string.IsNullOrEmpty(rslt.Authority))
var details = GetAuthorityFromTargetServiceAsync(ClientServiceProviders.Instance.GetService<IHttpClientFactory>(), processResult.TargetServiceUrl, logSink).ConfigureAwait(false).GetAwaiter().GetResult();
if (details.Success)
{
Authority = rslt.Authority;
Resource = rslt.Resource;
Authority = details.Authority.AbsoluteUri;
Resource = details.Resource.AbsoluteUri;
}
else
throw new ArgumentNullException("Authority", "Need a non-empty authority");
Expand Down Expand Up @@ -446,85 +446,18 @@ internal static UriBuilder GetUriBuilderWithVersion(Uri discoveryServiceUri)
return versionTaggedUriBuilder;
}


private const string AuthenticateHeader = "WWW-Authenticate";
private const string Bearer = "bearer";
private const string AuthorityKey = "authorization_uri";
private const string ResourceKey = "resource_id";

internal class AuthRoutingProperties
{
public string Authority { get; set; }
public string Resource { get; set; }
}

/// <summary>
/// Get authority and resource for this instance.
/// </summary>
/// <param name="targetServiceUrl">URI to query</param>
/// <param name="logger">Logger to write info too</param>
/// <param name="clientFactory">HTTP Client factory to use for this request.</param>
/// <returns></returns>
private static async Task<AuthRoutingProperties> GetAuthorityFromTargetServiceAsync(IHttpClientFactory clientFactory, Uri targetServiceUrl, DataverseTraceLogger logger)
private static async Task<AuthenticationDetails> GetAuthorityFromTargetServiceAsync(IHttpClientFactory clientFactory, Uri targetServiceUrl, DataverseTraceLogger logger)
{
AuthRoutingProperties authRoutingProperties = new AuthRoutingProperties();
var client = clientFactory.CreateClient("DataverseHttpClientFactory");
var rslt = await client.GetAsync(targetServiceUrl).ConfigureAwait(false);

if (rslt.StatusCode == System.Net.HttpStatusCode.NotFound || rslt.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
// didn't find endpoint.
logger.Log($"Failed to get Authority and Resource error. Attempt to Access Endpoint {targetServiceUrl.ToString()} resulted in {rslt.StatusCode}.", TraceEventType.Error);
return authRoutingProperties;
}

if (rslt.Headers.Contains("WWW-Authenticate"))
{
var authenticateHeader = rslt.Headers.GetValues("WWW-Authenticate").FirstOrDefault();
authenticateHeader = authenticateHeader.Trim();

// This also checks for cases like "BearerXXXX authorization_uri=...." and "Bearer" and "Bearer "
if (!authenticateHeader.StartsWith(Bearer, StringComparison.OrdinalIgnoreCase)
|| authenticateHeader.Length < Bearer.Length + 2
|| !char.IsWhiteSpace(authenticateHeader[Bearer.Length]))
{
//var ex = new ArgumentException(AdalErrorMessage.InvalidAuthenticateHeaderFormat,
// nameof(authenticateHeader));
//CoreLoggerBase.Default.Error(AdalErrorMessage.InvalidAuthenticateHeaderFormat);
//CoreLoggerBase.Default.ErrorPii(ex);
//throw ex;
}

authenticateHeader = authenticateHeader.Substring(Bearer.Length).Trim();

IDictionary<string, string> authenticateHeaderItems = null;
try
{
authenticateHeaderItems =
EncodingHelper.ParseKeyValueListStrict(authenticateHeader, ',', false, true);
}
catch //(ArgumentException ex)
{
//var newEx = new ArgumentException(AdalErrorMessage.InvalidAuthenticateHeaderFormat,
// nameof(authenticateHeader), ex);
//CoreLoggerBase.Default.Error(AdalErrorMessage.InvalidAuthenticateHeaderFormat);
//CoreLoggerBase.Default.ErrorPii(newEx);
//throw newEx;
}

if (authenticateHeaderItems != null)
{
string param;
authenticateHeaderItems.TryGetValue(AuthorityKey, out param);
authRoutingProperties.Authority =
param.Replace("oauth2/authorize", "") // swap out the old oAuth pattern.
.Replace("common", "organizations"); // swap common for organizations because MSAL reasons.
authenticateHeaderItems.TryGetValue(ResourceKey, out param);
authRoutingProperties.Resource = param;
}
}

return authRoutingProperties;
var resolver = new AuthorityResolver(client, (t, msg) => logger.Log(msg, t));
return await resolver.ProbeForExpectedAuthentication(targetServiceUrl);
}

/// <summary>
Expand Down
155 changes: 155 additions & 0 deletions src/GeneralTools/DataverseClient/Client/Auth/AuthorityResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace Microsoft.PowerPlatform.Dataverse.Client.Auth
{
/// <summary>
/// Details of expected authentication.
/// </summary>
public sealed class AuthenticationDetails
{
/// <summary>
/// True if probing returned a WWW-Authenticate header.
/// </summary>
public bool Success { get; internal set; }

/// <summary>
/// Authority to initiate OAuth flow with.
/// </summary>
// 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
public Uri Authority { get; internal set; }

/// <summary>
/// OAuth resource to request authentication for.
/// </summary>
public Uri Resource { get; internal set; }
}

/// <summary>
/// Probes API endpoint to elicit a 401 response with the WWW-Authenticate header and processes the found information
/// </summary>
public sealed class AuthorityResolver
{
private const string AuthenticateHeader = "WWW-Authenticate";
private const string Bearer = "bearer";
private const string AuthorityKey = "authorization_uri";
private const string ResourceKey = "resource_id";

private readonly HttpClient _httpClient;
private readonly Action<TraceEventType, string> _logger;

/// <summary>
/// instantiate resolver, using specified HttpClient to be used.
/// </summary>
/// <param name="httpClient"></param>
/// <param name="logger"></param>
public AuthorityResolver(HttpClient httpClient, Action<TraceEventType, string> logger = null)
{
_ = httpClient ?? throw new ArgumentNullException(nameof(httpClient));

_httpClient = httpClient;
_logger = logger;
}

/// <summary>
/// Attemtps to solicit a WWW-Authenticate reply using an unauthenticated GET call to the given endpoint.
/// Parses returned header for details
/// </summary>
/// <param name="endpoint"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public async Task<AuthenticationDetails> ProbeForExpectedAuthentication(Uri endpoint)
{
_ = endpoint ?? throw new ArgumentNullException(nameof(endpoint));
var details = new AuthenticationDetails();

HttpResponseMessage response;
try
{
response = await _httpClient.GetAsync(endpoint).ConfigureAwait(false);
}
catch (HttpRequestException ex)
{
var errDetails = string.Empty;
if (ex.InnerException is WebException wex)
{
errDetails = $"; details: {wex.Message} ({wex.Status})";
}
LogError($"Failed to get response from: {endpoint}; error: {ex.Message}{errDetails}");
return details;
}


if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.BadRequest)
{
// didn't find endpoint.
LogError($"Failed to get Authority and Resource error. Attempt to Access Endpoint {endpoint} resulted in {response.StatusCode}.");
return details;
}

if (response.Headers.Contains(AuthenticateHeader))
{
var authenticateHeader = response.Headers.GetValues(AuthenticateHeader).FirstOrDefault();
authenticateHeader = authenticateHeader.Trim();

// This also checks for cases like "BearerXXXX authorization_uri=...." and "Bearer" and "Bearer "
if (!authenticateHeader.StartsWith(Bearer, StringComparison.OrdinalIgnoreCase)
|| authenticateHeader.Length < Bearer.Length + 2
|| !char.IsWhiteSpace(authenticateHeader[Bearer.Length]))
{
LogError($"Malformed 'Bearer' format: {authenticateHeader}");
return details;
}

authenticateHeader = authenticateHeader.Substring(Bearer.Length).Trim();

IDictionary<string, string> authenticateHeaderItems = null;
try
{
authenticateHeaderItems =
EncodingHelper.ParseKeyValueListStrict(authenticateHeader, ',', false, true);
}
catch (ArgumentException)
{
LogError($"Malformed arguments in '{AuthenticateHeader}: {authenticateHeader}");
return details;
}

if (authenticateHeaderItems != null)
{
if (!authenticateHeaderItems.TryGetValue(AuthorityKey, out var auth))
{
LogError($"Response header from {endpoint} is missing expected key/value for {AuthorityKey}");
return details;
}
details.Authority = new Uri(
auth.Replace("oauth2/authorize", "") // swap out the old oAuth pattern.
.Replace("common", "organizations")); // swap common for organizations because MSAL reasons.

if (!authenticateHeaderItems.TryGetValue(ResourceKey, out var res))
{
LogError($"Response header from {endpoint} is missing expected key/value for {ResourceKey}");
return details;
}
details.Resource = new Uri(res);
details.Success = true;
}
}

return details;
}

private void LogError(string message)
{
if (_logger != null)
{
_logger(TraceEventType.Error, message);
}
}
}
}
6 changes: 4 additions & 2 deletions src/GeneralTools/DataverseClient/Client/ConnectionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ internal bool EnableCookieRelay
/// Cookies that are being passed though clients, when cookies are used
/// </summary>
internal Dictionary<string, string> CurrentCookieCollection { get; set; } = null;

/// <summary>
/// Server Hint for the number of concurrent threads that would provbide optimal processing.
/// </summary>
Expand Down Expand Up @@ -1581,7 +1581,9 @@ internal async Task<OrganizationResponse> Command_WebAPIProcess_ExecuteAsync(Org
}
else if (req.Parameters.ContainsKey("Target") && req.Parameters["Target"] is EntityReference entRef) // this should cover things that have targets.
{
cReq = new Entity(entRef.LogicalName, entRef.Id);
cReq = entRef.KeyAttributes.Any()
? new Entity(entRef.LogicalName, entRef.KeyAttributes)
: new Entity(entRef.LogicalName, entRef.Id);
}

EntityMetadata entityMetadata = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ComponentAreaName>DataverseConnectControl</ComponentAreaName>
<ComponentAreaName>DataverseClient</ComponentAreaName>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Microsoft.PowerPlatform.Dataverse.ConnectControl</RootNamespace>
Expand All @@ -11,7 +11,7 @@
<Import Project="..\..\..\Build.Common.StandardAndLegacy.props" />
<PropertyGroup>
<TargetFrameworks>$(DotNetClassicTargetFrameworks)</TargetFrameworks>
<DocumentationFile>$(OutDir)\Microsoft.PowerPlatform.Dataverse.CrmConnectControl.xml</DocumentationFile>
<DocumentationFile>$(OutDir)\Microsoft.PowerPlatform.Dataverse.ConnectControl.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="$(PackageVersion_Adal)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.31729.503
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LivePackageTestsConsole", "UnitTests\LivePackageTestsConsole\LivePackageTestsConsole.csproj", "{AD21CFD4-EDA7-4F31-9A07-CC90C03B4C27}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LivePackageRunUnitTests", "UnitTests\LivePackageRunUnitTests\LivePackageRunUnitTests.csproj", "{F90838B9-F2D2-499B-8C37-4F8389380743}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +17,10 @@ Global
{AD21CFD4-EDA7-4F31-9A07-CC90C03B4C27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD21CFD4-EDA7-4F31-9A07-CC90C03B4C27}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD21CFD4-EDA7-4F31-9A07-CC90C03B4C27}.Release|Any CPU.Build.0 = Release|Any CPU
{F90838B9-F2D2-499B-8C37-4F8389380743}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F90838B9-F2D2-499B-8C37-4F8389380743}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F90838B9-F2D2-499B-8C37-4F8389380743}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F90838B9-F2D2-499B-8C37-4F8389380743}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ComponentAreaName>DataverseConnectControl</ComponentAreaName>
<ComponentAreaName>DataverseClient</ComponentAreaName>
<OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>LoginControlTester</RootNamespace>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ComponentAreaName>DataverseConnectControl</ComponentAreaName>
<ComponentAreaName>DataverseClient</ComponentAreaName>
<SignAssembly>true</SignAssembly>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Microsoft.PowerPlatform.Dataverse.Ui.Styles</RootNamespace>
Expand Down
Loading