[MQTT] Added authentication HTTP endpoint for external processes (#2881)

MQTT Broker is running as an external process to EdgeHub, but relies on it to authenticate connecting clients. An HTTP endpoint is added to EdgeHub, which can receive different type of credentials (SAS token, certificates) and responses with yes/no, reflecting the authentication result.
The endpoint is designed for internal (in-container) use.

The main file is AuthAgentListener.cs, which handles the HTTP calls and dispatches the queries to the already implemented authentication machinery.
This commit is contained in:
vipeller 2020-05-20 09:38:46 -07:00 коммит произвёл GitHub
Родитель bb239af6c8
Коммит adabc5ae37
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
29 изменённых файлов: 1496 добавлений и 219 удалений

Просмотреть файл

@ -219,7 +219,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MetricsValidator", "test\mo
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeHubRestartTester", "test\modules\EdgeHubRestartTester\EdgeHubRestartTester.csproj", "{DA073067-83EF-4F4C-8E58-51CA7870C1F6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudToDeviceMessageTester", "test\modules\CloudToDeviceMessageTester\CloudToDeviceMessageTester.csproj", "{446F50F3-3E93-454B-8EBF-4830E92DFE5D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudToDeviceMessageTester", "test\modules\CloudToDeviceMessageTester\CloudToDeviceMessageTester.csproj", "{446F50F3-3E93-454B-8EBF-4830E92DFE5D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter", "edge-hub\src\Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter\Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter.csproj", "{F040EA42-20FB-453E-8E09-9A1E04E80039}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter.Test", "edge-hub\test\Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter.Test\Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter.Test.csproj", "{E5E341ED-571D-473F-B802-0CA885723C67}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -590,18 +594,10 @@ Global
{1C1C8203-CD73-4CC1-9112-7315F9DE1AB6}.Release|Any CPU.Build.0 = Release|Any CPU
{B3C68519-B309-42FF-B4DF-9E77B2D4BD50}.CheckInBuild|Any CPU.ActiveCfg = CheckInBuild|Any CPU
{B3C68519-B309-42FF-B4DF-9E77B2D4BD50}.CheckInBuild|Any CPU.Build.0 = CheckInBuild|Any CPU
{B3C68519-B309-42FF-B4DF-9E77B2D4BD50}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
{B3C68519-B309-42FF-B4DF-9E77B2D4BD50}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
{B3C68519-B309-42FF-B4DF-9E77B2D4BD50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B3C68519-B309-42FF-B4DF-9E77B2D4BD50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B3C68519-B309-42FF-B4DF-9E77B2D4BD50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B3C68519-B309-42FF-B4DF-9E77B2D4BD50}.Release|Any CPU.Build.0 = Release|Any CPU
{446F50F3-3E93-454B-8EBF-4830E92DFE5D}.CheckInBuild|Any CPU.ActiveCfg = Debug|Any CPU
{446F50F3-3E93-454B-8EBF-4830E92DFE5D}.CheckInBuild|Any CPU.Build.0 = Debug|Any CPU
{446F50F3-3E93-454B-8EBF-4830E92DFE5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{446F50F3-3E93-454B-8EBF-4830E92DFE5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{446F50F3-3E93-454B-8EBF-4830E92DFE5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{446F50F3-3E93-454B-8EBF-4830E92DFE5D}.Release|Any CPU.Build.0 = Release|Any CPU
{5857DB4A-F61A-4467-96F1-003B47B8B2CF}.CheckInBuild|Any CPU.ActiveCfg = CheckInBuild|Any CPU
{5857DB4A-F61A-4467-96F1-003B47B8B2CF}.CheckInBuild|Any CPU.Build.0 = CheckInBuild|Any CPU
{5857DB4A-F61A-4467-96F1-003B47B8B2CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@ -610,12 +606,28 @@ Global
{5857DB4A-F61A-4467-96F1-003B47B8B2CF}.Release|Any CPU.Build.0 = Release|Any CPU
{DA073067-83EF-4F4C-8E58-51CA7870C1F6}.CheckInBuild|Any CPU.ActiveCfg = CheckInBuild|Any CPU
{DA073067-83EF-4F4C-8E58-51CA7870C1F6}.CheckInBuild|Any CPU.Build.0 = CheckInBuild|Any CPU
{DA073067-83EF-4F4C-8E58-51CA7870C1F6}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
{DA073067-83EF-4F4C-8E58-51CA7870C1F6}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
{DA073067-83EF-4F4C-8E58-51CA7870C1F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DA073067-83EF-4F4C-8E58-51CA7870C1F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA073067-83EF-4F4C-8E58-51CA7870C1F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA073067-83EF-4F4C-8E58-51CA7870C1F6}.Release|Any CPU.Build.0 = Release|Any CPU
{446F50F3-3E93-454B-8EBF-4830E92DFE5D}.CheckInBuild|Any CPU.ActiveCfg = Debug|Any CPU
{446F50F3-3E93-454B-8EBF-4830E92DFE5D}.CheckInBuild|Any CPU.Build.0 = Debug|Any CPU
{446F50F3-3E93-454B-8EBF-4830E92DFE5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{446F50F3-3E93-454B-8EBF-4830E92DFE5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{446F50F3-3E93-454B-8EBF-4830E92DFE5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{446F50F3-3E93-454B-8EBF-4830E92DFE5D}.Release|Any CPU.Build.0 = Release|Any CPU
{F040EA42-20FB-453E-8E09-9A1E04E80039}.CheckInBuild|Any CPU.ActiveCfg = CheckInBuild|Any CPU
{F040EA42-20FB-453E-8E09-9A1E04E80039}.CheckInBuild|Any CPU.Build.0 = CheckInBuild|Any CPU
{F040EA42-20FB-453E-8E09-9A1E04E80039}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F040EA42-20FB-453E-8E09-9A1E04E80039}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F040EA42-20FB-453E-8E09-9A1E04E80039}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F040EA42-20FB-453E-8E09-9A1E04E80039}.Release|Any CPU.Build.0 = Release|Any CPU
{E5E341ED-571D-473F-B802-0CA885723C67}.CheckInBuild|Any CPU.ActiveCfg = Debug|Any CPU
{E5E341ED-571D-473F-B802-0CA885723C67}.CheckInBuild|Any CPU.Build.0 = Debug|Any CPU
{E5E341ED-571D-473F-B802-0CA885723C67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5E341ED-571D-473F-B802-0CA885723C67}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5E341ED-571D-473F-B802-0CA885723C67}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5E341ED-571D-473F-B802-0CA885723C67}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -704,6 +716,8 @@ Global
{5857DB4A-F61A-4467-96F1-003B47B8B2CF} = {F921339B-32F9-4BF3-B364-2DB01FA2F1A1}
{DA073067-83EF-4F4C-8E58-51CA7870C1F6} = {F921339B-32F9-4BF3-B364-2DB01FA2F1A1}
{446F50F3-3E93-454B-8EBF-4830E92DFE5D} = {F921339B-32F9-4BF3-B364-2DB01FA2F1A1}
{F040EA42-20FB-453E-8E09-9A1E04E80039} = {AB4285D8-CF1D-4B20-95F6-CB80892C8321}
{E5E341ED-571D-473F-B802-0CA885723C67} = {63969606-14B2-4D9D-AB72-A5D60D22037C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D71830F5-3AF5-46B4-8A9E-1DCE4F2253AC}

Просмотреть файл

@ -21,15 +21,9 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Core
this.credentialsCache = Preconditions.CheckNotNull(credentialsCache, nameof(ICredentialsCache));
}
/// <summary>
/// Authenticates the client credentials
/// </summary>
public Task<bool> AuthenticateAsync(IClientCredentials clientCredentials)
=> this.AuthenticateAsync(clientCredentials, false);
/// <summary>
/// Reauthenticates the client credentials
/// </summary>
public Task<bool> ReauthenticateAsync(IClientCredentials clientCredentials)
=> this.AuthenticateAsync(clientCredentials, true);

Просмотреть файл

@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
{
public class ClientInfo
{
public ClientInfo(string deviceId, string moduleId, string deviceClientType)
{
this.DeviceId = deviceId;
this.ModuleId = moduleId;
this.DeviceClientType = deviceClientType;
}
public string DeviceId { get; }
public string ModuleId { get; }
public string DeviceClientType { get; }
}
}

Просмотреть файл

@ -3,7 +3,6 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
@ -16,9 +15,8 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
public class DeviceIdentityProvider : IDeviceIdentityProvider
{
const string ApiVersionKey = "api-version";
const string DeviceClientTypeKey = "DeviceClientType";
readonly IAuthenticator authenticator;
readonly IUsernameParser usernameParser;
readonly IClientCredentialsFactory clientCredentialsFactory;
readonly bool clientCertAuthAllowed;
readonly IProductInfoStore productInfoStore;
@ -27,11 +25,13 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
public DeviceIdentityProvider(
IAuthenticator authenticator,
IUsernameParser usernameParser,
IClientCredentialsFactory clientCredentialsFactory,
IProductInfoStore productInfoStore,
bool clientCertAuthAllowed)
{
this.authenticator = Preconditions.CheckNotNull(authenticator, nameof(authenticator));
this.usernameParser = Preconditions.CheckNotNull(usernameParser, nameof(usernameParser));
this.clientCredentialsFactory = Preconditions.CheckNotNull(clientCredentialsFactory, nameof(clientCredentialsFactory));
this.productInfoStore = Preconditions.CheckNotNull(productInfoStore, nameof(productInfoStore));
this.clientCertAuthAllowed = clientCertAuthAllowed;
@ -46,18 +46,18 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
Preconditions.CheckNonWhiteSpace(username, nameof(username));
Preconditions.CheckNonWhiteSpace(clientId, nameof(clientId));
(string deviceId, string moduleId, string deviceClientType) = ParseUserName(username);
ClientInfo clientInfo = this.usernameParser.Parse(username);
IClientCredentials deviceCredentials = null;
if (!string.IsNullOrEmpty(password))
{
deviceCredentials = this.clientCredentialsFactory.GetWithSasToken(deviceId, moduleId, deviceClientType, password, false);
deviceCredentials = this.clientCredentialsFactory.GetWithSasToken(clientInfo.DeviceId, clientInfo.ModuleId, clientInfo.DeviceClientType, password, false);
}
else if (this.remoteCertificate.HasValue)
{
if (!this.clientCertAuthAllowed)
{
Events.CertAuthNotEnabled(deviceId, moduleId);
Events.CertAuthNotEnabled(clientInfo.DeviceId, clientInfo.ModuleId);
return UnauthenticatedDeviceIdentity.Instance;
}
@ -65,16 +65,16 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
cert =>
{
deviceCredentials = this.clientCredentialsFactory.GetWithX509Cert(
deviceId,
moduleId,
deviceClientType,
clientInfo.DeviceId,
clientInfo.ModuleId,
clientInfo.DeviceClientType,
cert,
this.remoteCertificateChain);
});
}
else
{
Events.AuthNotFound(deviceId, moduleId);
Events.AuthNotFound(clientInfo.DeviceId, clientInfo.ModuleId);
return UnauthenticatedDeviceIdentity.Instance;
}
@ -86,7 +86,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
return UnauthenticatedDeviceIdentity.Instance;
}
await this.productInfoStore.SetProductInfo(deviceCredentials.Identity.Id, deviceClientType);
await this.productInfoStore.SetProductInfo(deviceCredentials.Identity.Id, clientInfo.DeviceClientType);
Events.Success(clientId, username);
return new ProtocolGatewayIdentity(deviceCredentials);
}
@ -103,120 +103,6 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
this.remoteCertificateChain = Preconditions.CheckNotNull(chain, nameof(chain));
}
internal static (string deviceId, string moduleId, string deviceClientType) ParseUserName(string username)
{
// Username is of one of the 2 forms:
// username = edgeHubHostName "/" deviceId [ "/" moduleId ] "/?" properties
// Note, the ? should be the first character of the last segment (as it is a valid character for a deviceId/moduleId)
// OR
// username = edgeHubHostName "/" deviceId [ "/" moduleId ] "/" properties
// properties = property *("&" property)
// property = name "=" value
// We recognize two property names:
// "api-version" [mandatory]
// "DeviceClientType" [optional]
// We ignore any properties we don't recognize.
// Note - this logic does not check the query parameters for special characters, and '?' is treated as a valid value
// and not used as a separator, unless it is the first character of the last segment
// (since the property bag is not url encoded). So the following are valid username inputs -
// "iotHub1/device1/module1/foo?bar=b1&api-version=2010-01-01&DeviceClientType=customDeviceClient1"
// "iotHub1/device1?&api-version=2010-01-01&DeviceClientType=customDeviceClient1"
// "iotHub1/device1/module1?&api-version=2010-01-01&DeviceClientType=customDeviceClient1"
string deviceId;
string moduleId = string.Empty;
IDictionary<string, string> queryParameters;
string[] usernameSegments = Preconditions.CheckNonWhiteSpace(username, nameof(username)).Split('/');
if (usernameSegments[usernameSegments.Length - 1].StartsWith("?", StringComparison.OrdinalIgnoreCase))
{
// edgeHubHostName/device1/?apiVersion=10-2-3&DeviceClientType=foo
if (usernameSegments.Length == 3)
{
deviceId = usernameSegments[1].Trim();
queryParameters = ParseDeviceClientType(usernameSegments[2].Substring(1).Trim());
}
else if (usernameSegments.Length == 4)
{
// edgeHubHostName/device1/module1/?apiVersion=10-2-3&DeviceClientType=foo
deviceId = usernameSegments[1].Trim();
moduleId = usernameSegments[2].Trim();
queryParameters = ParseDeviceClientType(usernameSegments[3].Substring(1).Trim());
}
else
{
throw new EdgeHubConnectionException($"Username {username} does not contain valid values");
}
}
else
{
// edgeHubHostName/device1/apiVersion=10-2-3&DeviceClientType=foo
if (usernameSegments.Length == 3 && usernameSegments[2].Contains("api-version="))
{
deviceId = usernameSegments[1].Trim();
queryParameters = ParseDeviceClientType(usernameSegments[2].Trim());
}
else if (usernameSegments.Length == 4 && usernameSegments[3].Contains("api-version="))
{
// edgeHubHostName/device1/module1/apiVersion=10-2-3&DeviceClientType=foo
deviceId = usernameSegments[1].Trim();
moduleId = usernameSegments[2].Trim();
queryParameters = ParseDeviceClientType(usernameSegments[3].Trim());
}
else if (usernameSegments.Length == 6 && username.EndsWith("/api-version=2017-06-30/DeviceClientType=Microsoft.Azure.Devices.Client/1.5.1-preview-003", StringComparison.OrdinalIgnoreCase))
{
// The Azure ML container is using an older client that returns a device client with the following format -
// username = edgeHubHostName/deviceId/moduleId/api-version=2017-06-30/DeviceClientType=Microsoft.Azure.Devices.Client/1.5.1-preview-003
// Notice how the DeviceClientType parameter is separated by a '/' instead of a '&', giving a usernameSegments.Length of 6 instead of the expected 4
// To allow those clients to work, check for that specific api-version, and version.
deviceId = usernameSegments[1].Trim();
moduleId = usernameSegments[2].Trim();
queryParameters = new Dictionary<string, string>
{
[ApiVersionKey] = "2017-06-30",
[DeviceClientTypeKey] = "Microsoft.Azure.Devices.Client/1.5.1-preview-003"
};
}
else
{
throw new EdgeHubConnectionException($"Username {username} does not contain valid values");
}
}
// Check if the api-version parameter exists, but don't check its value.
if (!queryParameters.TryGetValue(ApiVersionKey, out string apiVersionKey) || string.IsNullOrWhiteSpace(apiVersionKey))
{
throw new EdgeHubConnectionException($"Username {username} does not contain a valid Api-version property");
}
if (string.IsNullOrWhiteSpace(deviceId))
{
throw new EdgeHubConnectionException($"Username {username} does not contain a valid device ID");
}
if (!queryParameters.TryGetValue(DeviceClientTypeKey, out string deviceClientType))
{
deviceClientType = string.Empty;
}
return (deviceId, moduleId, deviceClientType);
}
static IDictionary<string, string> ParseDeviceClientType(string queryParameterString)
{
// example input: "api-version=version&DeviceClientType=url-escaped-string&other-prop=value&some-other-prop"
var kvsep = new[] { '=' };
Dictionary<string, string> queryParameters = queryParameterString
.Split('&') // split input string into params
.Select(s => s.Split(kvsep, 2)) // split each param into a key/value pair
.GroupBy(s => s[0]) // group duplicates (by key) together...
.Select(s => s.First()) // ...and keep only the first one
.ToDictionary( // convert to Dictionary<string, string>
s => s[0],
s => Uri.UnescapeDataString(s.ElementAtOrEmpty(1)));
return queryParameters;
}
static class Events
{
const int IdStart = MqttEventIds.SasTokenDeviceIdentityProvider;

Просмотреть файл

@ -0,0 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
{
public interface IUsernameParser
{
ClientInfo Parse(string username);
}
}

Просмотреть файл

@ -40,6 +40,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
readonly ISessionStatePersistenceProvider sessionProvider;
readonly IMqttConnectionProvider mqttConnectionProvider;
readonly IAuthenticator authenticator;
readonly IUsernameParser usernameParser;
readonly IClientCredentialsFactory clientCredentialsFactory;
readonly IWebSocketListenerRegistry webSocketListenerRegistry;
readonly IByteBufferAllocator byteBufferAllocator;
@ -55,6 +56,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
X509Certificate tlsCertificate,
IMqttConnectionProvider mqttConnectionProvider,
IAuthenticator authenticator,
IUsernameParser usernameParser,
IClientCredentialsFactory clientCredentialsFactory,
ISessionStatePersistenceProvider sessionProvider,
IWebSocketListenerRegistry webSocketListenerRegistry,
@ -67,6 +69,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
this.tlsCertificate = Preconditions.CheckNotNull(tlsCertificate, nameof(tlsCertificate));
this.mqttConnectionProvider = Preconditions.CheckNotNull(mqttConnectionProvider, nameof(mqttConnectionProvider));
this.authenticator = Preconditions.CheckNotNull(authenticator, nameof(authenticator));
this.usernameParser = Preconditions.CheckNotNull(usernameParser, nameof(usernameParser));
this.clientCredentialsFactory = Preconditions.CheckNotNull(clientCredentialsFactory, nameof(clientCredentialsFactory));
this.sessionProvider = Preconditions.CheckNotNull(sessionProvider, nameof(sessionProvider));
this.webSocketListenerRegistry = Preconditions.CheckNotNull(webSocketListenerRegistry, nameof(webSocketListenerRegistry));
@ -153,7 +156,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
new ActionChannelInitializer<ISocketChannel>(
channel =>
{
var identityProvider = new DeviceIdentityProvider(this.authenticator, this.clientCredentialsFactory, this.productInfoStore, this.clientCertAuthAllowed);
var identityProvider = new DeviceIdentityProvider(this.authenticator, this.usernameParser, this.clientCredentialsFactory, this.productInfoStore, this.clientCertAuthAllowed);
// configure the channel pipeline of the new Channel by adding handlers
TlsSettings serverSettings = new ServerTlsSettings(
certificate: this.tlsCertificate,
@ -185,6 +188,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
settings,
bridgeFactory,
this.authenticator,
this.usernameParser,
this.clientCredentialsFactory,
() => this.sessionProvider,
new MultithreadEventLoopGroup(Environment.ProcessorCount),

Просмотреть файл

@ -0,0 +1,129 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
{
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Azure.Devices.Edge.Hub.Core;
using Microsoft.Azure.Devices.Edge.Util;
public class MqttUsernameParser : IUsernameParser
{
const string ApiVersionKey = "api-version";
const string DeviceClientTypeKey = "DeviceClientType";
public ClientInfo Parse(string username)
{
// Username is of one of the 2 forms:
// username = edgeHubHostName "/" deviceId [ "/" moduleId ] "/?" properties
// Note, the ? should be the first character of the last segment (as it is a valid character for a deviceId/moduleId)
// OR
// username = edgeHubHostName "/" deviceId [ "/" moduleId ] "/" properties
// properties = property *("&" property)
// property = name "=" value
// We recognize two property names:
// "api-version" [mandatory]
// "DeviceClientType" [optional]
// We ignore any properties we don't recognize.
// Note - this logic does not check the query parameters for special characters, and '?' is treated as a valid value
// and not used as a separator, unless it is the first character of the last segment
// (since the property bag is not url encoded). So the following are valid username inputs -
// "iotHub1/device1/module1/foo?bar=b1&api-version=2010-01-01&DeviceClientType=customDeviceClient1"
// "iotHub1/device1?&api-version=2010-01-01&DeviceClientType=customDeviceClient1"
// "iotHub1/device1/module1?&api-version=2010-01-01&DeviceClientType=customDeviceClient1"
string deviceId;
string moduleId = string.Empty;
IDictionary<string, string> queryParameters;
string[] usernameSegments = Preconditions.CheckNonWhiteSpace(username, nameof(username)).Split('/');
if (usernameSegments[usernameSegments.Length - 1].StartsWith("?", StringComparison.OrdinalIgnoreCase))
{
// edgeHubHostName/device1/?apiVersion=10-2-3&DeviceClientType=foo
if (usernameSegments.Length == 3)
{
deviceId = usernameSegments[1].Trim();
queryParameters = ParseDeviceClientType(usernameSegments[2].Substring(1).Trim());
}
else if (usernameSegments.Length == 4)
{
// edgeHubHostName/device1/module1/?apiVersion=10-2-3&DeviceClientType=foo
deviceId = usernameSegments[1].Trim();
moduleId = usernameSegments[2].Trim();
queryParameters = ParseDeviceClientType(usernameSegments[3].Substring(1).Trim());
}
else
{
throw new EdgeHubConnectionException($"Username {username} does not contain valid values");
}
}
else
{
// edgeHubHostName/device1/apiVersion=10-2-3&DeviceClientType=foo
if (usernameSegments.Length == 3 && usernameSegments[2].Contains("api-version="))
{
deviceId = usernameSegments[1].Trim();
queryParameters = ParseDeviceClientType(usernameSegments[2].Trim());
}
else if (usernameSegments.Length == 4 && usernameSegments[3].Contains("api-version="))
{
// edgeHubHostName/device1/module1/apiVersion=10-2-3&DeviceClientType=foo
deviceId = usernameSegments[1].Trim();
moduleId = usernameSegments[2].Trim();
queryParameters = ParseDeviceClientType(usernameSegments[3].Trim());
}
else if (usernameSegments.Length == 6 && username.EndsWith("/api-version=2017-06-30/DeviceClientType=Microsoft.Azure.Devices.Client/1.5.1-preview-003", StringComparison.OrdinalIgnoreCase))
{
// The Azure ML container is using an older client that returns a device client with the following format -
// username = edgeHubHostName/deviceId/moduleId/api-version=2017-06-30/DeviceClientType=Microsoft.Azure.Devices.Client/1.5.1-preview-003
// Notice how the DeviceClientType parameter is separated by a '/' instead of a '&', giving a usernameSegments.Length of 6 instead of the expected 4
// To allow those clients to work, check for that specific api-version, and version.
deviceId = usernameSegments[1].Trim();
moduleId = usernameSegments[2].Trim();
queryParameters = new Dictionary<string, string>
{
[ApiVersionKey] = "2017-06-30",
[DeviceClientTypeKey] = "Microsoft.Azure.Devices.Client/1.5.1-preview-003"
};
}
else
{
throw new EdgeHubConnectionException($"Username {username} does not contain valid values");
}
}
// Check if the api-version parameter exists, but don't check its value.
if (!queryParameters.TryGetValue(ApiVersionKey, out string apiVersionKey) || string.IsNullOrWhiteSpace(apiVersionKey))
{
throw new EdgeHubConnectionException($"Username {username} does not contain a valid Api-version property");
}
if (string.IsNullOrWhiteSpace(deviceId))
{
throw new EdgeHubConnectionException($"Username {username} does not contain a valid device ID");
}
if (!queryParameters.TryGetValue(DeviceClientTypeKey, out string deviceClientType))
{
deviceClientType = string.Empty;
}
return new ClientInfo(deviceId, moduleId, deviceClientType);
}
static IDictionary<string, string> ParseDeviceClientType(string queryParameterString)
{
// example input: "api-version=version&DeviceClientType=url-escaped-string&other-prop=value&some-other-prop"
var kvsep = new[] { '=' };
Dictionary<string, string> queryParameters = queryParameterString
.Split('&') // split input string into params
.Select(s => s.Split(kvsep, 2)) // split each param into a key/value pair
.GroupBy(s => s[0]) // group duplicates (by key) together...
.Select(s => s.First()) // ...and keep only the first one
.ToDictionary( // convert to Dictionary<string, string>
s => s[0],
s => Uri.UnescapeDataString(s.ElementAtOrEmpty(1)));
return queryParameters;
}
}
}

Просмотреть файл

@ -23,6 +23,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
readonly Settings settings;
readonly MessagingBridgeFactoryFunc messagingBridgeFactoryFunc;
readonly IAuthenticator authenticator;
readonly IUsernameParser usernameParser;
readonly IClientCredentialsFactory clientCredentialsFactory;
readonly Func<ISessionStatePersistenceProvider> sessionProviderFactory;
readonly IEventLoopGroup workerGroup;
@ -36,6 +37,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
Settings settings,
MessagingBridgeFactoryFunc messagingBridgeFactoryFunc,
IAuthenticator authenticator,
IUsernameParser usernameParser,
IClientCredentialsFactory clientCredentialsFactory,
Func<ISessionStatePersistenceProvider> sessionProviderFactory,
IEventLoopGroup workerGroup,
@ -48,6 +50,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
this.settings = Preconditions.CheckNotNull(settings, nameof(settings));
this.messagingBridgeFactoryFunc = Preconditions.CheckNotNull(messagingBridgeFactoryFunc, nameof(messagingBridgeFactoryFunc));
this.authenticator = Preconditions.CheckNotNull(authenticator, nameof(authenticator));
this.usernameParser = Preconditions.CheckNotNull(usernameParser, nameof(usernameParser));
this.clientCredentialsFactory = Preconditions.CheckNotNull(clientCredentialsFactory, nameof(clientCredentialsFactory));
this.sessionProviderFactory = Preconditions.CheckNotNull(sessionProviderFactory, nameof(sessionProviderFactory));
this.workerGroup = Preconditions.CheckNotNull(workerGroup, nameof(workerGroup));
@ -62,7 +65,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
public Task ProcessWebSocketRequestAsync(WebSocket webSocket, Option<EndPoint> localEndPoint, EndPoint remoteEndPoint, string correlationId)
{
var identityProvider = new DeviceIdentityProvider(this.authenticator, this.clientCredentialsFactory, this.productInfoStore, this.clientCertAuthAllowed);
var identityProvider = new DeviceIdentityProvider(this.authenticator, this.usernameParser, this.clientCredentialsFactory, this.productInfoStore, this.clientCertAuthAllowed);
return this.ProcessWebSocketRequestAsyncInternal(identityProvider, webSocket, localEndPoint, remoteEndPoint, correlationId);
}
@ -74,7 +77,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt
X509Certificate2 clientCert,
IList<X509Certificate2> clientCertChain)
{
var identityProvider = new DeviceIdentityProvider(this.authenticator, this.clientCredentialsFactory, this.productInfoStore, this.clientCertAuthAllowed);
var identityProvider = new DeviceIdentityProvider(this.authenticator, this.usernameParser, this.clientCredentialsFactory, this.productInfoStore, this.clientCertAuthAllowed);
identityProvider.RegisterConnectionCertificate(clientCert, clientCertChain);
return this.ProcessWebSocketRequestAsyncInternal(identityProvider, webSocket, localEndPoint, remoteEndPoint, correlationId);
}

Просмотреть файл

@ -0,0 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter
{
class AuthAgentConstants
{
public const int Authenticated = 200;
public const int Unauthenticated = 403;
public const string ApiVersion = "2020-04-20";
}
}

Просмотреть файл

@ -0,0 +1,228 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Devices.Edge.Hub.Core;
using Microsoft.Azure.Devices.Edge.Hub.Core.Identity;
using Microsoft.Azure.Devices.Edge.Hub.Mqtt;
using Microsoft.Azure.Devices.Edge.Util;
using Microsoft.Extensions.Logging;
public class AuthAgentController : Controller
{
readonly IAuthenticator authenticator;
readonly IUsernameParser usernameParser;
readonly IClientCredentialsFactory clientCredentialsFactory;
readonly AuthAgentProtocolHeadConfig config;
public AuthAgentController(IAuthenticator authenticator, IUsernameParser usernameParser, IClientCredentialsFactory clientCredentialsFactory, AuthAgentProtocolHeadConfig config)
{
this.authenticator = Preconditions.CheckNotNull(authenticator, nameof(authenticator));
this.usernameParser = Preconditions.CheckNotNull(usernameParser, nameof(usernameParser));
this.clientCredentialsFactory = Preconditions.CheckNotNull(clientCredentialsFactory, nameof(clientCredentialsFactory));
this.config = Preconditions.CheckNotNull(config, nameof(config));
}
[HttpPost]
[Produces("application/json")]
public async Task<JsonResult> HandleAsync([FromBody] AuthRequest request)
{
if (request == null)
{
Events.ErrorDecodingPayload();
return this.Json(GetErrorResult());
}
if (!string.Equals(request.Version, AuthAgentConstants.ApiVersion))
{
Events.ErrorBadVersion(request.Version ?? "(none)");
return this.Json(GetErrorResult());
}
try
{
var isAuthenticated = default(bool);
var credentials = default(Option<IClientCredentials>);
(isAuthenticated, credentials) =
await this.GetIdsFromUsername(request)
.FlatMap(ci => this.GetCredentials(request, ci))
.Match(
async creds => (await this.AuthenticateAsync(creds), Option.Some(creds)),
() => Task.FromResult((false, Option.None<IClientCredentials>())));
return this.Json(GetAuthResult(isAuthenticated, credentials));
}
catch (Exception e)
{
Events.ErrorProcessingRequest(e);
return this.Json(GetErrorResult());
}
}
Option<ClientInfo> GetIdsFromUsername(AuthRequest request)
{
try
{
return Option.Some(this.usernameParser.Parse(request.Username));
}
catch (Exception e)
{
Events.InvalidUsernameFormat(e);
return Option.None<ClientInfo>();
}
}
Option<IClientCredentials> GetCredentials(AuthRequest request, ClientInfo clientInfo)
{
var result = Option.None<IClientCredentials>();
try
{
var isPasswordPresent = !string.IsNullOrWhiteSpace(request.Password);
var isCertificatePresent = !string.IsNullOrWhiteSpace(request.EncodedCertificate);
if (isPasswordPresent && isCertificatePresent)
{
Events.MoreCredentialsSpecified();
}
else if (isPasswordPresent)
{
result = Option.Some(
this.clientCredentialsFactory.GetWithSasToken(
clientInfo.DeviceId,
clientInfo.ModuleId,
clientInfo.DeviceClientType,
request.Password,
false));
}
else if (isCertificatePresent)
{
var certificate = DecodeCertificate(request.EncodedCertificate);
var chain = DecodeCertificateChain(request.EncodedCertificateChain);
result = Option.Some(
this.clientCredentialsFactory.GetWithX509Cert(
clientInfo.DeviceId,
clientInfo.ModuleId,
clientInfo.DeviceClientType,
certificate,
chain));
}
else
{
Events.NoCredentialsSpecified();
}
}
catch (Exception e)
{
Events.InvalidCredentials(e);
}
return result;
}
async Task<bool> AuthenticateAsync(IClientCredentials credentials)
{
try
{
return await this.authenticator.AuthenticateAsync(credentials);
}
catch (Exception e)
{
Events.InvalidCredentials(e);
}
return false;
}
static object GetAuthResult(bool isAuthenticated, Option<IClientCredentials> credentials)
{
// note, that if authenticated, then these values are present, and defaults never apply
var iotHubName = credentials.Map(c => c.Identity.IotHubHostName).GetOrElse("any");
var id = credentials.Map(c => c.Identity.Id).GetOrElse("anonymous");
if (isAuthenticated)
{
Events.AuthSucceeded(id);
return new
{
result = AuthAgentConstants.Authenticated,
identity = $"{iotHubName}/{id}",
version = AuthAgentConstants.ApiVersion
};
}
else
{
Events.AuthFailed(id);
return GetErrorResult();
}
}
static object GetErrorResult() => new { result = AuthAgentConstants.Unauthenticated, version = AuthAgentConstants.ApiVersion };
static X509Certificate2 DecodeCertificate(string encodedCertificate)
{
try
{
var certificateContent = Convert.FromBase64String(encodedCertificate);
var certificate = new X509Certificate2(certificateContent);
return certificate;
}
catch (Exception e)
{
Events.ErrorDecodingCertificate(e);
throw;
}
}
static List<X509Certificate2> DecodeCertificateChain(string[] encodedCertificates)
{
if (encodedCertificates != null)
{
return encodedCertificates.Select(DecodeCertificate).ToList();
}
else
{
return new List<X509Certificate2>();
}
}
static class Events
{
const int IdStart = AuthAgentEventIds.AuthAgentController;
static readonly ILogger Log = Logger.Factory.CreateLogger<AuthAgentController>();
enum EventIds
{
AuthSucceeded,
AuthFailed,
ErrorDecodingPayload,
ErrorDecodingCertificate,
ErrorProcessingRequest,
ErrorBadVersion,
NoCredentialsSpecified,
MoreCredentialsSpecified,
InvalidUsernameFormat,
InvalidCredentials,
}
public static void AuthSucceeded(string id) => Log.LogInformation((int)EventIds.AuthSucceeded, "AUTH succeeded {0}", id);
public static void AuthFailed(string id) => Log.LogWarning((int)EventIds.AuthFailed, "AUTH failed {0}", id);
public static void ErrorDecodingPayload() => Log.LogWarning((int)EventIds.ErrorDecodingPayload, "Error decoding AUTH request, invalid JSON structure");
public static void ErrorDecodingCertificate(Exception e) => Log.LogWarning((int)EventIds.ErrorDecodingCertificate, e, "Error decoding certificate");
public static void ErrorProcessingRequest(Exception e) => Log.LogWarning((int)EventIds.ErrorProcessingRequest, e, "Error processing AUTH request");
public static void ErrorBadVersion(string version) => Log.LogWarning((int)EventIds.ErrorProcessingRequest, "Bad version number received with AUTH request {0}", version);
public static void NoCredentialsSpecified() => Log.LogWarning((int)EventIds.NoCredentialsSpecified, "No credentials specified: either a certificate or a SAS token must be present for AUTH");
public static void MoreCredentialsSpecified() => Log.LogWarning((int)EventIds.MoreCredentialsSpecified, "More credentials specified: only a certificate or a SAS token must be present for AUTH");
public static void InvalidUsernameFormat(Exception e) => Log.LogWarning((int)EventIds.InvalidUsernameFormat, e, "Invalid username format provided for AUTH");
public static void InvalidCredentials(Exception e) => Log.LogWarning((int)EventIds.InvalidCredentials, e, "Invalid credentials provided for AUTH");
}
}
}

Просмотреть файл

@ -0,0 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter
{
public class AuthAgentEventIds
{
const int EventIdStart = 7000;
public const int AuthAgentProtocolHead = EventIdStart;
public const int AuthAgentController = EventIdStart + 200;
}
}

Просмотреть файл

@ -0,0 +1,135 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter
{
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Azure.Devices.Edge.Hub.Core;
using Microsoft.Azure.Devices.Edge.Hub.Core.Identity;
using Microsoft.Azure.Devices.Edge.Hub.Mqtt;
using Microsoft.Azure.Devices.Edge.Util;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
public class AuthAgentProtocolHead : IProtocolHead
{
readonly IAuthenticator authenticator;
readonly IUsernameParser usernameParser;
readonly IClientCredentialsFactory clientCredentialsFactory;
readonly AuthAgentProtocolHeadConfig config;
readonly object guard = new object();
Option<IWebHost> host;
public string Name => "AUTH";
public AuthAgentProtocolHead(
IAuthenticator authenticator,
IUsernameParser usernameParser,
IClientCredentialsFactory clientCredentialsFactory,
AuthAgentProtocolHeadConfig config)
{
this.authenticator = Preconditions.CheckNotNull(authenticator, nameof(authenticator));
this.usernameParser = Preconditions.CheckNotNull(usernameParser, nameof(usernameParser));
this.clientCredentialsFactory = Preconditions.CheckNotNull(clientCredentialsFactory, nameof(clientCredentialsFactory));
this.config = Preconditions.CheckNotNull(config);
}
public async Task StartAsync()
{
Events.Starting();
lock (this.guard)
{
if (this.host.HasValue)
{
Events.StartedWhenAlreadyRunning();
throw new InvalidOperationException("Cannot start AuthAgentProtocolHead twice");
}
else
{
this.host = Option.Some(
CreateWebHostBuilder(
this.authenticator,
this.usernameParser,
this.clientCredentialsFactory,
this.config));
}
}
await this.host.Expect(() => new Exception("No AUTH host instance found to start"))
.StartAsync();
Events.Started();
}
public async Task CloseAsync(CancellationToken token)
{
Events.Closing();
Option<IWebHost> hostToStop;
lock (this.guard)
{
hostToStop = this.host;
this.host = Option.None<IWebHost>();
}
await hostToStop.Match(
async h => await h.StopAsync(),
() =>
{
Events.ClosedWhenNotRunning();
throw new InvalidOperationException("Cannot stop AuthAgentProtocolHead when not running");
});
Events.Closed();
}
public void Dispose() => this.CloseAsync(CancellationToken.None).Wait();
static IWebHost CreateWebHostBuilder(
IAuthenticator authenticator,
IUsernameParser usernameParser,
IClientCredentialsFactory clientCredentialsFactory,
AuthAgentProtocolHeadConfig config)
{
return WebHost.CreateDefaultBuilder()
.UseStartup<AuthAgentStartup>()
.UseKestrel(serverOptions => serverOptions.Limits.MaxRequestBufferSize = 64 * 1024)
.UseUrls($"http://*:{config.Port}")
.ConfigureServices(s => s.TryAddSingleton(authenticator))
.ConfigureServices(s => s.TryAddSingleton(usernameParser))
.ConfigureServices(s => s.TryAddSingleton(clientCredentialsFactory))
.ConfigureServices(s => s.TryAddSingleton(config))
.ConfigureServices(s => s.AddMvc())
.ConfigureLogging(c => c.ClearProviders())
.Build();
}
static class Events
{
const int IdStart = AuthAgentEventIds.AuthAgentProtocolHead;
static readonly ILogger Log = Logger.Factory.CreateLogger<AuthAgentProtocolHead>();
enum EventIds
{
Starting = IdStart,
Started,
Closing,
Closed,
ClosedWhenNotRunning,
StartedWhenAlreadyRunning
}
public static void Starting() => Log.LogInformation((int)EventIds.Starting, "Starting AUTH head");
public static void Started() => Log.LogInformation((int)EventIds.Started, "Started AUTH head");
public static void Closing() => Log.LogInformation((int)EventIds.Closing, "Closing AUTH head");
public static void Closed() => Log.LogInformation((int)EventIds.Closed, "Closed AUTH head");
public static void ClosedWhenNotRunning() => Log.LogInformation((int)EventIds.ClosedWhenNotRunning, "Closed AUTH head when it was not running");
public static void StartedWhenAlreadyRunning() => Log.LogWarning((int)EventIds.StartedWhenAlreadyRunning, "Started AUTH head when it was already running");
}
}
}

Просмотреть файл

@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter
{
using Microsoft.Azure.Devices.Edge.Util;
public class AuthAgentProtocolHeadConfig
{
public AuthAgentProtocolHeadConfig(int port, string baseUrl)
{
this.Port = Preconditions.CheckNotNull(port, nameof(port));
this.BaseUrl = Preconditions.CheckNotNull(baseUrl, nameof(baseUrl));
}
public int Port { get; }
public string BaseUrl { get; }
}
}

Просмотреть файл

@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter
{
using Microsoft.AspNetCore.Builder;
public class AuthAgentStartup
{
readonly AuthAgentProtocolHeadConfig config;
public AuthAgentStartup(AuthAgentProtocolHeadConfig config)
{
this.config = config;
}
public void Configure(IApplicationBuilder app)
{
app.UseMvc(routes =>
{
routes.MapRoute(
"authenticate",
this.config.BaseUrl,
defaults: new { controller = "AuthAgent", action = "HandleAsync" });
});
}
}
}

Просмотреть файл

@ -0,0 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter
{
using System.Linq;
using Newtonsoft.Json;
public class AuthRequest
{
[JsonConstructor]
AuthRequest(string version, string username, string password, string encodedCertificate, string[] encodedCertificateChain)
{
this.Version = version;
this.Username = username;
this.Password = password;
this.EncodedCertificate = encodedCertificate;
if (encodedCertificateChain != null)
{
this.EncodedCertificateChain = encodedCertificateChain.ToArray();
}
}
[JsonProperty("version", Required = Required.Always)]
public string Version { get; }
[JsonProperty("username", Required = Required.Always)]
public string Username { get; }
[JsonProperty("password")]
public string Password { get; }
[JsonProperty("certificate")]
public string EncodedCertificate { get; }
[JsonProperty("certificateChain")]
public string[] EncodedCertificateChain { get; }
}
}

Просмотреть файл

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<Configurations>Debug;Release;CheckInBuild</Configurations>
<HighEntropyVA>true</HighEntropyVA>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Azure.Devices.Edge.Hub.Core\Microsoft.Azure.Devices.Edge.Hub.Core.csproj" />
<ProjectReference Include="..\Microsoft.Azure.Devices.Edge.Hub.Mqtt\Microsoft.Azure.Devices.Edge.Hub.Mqtt.csproj" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\..\stylecop.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<Import Project="..\..\..\stylecop.props" />
</Project>

Просмотреть файл

@ -95,6 +95,9 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Service
this.RegisterMqttModule(builder, storeAndForward, optimizeForPerformance);
this.RegisterAmqpModule(builder);
builder.RegisterModule(new HttpModule());
var authConfig = this.configuration.GetSection("authAgentSettings");
builder.RegisterModule(new AuthModule(authConfig));
}
internal static Option<UpstreamProtocol> GetUpstreamProtocol(IConfigurationRoot configuration) =>

Просмотреть файл

@ -40,6 +40,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\edge-util\src\Microsoft.Azure.Devices.Edge.Storage.RocksDb\Microsoft.Azure.Devices.Edge.Storage.RocksDb.csproj" />
<ProjectReference Include="..\Microsoft.Azure.Devices.Edge.Hub.Amqp\Microsoft.Azure.Devices.Edge.Hub.Amqp.csproj" />
<ProjectReference Include="..\Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter\Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter.csproj" />
<ProjectReference Include="..\Microsoft.Azure.Devices.Edge.Hub.CloudProxy\Microsoft.Azure.Devices.Edge.Hub.CloudProxy.csproj" />
<ProjectReference Include="..\Microsoft.Azure.Devices.Edge.Hub.Http\Microsoft.Azure.Devices.Edge.Hub.Http.csproj" />
<ProjectReference Include="..\Microsoft.Azure.Devices.Edge.Hub.Mqtt\Microsoft.Azure.Devices.Edge.Hub.Mqtt.csproj" />

Просмотреть файл

@ -15,6 +15,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Service
using Microsoft.Azure.Devices.Edge.Hub.Core.Identity;
using Microsoft.Azure.Devices.Edge.Hub.Http;
using Microsoft.Azure.Devices.Edge.Hub.Mqtt;
using Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter;
using Microsoft.Azure.Devices.Edge.Storage;
using Microsoft.Azure.Devices.Edge.Util;
using Microsoft.Azure.Devices.Edge.Util.Metrics;
@ -153,6 +154,11 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Service
protocolHeads.Add(new HttpProtocolHead(hosting.WebHost));
}
if (configuration.GetValue("authAgentSettings:enabled", true))
{
protocolHeads.Add(await container.Resolve<Task<AuthAgentProtocolHead>>());
}
return new EdgeHubProtocolHead(protocolHeads, logger);
}

Просмотреть файл

@ -29,6 +29,11 @@
"enabled": true,
"port": 443
},
"authAgentSettings": {
"enabled": true,
"port": 7120,
"baseUrl": "/authenticate/"
},
"IotHubConnectionPoolSize": 1,
"IotHubConnectionString": "",
"mqttTopicNameConversion": {

Просмотреть файл

@ -0,0 +1,49 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Hub.Service.Modules
{
using System.Threading.Tasks;
using Autofac;
using Microsoft.Azure.Devices.Edge.Hub.Core;
using Microsoft.Azure.Devices.Edge.Hub.Core.Identity;
using Microsoft.Azure.Devices.Edge.Hub.Mqtt;
using Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter;
using Microsoft.Azure.Devices.Edge.Util;
using Microsoft.Extensions.Configuration;
class AuthModule : Module
{
static readonly int defaultPort = 7120;
static readonly string defaultBaseUrl = "/authenticate/";
readonly IConfiguration config;
public AuthModule(IConfiguration config)
{
this.config = Preconditions.CheckNotNull(config, nameof(config));
}
protected override void Load(ContainerBuilder builder)
{
builder.Register(
async c =>
{
var auth = await c.Resolve<Task<IAuthenticator>>();
var usernameParser = c.Resolve<IUsernameParser>();
var identityFactory = c.Resolve<IClientCredentialsFactory>();
var port = this.config.GetValue("port", defaultPort);
var baseUrl = this.config.GetValue("baseUrl", defaultBaseUrl);
var config = new AuthAgentProtocolHeadConfig(port, baseUrl);
return new AuthAgentProtocolHead(auth, usernameParser, identityFactory, config);
})
.As<Task<AuthAgentProtocolHead>>()
.SingleInstance();
base.Load(builder);
}
}
}

Просмотреть файл

@ -124,6 +124,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Service.Modules
var mqttConnectionProviderTask = c.Resolve<Task<IMqttConnectionProvider>>();
var sessionStatePersistenceProviderTask = c.Resolve<Task<ISessionStatePersistenceProvider>>();
var authenticatorProviderTask = c.Resolve<Task<IAuthenticator>>();
var usernameParser = c.Resolve<IUsernameParser>();
IClientCredentialsFactory clientCredentialsProvider = c.Resolve<IClientCredentialsFactory>();
IMqttConnectionProvider mqttConnectionProvider = await mqttConnectionProviderTask;
ISessionStatePersistenceProvider sessionStatePersistenceProvider = await sessionStatePersistenceProviderTask;
@ -133,6 +134,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Service.Modules
this.tlsCertificate,
mqttConnectionProvider,
authenticator,
usernameParser,
clientCredentialsProvider,
sessionStatePersistenceProvider,
websocketListenerRegistry,

Просмотреть файл

@ -15,6 +15,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Service.Modules
using Microsoft.Azure.Devices.Edge.Hub.Core.Routing;
using Microsoft.Azure.Devices.Edge.Hub.Core.Storage;
using Microsoft.Azure.Devices.Edge.Hub.Core.Twin;
using Microsoft.Azure.Devices.Edge.Hub.Mqtt;
using Microsoft.Azure.Devices.Edge.Storage;
using Microsoft.Azure.Devices.Edge.Util;
using Microsoft.Azure.Devices.Edge.Util.TransientFaultHandling;
@ -585,6 +586,8 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Service.Modules
.As<Task<IConnectionProvider>>()
.SingleInstance();
builder.RegisterType<MqttUsernameParser>().As<IUsernameParser>().SingleInstance();
base.Load(builder);
}

Просмотреть файл

@ -93,27 +93,6 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt.Test
};
}
public static IEnumerable<object[]> GetUsernames()
{
yield return new object[] { "iotHub1/device1/api-version=2010-01-01&DeviceClientType=customDeviceClient1", "device1", string.Empty, "customDeviceClient1" };
yield return new object[] { "iotHub1/device1/module1/api-version=2010-01-01&DeviceClientType=customDeviceClient2", "device1", "module1", "customDeviceClient2" };
yield return new object[] { "iotHub1/device1/module1/api-version=2017-06-30/DeviceClientType=Microsoft.Azure.Devices.Client/1.5.1-preview-003", "device1", "module1", "Microsoft.Azure.Devices.Client/1.5.1-preview-003" };
yield return new object[] { "iotHub1/device1/?api-version=2010-01-01&DeviceClientType=customDeviceClient1", "device1", string.Empty, "customDeviceClient1" };
yield return new object[] { "iotHub1/device1/module1/?api-version=2010-01-01&DeviceClientType=customDeviceClient1", "device1", "module1", "customDeviceClient1" };
yield return new object[] { "iotHub1/device1/api-version=2010-01-01&DeviceClientType1=customDeviceClient1", "device1", string.Empty, string.Empty };
yield return new object[] { "iotHub1/device1/module1/api-version=2010-01-01&", "device1", "module1", string.Empty };
yield return new object[] { "iotHub1/device1/?api-version=2010-01-01", "device1", string.Empty, string.Empty };
yield return new object[] { "iotHub1/device1/module1/?api-version=2010-01-01&Foo=customDeviceClient1", "device1", "module1", string.Empty };
}
[Theory]
[Integration]
[MemberData(nameof(GetIdentityProviderInputs))]
@ -130,7 +109,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt.Test
var authenticator = new Mock<IAuthenticator>();
var productInfoStore = Mock.Of<IProductInfoStore>();
authenticator.Setup(a => a.AuthenticateAsync(It.IsAny<IClientCredentials>())).ReturnsAsync(authRetVal);
var deviceIdentityProvider = new DeviceIdentityProvider(authenticator.Object, new ClientCredentialsFactory(new IdentityProvider(iotHubHostName)), productInfoStore, true);
var deviceIdentityProvider = new DeviceIdentityProvider(authenticator.Object, new MqttUsernameParser(), new ClientCredentialsFactory(new IdentityProvider(iotHubHostName)), productInfoStore, true);
if (certificate != null)
{
deviceIdentityProvider.RegisterConnectionCertificate(certificate, chain);
@ -140,36 +119,6 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt.Test
Assert.IsAssignableFrom(expectedType, deviceIdentity);
}
[Theory]
[MemberData(nameof(GetUsernames))]
[Unit]
public void ParseUsernameTest(string username, string expectedDeviceId, string expectedModuleId, string expectedDeviceClientType)
{
(string deviceId, string moduleId, string deviceClientType) = DeviceIdentityProvider.ParseUserName(username);
Assert.Equal(expectedDeviceId, deviceId);
Assert.Equal(expectedModuleId, moduleId);
Assert.Equal(expectedDeviceClientType, deviceClientType);
}
[Theory]
[InlineData("iotHub1/device1")]
[InlineData("iotHub1/device1/fooBar")]
[InlineData("iotHub1/device1/api-version")]
[InlineData("iotHub1/device1/module1/fooBar")]
[InlineData("iotHub1/device1/module1/api-version")]
[InlineData("iotHub1/device1/module1/api-version=2017-06-30/DeviceClientType=Microsoft.Azure.Devices.Client/1.5.1-preview-003/prodInfo")]
[InlineData("iotHub1/device1/module1/api-version=2017-06-30/DeviceClientType=Microsoft.Azure.Devices.Client")]
[InlineData("iotHub1/device1/module1")]
[InlineData("iotHub1/device1/module1?api-version=2010-01-01?DeviceClientType=customDeviceClient1")]
[InlineData("iotHub1?api-version=2010-01-01&DeviceClientType=customDeviceClient1")]
[InlineData("iotHub1/device1/module1/?version=2010-01-01&DeviceClientType=customDeviceClient1")]
[InlineData("iotHub1//?api-version=2010-01-01&DeviceClientType=customDeviceClient1")]
[Unit]
public void ParseUserNameErrorTest(string username)
{
Assert.Throws<EdgeHubConnectionException>(() => DeviceIdentityProvider.ParseUserName(username));
}
[Fact]
[Unit]
public async Task GetIdentityCertAuthNotEnabled()
@ -178,7 +127,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt.Test
var authenticator = new Mock<IAuthenticator>();
var productInfoStore = Mock.Of<IProductInfoStore>();
authenticator.Setup(a => a.AuthenticateAsync(It.IsAny<IClientCredentials>())).ReturnsAsync(true);
var deviceIdentityProvider = new DeviceIdentityProvider(authenticator.Object, new ClientCredentialsFactory(new IdentityProvider(iotHubHostName)), productInfoStore, false);
var deviceIdentityProvider = new DeviceIdentityProvider(authenticator.Object, new MqttUsernameParser(), new ClientCredentialsFactory(new IdentityProvider(iotHubHostName)), productInfoStore, false);
deviceIdentityProvider.RegisterConnectionCertificate(new X509Certificate2(), new List<X509Certificate2> { new X509Certificate2() });
IDeviceIdentity deviceIdentity = await deviceIdentityProvider.GetAsync(
"Device_2",

Просмотреть файл

@ -233,18 +233,6 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt.Test
};
}
public static IEnumerable<object[]> GetBadUsernameInputs()
{
yield return new[] { "missingEverythingAfterHostname" };
yield return new[] { "hostname/missingEverthingAfterDeviceId" };
yield return new[] { "hostname/deviceId/missingApiVersionProperty" };
yield return new[] { "hostname/deviceId/moduleId/missingApiVersionProperty" };
yield return new[] { "hostname/deviceId/moduleId/stillMissingApiVersionProperty&DeviceClientType=whatever" };
yield return new[] { "hostname/deviceId/moduleId/DeviceClientType=whatever&stillMissingApiVersionProperty" };
yield return new[] { "hostname/deviceId/moduleId/DeviceClientType=stillMissingApiVersionProperty" };
yield return new[] { "hostname/deviceId/moduleId/api-version=whatever/tooManySegments" };
}
[Theory]
[Unit]
[MemberData(nameof(GetIdentityInputs))]
@ -306,14 +294,6 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt.Test
productInfoStore.VerifyAll();
}
[Theory]
[Unit]
[MemberData(nameof(GetBadUsernameInputs))]
public void NegativeUsernameTest(string username)
{
Assert.Throws<EdgeHubConnectionException>(() => DeviceIdentityProvider.ParseUserName(username));
}
[Theory]
[Unit]
[MemberData(nameof(GetModuleIdentityInputs))]
@ -341,8 +321,9 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt.Test
{
productInfoStore = productInfoStore ?? Mock.Of<IProductInfoStore>();
var authenticator = Mock.Of<IAuthenticator>(a => a.AuthenticateAsync(It.IsAny<IClientCredentials>()) == Task.FromResult(true));
var usernameParser = new MqttUsernameParser();
var factory = new ClientCredentialsFactory(new IdentityProvider(iotHubHostName), productInfo);
var credentialIdentityProvider = new DeviceIdentityProvider(authenticator, factory, productInfoStore, isCertAuthAllowed);
var credentialIdentityProvider = new DeviceIdentityProvider(authenticator, usernameParser, factory, productInfoStore, isCertAuthAllowed);
if (certificate != null && chain != null)
{
credentialIdentityProvider.RegisterConnectionCertificate(certificate, chain);

Просмотреть файл

@ -0,0 +1,81 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Hub.Core.Test
{
using System.Collections.Generic;
using Microsoft.Azure.Devices.Edge.Hub.Mqtt;
using Microsoft.Azure.Devices.Edge.Util.Test.Common;
using Xunit;
[Unit]
public class MqttUsernameParserTest
{
public static IEnumerable<object[]> GetUsernames()
{
yield return new object[] { "iotHub1/device1/api-version=2010-01-01&DeviceClientType=customDeviceClient1", "device1", string.Empty, "customDeviceClient1" };
yield return new object[] { "iotHub1/device1/module1/api-version=2010-01-01&DeviceClientType=customDeviceClient2", "device1", "module1", "customDeviceClient2" };
yield return new object[] { "iotHub1/device1/module1/api-version=2017-06-30/DeviceClientType=Microsoft.Azure.Devices.Client/1.5.1-preview-003", "device1", "module1", "Microsoft.Azure.Devices.Client/1.5.1-preview-003" };
yield return new object[] { "iotHub1/device1/?api-version=2010-01-01&DeviceClientType=customDeviceClient1", "device1", string.Empty, "customDeviceClient1" };
yield return new object[] { "iotHub1/device1/module1/?api-version=2010-01-01&DeviceClientType=customDeviceClient1", "device1", "module1", "customDeviceClient1" };
yield return new object[] { "iotHub1/device1/api-version=2010-01-01&DeviceClientType1=customDeviceClient1", "device1", string.Empty, string.Empty };
yield return new object[] { "iotHub1/device1/module1/api-version=2010-01-01&", "device1", "module1", string.Empty };
yield return new object[] { "iotHub1/device1/?api-version=2010-01-01", "device1", string.Empty, string.Empty };
yield return new object[] { "iotHub1/device1/module1/?api-version=2010-01-01&Foo=customDeviceClient1", "device1", "module1", string.Empty };
}
public static IEnumerable<object[]> GetBadUsernameInputs()
{
yield return new[] { "missingEverythingAfterHostname" };
yield return new[] { "hostname/missingEverthingAfterDeviceId" };
yield return new[] { "hostname/deviceId/missingApiVersionProperty" };
yield return new[] { "hostname/deviceId/moduleId/missingApiVersionProperty" };
yield return new[] { "hostname/deviceId/moduleId/stillMissingApiVersionProperty&DeviceClientType=whatever" };
yield return new[] { "hostname/deviceId/moduleId/DeviceClientType=whatever&stillMissingApiVersionProperty" };
yield return new[] { "hostname/deviceId/moduleId/DeviceClientType=stillMissingApiVersionProperty" };
yield return new[] { "hostname/deviceId/moduleId/api-version=whatever/tooManySegments" };
}
[Theory]
[MemberData(nameof(GetUsernames))]
[Unit]
public void ParseUsernameTest(string username, string expectedDeviceId, string expectedModuleId, string expectedDeviceClientType)
{
var usernameParser = new MqttUsernameParser();
ClientInfo clientInfo = usernameParser.Parse(username);
Assert.Equal(expectedDeviceId, clientInfo.DeviceId);
Assert.Equal(expectedModuleId, clientInfo.ModuleId);
Assert.Equal(expectedDeviceClientType, clientInfo.DeviceClientType);
}
[Theory]
[InlineData("iotHub1/device1")]
[InlineData("iotHub1/device1/fooBar")]
[InlineData("iotHub1/device1/api-version")]
[InlineData("iotHub1/device1/module1/fooBar")]
[InlineData("iotHub1/device1/module1/api-version")]
[InlineData("iotHub1/device1/module1/api-version=2017-06-30/DeviceClientType=Microsoft.Azure.Devices.Client/1.5.1-preview-003/prodInfo")]
[InlineData("iotHub1/device1/module1/api-version=2017-06-30/DeviceClientType=Microsoft.Azure.Devices.Client")]
[InlineData("iotHub1/device1/module1")]
[InlineData("iotHub1/device1/module1?api-version=2010-01-01?DeviceClientType=customDeviceClient1")]
[InlineData("iotHub1?api-version=2010-01-01&DeviceClientType=customDeviceClient1")]
[InlineData("iotHub1/device1/module1/?version=2010-01-01&DeviceClientType=customDeviceClient1")]
[InlineData("iotHub1//?api-version=2010-01-01&DeviceClientType=customDeviceClient1")]
[Unit]
public void ParseUserNameErrorTest(string username)
{
var usernameParser = new MqttUsernameParser();
Assert.Throws<EdgeHubConnectionException>(() => usernameParser.Parse(username));
}
[Theory]
[Unit]
[MemberData(nameof(GetBadUsernameInputs))]
public void NegativeUsernameTest(string username)
{
var usernameParser = new MqttUsernameParser();
Assert.Throws<EdgeHubConnectionException>(() => usernameParser.Parse(username));
}
}
}

Просмотреть файл

@ -25,6 +25,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Mqtt.Test
new Settings(Mock.Of<ISettingsProvider>()),
id => Task.FromResult(Mock.Of<IMessagingBridge>()),
Mock.Of<IAuthenticator>(),
Mock.Of<IUsernameParser>(),
Mock.Of<IClientCredentialsFactory>(),
() => Mock.Of<ISessionStatePersistenceProvider>(),
new MultithreadEventLoopGroup(),

Просмотреть файл

@ -0,0 +1,627 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter.Test
{
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Edge.Hub.Core;
using Microsoft.Azure.Devices.Edge.Hub.Core.Identity;
using Microsoft.Azure.Devices.Edge.Hub.Mqtt;
using Microsoft.Azure.Devices.Edge.Util;
using Microsoft.Azure.Devices.Edge.Util.Test.Common;
using Moq;
using Newtonsoft.Json;
using Xunit;
[Integration]
public class AuthAgentHeadTest
{
const string HOST = "localhost";
const int PORT = 7120;
const string URL = "http://localhost:7120/authenticate/";
readonly AuthAgentProtocolHeadConfig config = new AuthAgentProtocolHeadConfig(PORT, "/authenticate/");
[Fact]
public async Task StartsUpAndServes()
{
(var authenticator, var usernameParser, var credFactory) = SetupAcceptEverything();
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
dynamic content = new ExpandoObject();
content.version = "2020-04-20";
content.username = "testhub/device/api-version=2018-06-30";
content.password = "somepassword";
dynamic response = await PostAsync(content, URL);
Assert.Equal(200, (int)response.result);
}
}
[Fact]
public async Task CannotStartTwice()
{
(var authenticator, var usernameParser, var credFactory) = SetupAcceptEverything();
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sut.StartAsync());
}
}
[Fact]
public async Task DeniesNoPasswordNorCertificate()
{
(var authenticator, var usernameParser, var credFactory) = SetupAcceptEverything();
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
dynamic content = new ExpandoObject();
content.version = "2020-04-20";
content.username = "testhub/device/api-version=2018-06-30";
dynamic response = await PostAsync(content, URL);
Assert.Equal(403, (int)response.result);
}
}
[Fact]
public async Task DeniesBothPasswordAndCertificate()
{
(var authenticator, var usernameParser, var credFactory) = SetupAcceptEverything();
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
dynamic content = new ExpandoObject();
content.version = "2020-04-20";
content.username = "testhub/device/api-version=2018-06-30";
content.password = "somepassword";
content.certificate = ThumbprintTestCert;
dynamic response = await PostAsync(content, URL);
Assert.Equal(403, (int)response.result);
}
}
[Fact]
public async Task DeniesBadCertificateFormat()
{
(var authenticator, var usernameParser, var credFactory) = SetupAcceptEverything();
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
dynamic content = new ExpandoObject();
content.version = "2020-04-20";
content.username = "testhub/device/api-version=2018-06-30";
content.certificate = new byte[] { 0x30, 0x23, 0x44 };
dynamic response = await PostAsync(content, URL);
Assert.Equal(403, (int)response.result);
}
}
[Fact]
public async Task DeniesNoVersion()
{
(var authenticator, var usernameParser, var credFactory) = SetupAcceptEverything();
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
dynamic content = new ExpandoObject();
content.username = "testhub/device/api-version=2018-06-30";
content.password = "somepassword";
dynamic response = await PostAsync(content, URL);
Assert.Equal(403, (int)response.result);
}
}
[Fact]
public async Task DeniesBadVersion()
{
(var authenticator, var usernameParser, var credFactory) = SetupAcceptEverything();
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
dynamic content = new ExpandoObject();
content.version = "2017-04-20";
content.username = "testhub/device/api-version=2018-06-30";
content.password = "somepassword";
dynamic response = await PostAsync(content, URL);
Assert.Equal(403, (int)response.result);
}
}
[Fact]
public async Task AcceptsGoodTokenDeniesBadToken()
{
(_, var usernameParser, var credFactory) = SetupAcceptEverything();
var authenticator = SetupAcceptGoodToken("good_token");
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
dynamic content = new ExpandoObject();
content.version = "2020-04-20";
content.username = "testhub/device/api-version=2018-06-30";
content.password = "bad_token";
dynamic response = await PostAsync(content, URL);
Assert.Equal(403, (int)response.result);
content.password = "good_token";
response = await PostAsync(content, URL);
Assert.Equal(200, (int)response.result);
}
}
[Fact]
public async Task AcceptsGoodThumbprintDeniesBadThumbprint()
{
(_, var usernameParser, var credFactory) = SetupAcceptEverything();
var authenticator = SetupAcceptGoodThumbprint(ThumbprintTestCertThumbprint2);
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
dynamic content = new ExpandoObject();
content.version = "2020-04-20";
content.username = "testhub/device/api-version=2018-06-30";
content.certificate = ThumbprintTestCert;
dynamic response = await PostAsync(content, URL);
Assert.Equal(403, (int)response.result);
content.certificate = ThumbprintTestCert2;
response = await PostAsync(content, URL);
Assert.Equal(200, (int)response.result);
}
}
[Fact]
public async Task AcceptsGoodCaDeniesBadCa()
{
(_, var usernameParser, var credFactory) = SetupAcceptEverything();
var goodCa = new X509Certificate2(Encoding.ASCII.GetBytes(CaTestRoot2));
var authenticator = SetupAcceptGoodCa(goodCa);
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
dynamic content = new ExpandoObject();
content.version = "2020-04-20";
content.username = "testhub/device/api-version=2018-06-30";
content.certificate = CaTestDevice;
content.certificateChain = new List<string>() { CaTestRoot };
dynamic response = await PostAsync(content, URL);
Assert.Equal(403, (int)response.result);
content.certificate = CaTestDevice2;
content.certificateChain = new List<string>() { CaTestRoot2 };
response = await PostAsync(content, URL);
Assert.Equal(200, (int)response.result);
}
}
[Fact]
public async Task ReturnsDeviceIdentity()
{
(var authenticator, var usernameParser, var credFactory) = SetupAcceptEverything();
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
dynamic content = new ExpandoObject();
content.version = "2020-04-20";
content.username = "testhub/device/api-version=2018-06-30";
content.password = "somepassword";
var response = await PostAsync(content, URL);
Assert.Equal(200, (int)response.result);
Assert.Equal("testhub/device", (string)response.identity);
}
}
[Fact]
public async Task ReturnsModuleIdentity()
{
(var authenticator, var usernameParser, var credFactory) = SetupAcceptEverything();
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
dynamic content = new ExpandoObject();
content.version = "2020-04-20";
content.username = "testhub/device/module/api-version=2018-06-30";
content.password = "somepassword";
var response = await PostAsync(content, URL);
Assert.Equal(200, (int)response.result);
Assert.Equal("testhub/device/module", (string)response.identity);
}
}
[Fact]
public async Task AcceptsRequestWithContentLength()
{
(var authenticator, var usernameParser, var credFactory) = SetupAcceptEverything();
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
var result = await SendDirectRequest(RequestBody);
Assert.StartsWith(@"{""result"":200,", result);
}
}
[Fact]
public async Task AcceptsRequestWithNoContentLength()
{
(var authenticator, var usernameParser, var credFactory) = SetupAcceptEverything();
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
var result = await SendDirectRequest(RequestBody, withContentLength: false);
Assert.StartsWith(@"{""result"":200,", result);
}
}
[Fact]
public async Task DeniesMalformedJsonRequest()
{
(var authenticator, var usernameParser, var credFactory) = SetupAcceptEverything();
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
var result = await SendDirectRequest(NonJSONRequestBody);
Assert.StartsWith(@"{""result"":403,", result);
}
}
[Fact]
public async Task DeniesBadContentLengthLongBody()
{
(var authenticator, var usernameParser, var credFactory) = SetupAcceptEverything();
using (var sut = new AuthAgentProtocolHead(authenticator, usernameParser, credFactory, config))
{
await sut.StartAsync();
var result = await SendDirectRequest(RequestBody, contentLengthOverride: RequestBody.Length - 10);
Assert.StartsWith(@"{""result"":403,", result);
}
}
private async Task<string> SendDirectRequest(string content, bool withContentLength = true, int contentLengthOverride = 0)
{
using (var client = new TcpClient())
{
await client.ConnectAsync(HOST, PORT);
using (var stream = client.GetStream())
{
var request = GetRequestWithBody(content, withContentLength, contentLengthOverride);
await stream.WriteAsync(request);
var response = await ReadResponse(stream);
return GetContentFromResponse(response);
}
}
}
private async Task<string> ReadResponse(NetworkStream stream)
{
var startTime = DateTime.Now;
var readBytes = default(int);
var readBuffer = new byte[500];
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
do
{
readBytes += await stream.ReadAsync(readBuffer, readBytes, readBuffer.Length - readBytes, tokenSource.Token);
}
while (!IsTimeout(startTime) && readBytes == 0);
var response = Encoding.UTF8.GetString(readBuffer, 0, readBytes);
return response;
}
private byte[] GetRequestWithBody(string content, bool withContentLength, int contentLengthOverride)
{
var encodedBody = Encoding.UTF8.GetBytes(content);
var contentLength = default(int);
var chunkCloseLength = 0;
var chunkClose = Encoding.ASCII.GetBytes("\r\n0\r\n\r\n");
if (contentLengthOverride > 0)
{
contentLength = contentLengthOverride;
}
else
{
contentLength = encodedBody.Length;
}
var headerTemplate = default(string);
if (withContentLength)
{
headerTemplate = String.Format(RequestWithContentLenTemplate, contentLength);
}
else
{
headerTemplate = String.Format(RequestWithNoContentLenTemplate, contentLength);
chunkCloseLength = chunkClose.Length;
}
var header = Encoding.ASCII.GetBytes(headerTemplate);
var request = new byte[header.Length + encodedBody.Length + chunkCloseLength];
Array.Copy(header, request, header.Length);
Array.Copy(encodedBody, 0, request, header.Length, encodedBody.Length);
if (withContentLength == false)
{
Array.Copy(chunkClose, 0, request, request.Length - chunkCloseLength, chunkCloseLength);
}
return request;
}
private string GetContentFromResponse(string response)
{
var contentStart = response.IndexOf("\r\n\r\n");
if (contentStart < 0)
{
return string.Empty;
}
else
{
var content = response.Substring(contentStart + 4);
return CutChunkNumber(content);
}
}
private string CutChunkNumber(string content)
{
var result = content;
if (content.Length > 0 && char.IsDigit(content[0]))
{
var contentStart = content.IndexOf("\r\n");
result = content.Substring(contentStart + 2);
}
return result;
}
private bool IsTimeout(DateTime startTime) => DateTime.Now - startTime > TimeSpan.FromSeconds(5);
private (IAuthenticator, IUsernameParser, IClientCredentialsFactory) SetupAcceptEverything(string hubname = "testhub")
{
var authenticator = Mock.Of<IAuthenticator>();
var usernameParser = new MqttUsernameParser();
var credFactory = new ClientCredentialsFactory(new IdentityProvider(hubname));
Mock.Get(authenticator).Setup(a => a.AuthenticateAsync(It.IsAny<IClientCredentials>())).Returns(Task.FromResult(true));
return (authenticator, usernameParser, credFactory);
}
private IAuthenticator SetupAcceptGoodToken(string goodToken) => SetupAccept(
c =>
{
var result = false;
if (c is TokenCredentials tokenCreds)
{
result = tokenCreds.Token == goodToken;
}
return Task.FromResult(result);
});
private IAuthenticator SetupAcceptGoodThumbprint(string goodThumbprint) => SetupAccept(
c =>
{
var result = false;
if (c is X509CertCredentials x509Creds)
{
result = string.Equals(x509Creds.ClientCertificate.Thumbprint, goodThumbprint, StringComparison.OrdinalIgnoreCase);
}
return Task.FromResult(result);
});
private IAuthenticator SetupAcceptGoodCa(X509Certificate2 goodCa) => SetupAccept(
c =>
{
var trustedCaList = Option.Some<IList<X509Certificate2>>(new List<X509Certificate2>() { goodCa });
var result = false;
if (c is X509CertCredentials x509Creds)
{
(result, _) = Util.CertificateHelper.ValidateCert(x509Creds.ClientCertificate, x509Creds.ClientCertificateChain, trustedCaList);
}
return Task.FromResult(result);
});
private IAuthenticator SetupAccept(Func<IClientCredentials, Task<bool>> condition)
{
var authenticator = Mock.Of<IAuthenticator>();
Mock.Get(authenticator)
.Setup(a => a.AuthenticateAsync(It.IsAny<IClientCredentials>()))
.Returns(condition);
return authenticator;
}
private static async Task<dynamic> PostAsync(dynamic content, string requestUri)
{
using (var client = new HttpClient())
using (var request = new HttpRequestMessage(HttpMethod.Post, requestUri))
using (var httpContent = CreateContent(content))
{
request.Content = httpContent;
using (var response = await client.SendAsync(request))
{
// Note, the AuthAgent protocol is such that it always should return 200 even in case of errors.
// A test never should throw here, even if it tests a failure case
response.EnsureSuccessStatusCode();
return await ReadContent(response.Content);
}
}
}
private static async Task<dynamic> ReadContent(HttpContent content)
{
var contentStream = await content.ReadAsStreamAsync();
using (var streamReader = new StreamReader(contentStream, new UTF8Encoding()))
using (var jsonReader = new JsonTextReader(streamReader))
{
return new JsonSerializer().Deserialize<dynamic>(jsonReader);
}
}
private static HttpContent CreateContent(dynamic content)
{
var stream = SerializeJsonIntoStream(content);
var httpContent = new StreamContent(stream);
httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
return httpContent;
}
private static MemoryStream SerializeJsonIntoStream(dynamic value)
{
var result = new MemoryStream();
using (var streamWriter = new StreamWriter(result, new UTF8Encoding(false), 1024, true))
using (var jsonWriter = new JsonTextWriter(streamWriter) { Formatting = Formatting.None })
{
new JsonSerializer().Serialize(jsonWriter, value);
}
result.Seek(0, SeekOrigin.Begin);
return result;
}
private static readonly string ThumbprintTestCertThumbprint = "d57001602dd584cf0f7619ef11644d42dcd3505c";
private static readonly string ThumbprintTestCert = "MIIBLjCB1AIJAOTg4Zxl8B7jMAoGCCqGSM49BAMCMB8xHTAbBgNVBAMMFFRodW1i" +
"cHJpbnQgVGVzdCBDZXJ0MB4XDTIwMDQyMzE3NTgwN1oXDTMzMTIzMTE3NTgwN1ow" +
"HzEdMBsGA1UEAwwUVGh1bWJwcmludCBUZXN0IENlcnQwWTATBgcqhkjOPQIBBggq" +
"hkjOPQMBBwNCAARDJJBtVlgM0mBWMhAYagF7Wuc2aQYefhj0cG4wAmn3M4XcxJ39" +
"XkEup2RRAj7SSdOYhTmRpg5chhpZX/4/eF8gMAoGCCqGSM49BAMCA0kAMEYCIQD/" +
"wNzMjU1B8De5/+jEif8rkLDtqnohmVRXuAE5dCfbvAIhAJTJ+Fyg19uLSKVyOK8R" +
"5q87sIqhJXhTfNYvIt77Dq4J";
private static readonly string ThumbprintTestCertThumbprint2 = "c69f30b8feb9329506fa3f4167636915f494d19b";
private static readonly string ThumbprintTestCert2 = "MIIBMTCB2AIJAM6QHTdXFpL6MAoGCCqGSM49BAMCMCExHzAdBgNVBAMMFlRodW1i" +
"cHJpbnQgVGVzdCBDZXJ0IDIwHhcNMjAwNDIzMTgwNTMzWhcNMzMxMjMxMTgwNTMz" +
"WjAhMR8wHQYDVQQDDBZUaHVtYnByaW50IFRlc3QgQ2VydCAyMFkwEwYHKoZIzj0C" +
"AQYIKoZIzj0DAQcDQgAEKqZnpWfqQa/wS9BAeLMnhynlmHApP0/96R4q+q+HXO4m" +
"9vXQEszj2KHk9u3t/TKFfFCqbCb4uRNhQbsWDBwFqTAKBggqhkjOPQQDAgNIADBF" +
"AiBuh6l2aW4yxyhcPxOyRd0qbNJMMpx04a7waO8XvK5GNwIhALPq5K8sNzkMZhnZ" +
"tp8R7qnaCWxYLkGuaXwuZw4LST1U";
private static readonly string CaTestRoot = "MIIBfDCCASKgAwIBAgIJAIIuyXPWOrrvMAoGCCqGSM49BAMCMBQxEjAQBgNVBAMM" +
"CVRlc3QgUm9vdDAeFw0yMDA0MjQyMDUwMTRaFw0zNDAxMDEyMDUwMTRaMBQxEjAQ" +
"BgNVBAMMCVRlc3QgUm9vdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABM58STQE" +
"OhfUumBphMzVpa4dQSS6lv+qJP/Q1XV1xTW6MQBAbpxabqk4jNbCe2XLTdETbzrn" +
"Wnskm40CzxAVkBmjXTBbMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgHGMB0GA1Ud" +
"DgQWBBSrZLo1F8FcV6c4eYH1IPIlQdU9lzAfBgNVHSMEGDAWgBSrZLo1F8FcV6c4" +
"eYH1IPIlQdU9lzAKBggqhkjOPQQDAgNIADBFAiEApVcPpM3I0lsJjc1OmOOO8SGy" +
"rbv22nbkceeenoGRkyQCIHy5Na2OY49AJc1mzRpKCH10mQYkTUkSX1DaqIo//tYF";
private static readonly string CaTestDevice = "MIIBRjCB7KADAgECAgkA+igvZ6louWcwCgYIKoZIzj0EAwIwFDESMBAGA1UEAwwJ" +
"VGVzdCBSb290MB4XDTIwMDQyNDIwNTAxNVoXDTM0MDEwMTIwNTAxNVowETEPMA0G" +
"A1UEAwwGZGV2aWNlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0p3AZbTbMkJb" +
"VXQdLjoZ3wosQ5vU5NX22w7coUtz9RECDgZ6YKa6r28s1/z18Q2MRd534NOu+OUB" +
"x0UFD0qI26MqMCgwEwYDVR0lBAwwCgYIKwYBBQUHAwEwEQYDVR0RBAowCIIGZGV2" +
"aWNlMAoGCCqGSM49BAMCA0kAMEYCIQC/VEzxzPpJeD8//ltr7mUIhb/owzgbLrmi" +
"kAFHRd1UDgIhALUZ081U9Tm/bLw9rlRb5iMzrj4tUmMcwujlz+Sl73KX";
private static readonly string CaTestRoot2 = "MIIBfDCCASKgAwIBAgIJAOhzRYU913Y6MAoGCCqGSM49BAMCMBQxEjAQBgNVBAMM" +
"CVRlc3QgUm9vdDAeFw0yMDA0MjQyMDU0MzJaFw0zNDAxMDEyMDU0MzJaMBQxEjAQ" +
"BgNVBAMMCVRlc3QgUm9vdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGkdpWjE" +
"uIBtieocAB0n7/uRA0lRmwToOqNRZgb05C2Aq66QjuYXpewUzIMoaweRPFYRZQ+l" +
"8TxanEkYNKS7KAijXTBbMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgHGMB0GA1Ud" +
"DgQWBBSCNXZZQGZh6o7IIOkPPhqb11pYaDAfBgNVHSMEGDAWgBSCNXZZQGZh6o7I" +
"IOkPPhqb11pYaDAKBggqhkjOPQQDAgNIADBFAiEA/s0g4uAhcXb4i6oqJDmR0alY" +
"O+RyzRgCy22Ap3CTlC4CIHjA2CF7sMxOb5oADQRKxEDw40QDvlyXys/akxD03K49";
private static readonly string CaTestDevice2 = "MIIBRDCB7KADAgECAgkAlwSWPRWfIsYwCgYIKoZIzj0EAwIwFDESMBAGA1UEAwwJ" +
"VGVzdCBSb290MB4XDTIwMDQyNDIwNTQzMloXDTM0MDEwMTIwNTQzMlowETEPMA0G" +
"A1UEAwwGZGV2aWNlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExFs+1Rdwnr9k" +
"QAMu6vdKiDRKPgeIq0GljTzMDh5NsJhlE252CLqtI6oMdZ0Zz/3Ym5WONgcxgyyY" +
"dFFPOU5l/KMqMCgwEwYDVR0lBAwwCgYIKwYBBQUHAwEwEQYDVR0RBAowCIIGZGV2" +
"aWNlMAoGCCqGSM49BAMCA0cAMEQCIDq8t07xw0wP2qS7ynjOfWxGZcNvJhcLZNPT" +
"kIBHATXPAiAxv00Sv6MsM+a8aKhns2/yfGRKOVEhpqeSUqoqn9fhSg==";
private static readonly string RequestWithContentLenTemplate = "POST /authenticate/ HTTP/1.1\r\n" +
"Content-Type: application/json\r\n" +
"Content-Length: {0}\r\n" +
"Host: localhost\r\n\r\n";
private static readonly string RequestWithNoContentLenTemplate = "POST /authenticate/ HTTP/1.1\r\n" +
"Content-Type: application/json\r\n" +
"Transfer-Encoding: chunked\r\n" +
"Host: localhost\r\n\r\n{0:x}\r\n";
private static readonly string RequestBody = @"{""version"":""2020-04-20"",""username"":""vikauthtest/cathumb/api-version=2018-06-30"",""password"":""somesecret""}";
private static readonly string NonJSONRequestBody = @"[""version"":""2020-04-20"",""username"":""vikauthtest/cathumb/api-version=2018-06-30"",""password"":""somesecret""]";
}
}

Просмотреть файл

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\edge-util\test\Microsoft.Azure.Devices.Edge.Util.Test.Common\Microsoft.Azure.Devices.Edge.Util.Test.Common.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter\Microsoft.Azure.Devices.Edge.Hub.MqttBrokerAdapter.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Azure.Devices.Edge.Hub.Mqtt\Microsoft.Azure.Devices.Edge.Hub.Mqtt.csproj" />
</ItemGroup>
</Project>