Skip to content

Commit

Permalink
(service-client): Add constructors to accept aad and sas tokens for d…
Browse files Browse the repository at this point in the history
…igital twins client. (#1790)
  • Loading branch information
vinagesh authored Feb 11, 2021
1 parent 4ede5e5 commit bd19fac
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
namespace Microsoft.Azure.Devices.Authentication
{
/// <summary>
/// Allows authentication to the API using a Shared Access Key
/// Allows authentication to the API using a Shared Access Key generated from the connection string provided.
/// The PnP client is auto generated from swagger and needs to implement a specific class to pass to the protocol layer
/// unlike the rest of the clients which are hand-written. So, this implementation for authentication is specific to digital twin (Pnp).
/// </summary>
internal class SharedAccessKeyCredentials : IotServiceClientCredentials
internal class DigitalTwinConnectionStringCredential : DigitalTwinServiceClientCredentials
{
// Time buffer before expiry when the token should be renewed, expressed as a percentage of the time to live.
// The token will be renewed when it has 15% or less of the sas token's lifespan left.
// The token will be renewed when it has 15% or less of the SAS token's lifespan left.
private const int RenewalTimeBufferPercentage = 15;

private readonly object _sasLock = new object();
Expand All @@ -26,22 +28,20 @@ internal class SharedAccessKeyCredentials : IotServiceClientCredentials
private DateTimeOffset _tokenExpiryTime;

/// <summary>
/// Initializes a new instance of <see cref="SharedAccessKeyCredentials"/> class.
/// Initializes a new instance of <see cref="DigitalTwinConnectionStringCredential"/> class.
/// </summary>
/// <param name="connectionString">The IoT Hub connection string.</param>
internal SharedAccessKeyCredentials(string connectionString)
/// <param name="connectionString">The IoT Hub connection string properties.</param>
internal DigitalTwinConnectionStringCredential(IotHubConnectionString connectionString)
{
var iotHubConnectionString = IotHubConnectionString.Parse(connectionString);

_sharedAccessKey = iotHubConnectionString.SharedAccessKey;
_sharedAccessPolicy = iotHubConnectionString.SharedAccessKeyName;
_audience = iotHubConnectionString.Audience;
_sharedAccessKey = connectionString.SharedAccessKey;
_sharedAccessPolicy = connectionString.SharedAccessKeyName;
_audience = connectionString.Audience;

_cachedSasToken = null;
}

/// <inheritdoc />
protected override string GetSasToken()
public override string GetAuthorizationHeader()
{
lock (_sasLock)
{
Expand Down Expand Up @@ -76,6 +76,5 @@ private bool TokenShouldBeGenerated()
DateTimeOffset tokenExpiryTimeWithBuffer = _tokenExpiryTime.AddMilliseconds(-bufferTimeInMilliseconds);
return DateTimeOffset.UtcNow.CompareTo(tokenExpiryTimeWithBuffer) >= 0;
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Text;
using Azure;
using Microsoft.Azure.Devices.Authentication;

namespace Microsoft.Azure.Devices.DigitalTwin.Authentication
{
/// <summary>
/// Allows authentication to the API using a Shared Access Key provided by custom implementation.
/// The PnP client is auto generated from swagger and needs to implement a specific class to pass to the protocol layer
/// unlike the rest of the clients which are hand-written. So, this implementation for authentication is specific to digital twin (Pnp).
/// </summary>
internal class DigitalTwinSasCredential : DigitalTwinServiceClientCredentials
{
private AzureSasCredential _credential;

public DigitalTwinSasCredential(AzureSasCredential credential)
{
_credential = credential;
}

public override string GetAuthorizationHeader()
{
return _credential.Signature;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@

namespace Microsoft.Azure.Devices.Authentication
{
internal abstract class IotServiceClientCredentials : ServiceClientCredentials
/// <summary>
/// This class adds the authentication tokens to the header before calling the digital twin APIs.
/// </summary>
internal abstract class DigitalTwinServiceClientCredentials : ServiceClientCredentials, IAuthorizationHeaderProvider
{
/// <summary>
/// Add a SAS token to the outgoing http request, then send it to the next pipeline segment
/// Add a JWT for Azure Active Directory or SAS token to the outgoing http request, then send it to the next pipeline segment.
/// </summary>
/// <param name="request">The request that is being sent</param>
/// <param name="cancellationToken">The cancellation token</param>
Expand All @@ -23,7 +26,7 @@ public override Task ProcessHttpRequestAsync(HttpRequestMessage request, Cancell
{
request.ThrowIfNull(nameof(HttpRequestMessage));

request.Headers.Add(HttpRequestHeader.Authorization.ToString(), GetSasToken());
request.Headers.Add(HttpRequestHeader.Authorization.ToString(), GetAuthorizationHeader());
request.Headers.Add(HttpRequestHeader.UserAgent.ToString(), Utils.GetClientVersion());

return base.ProcessHttpRequestAsync(request, cancellationToken);
Expand All @@ -33,6 +36,6 @@ public override Task ProcessHttpRequestAsync(HttpRequestMessage request, Cancell
/// Return a SAS token
/// </summary>
/// <returns>A SAS token</returns>
protected abstract string GetSasToken();
public abstract string GetAuthorizationHeader();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Azure.Core;
using Microsoft.Azure.Devices.Authentication;

namespace Microsoft.Azure.Devices.DigitalTwin.Authentication
{
/// <summary>
/// Allows authentication to the API using a JWT token generated for Azure active directory.
/// The PnP client is auto generated from swagger and needs to implement a specific class to pass to the protocol layer
/// unlike the rest of the clients which are hand-written. so, this implementation for authentication is specific to digital twin (Pnp).
/// </summary>
internal class DigitalTwinTokenCredential : DigitalTwinServiceClientCredentials
{
private TokenCredential _credential;

public DigitalTwinTokenCredential(TokenCredential credential)
{
_credential = credential;
}

public override string GetAuthorizationHeader()
{
AccessToken token = _credential.GetToken(new TokenRequestContext(), new CancellationToken());
return $"Bearer {token.Token}";
}
}
}
120 changes: 89 additions & 31 deletions iothub/service/src/DigitalTwin/DigitalTwinClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
using Microsoft.Azure.Devices.Generated;
using Microsoft.Rest;
using Newtonsoft.Json;
using Microsoft.Azure.Devices.DigitalTwin.Authentication;
using Azure;
using Azure.Core;
using PnpDigitalTwin = Microsoft.Azure.Devices.Generated.DigitalTwin;

namespace Microsoft.Azure.Devices
{
Expand All @@ -18,8 +22,16 @@ namespace Microsoft.Azure.Devices
/// </summary>
public class DigitalTwinClient : IDisposable
{
private const string HttpsEndpointPrefix = "https";
private readonly IotHubGatewayServiceAPIs _client;
private readonly DigitalTwin _protocolLayer;
private readonly PnpDigitalTwin _protocolLayer;

private DigitalTwinClient(string hostName, DigitalTwinServiceClientCredentials credentials, params DelegatingHandler[] handlers)
{
var httpsEndpoint = new UriBuilder(HttpsEndpointPrefix, hostName).Uri;
_client = new IotHubGatewayServiceAPIs(httpsEndpoint, credentials, handlers);
_protocolLayer = new PnpDigitalTwin(_client);
}

/// <summary>
/// Initializes a new instance of the <see cref="DigitalTwinClient"/> class.</summary>
Expand All @@ -30,14 +42,60 @@ public static DigitalTwinClient CreateFromConnectionString(string connectionStri
connectionString.ThrowIfNullOrWhiteSpace(nameof(connectionString));

var iotHubConnectionString = IotHubConnectionString.Parse(connectionString);
var sharedAccessKeyCredential = new SharedAccessKeyCredentials(connectionString);
return new DigitalTwinClient(iotHubConnectionString.HttpsEndpoint, sharedAccessKeyCredential, handlers);
var connectionStringCredential = new DigitalTwinConnectionStringCredential(iotHubConnectionString);
return new DigitalTwinClient(iotHubConnectionString.HostName, connectionStringCredential, handlers);
}

/// <summary>
/// Creates an instance of <see cref="DigitalTwinClient"/>.
/// </summary>
/// <param name="hostName">IoT hub host name.</param>
/// <param name="credential">Azure Active Directory credentials to authenticate with IoT hub. See <see cref="TokenCredential"/></param>
/// <param name="handlers">The delegating handlers to add to the http client pipeline. You can add handlers for tracing, implementing a retry strategy, routing requests through a proxy, etc.</param>
/// <returns>An instance of <see cref="DigitalTwinClient"/>.</returns>
public static DigitalTwinClient Create(
string hostName,
TokenCredential credential,
params DelegatingHandler[] handlers)
{
if (string.IsNullOrEmpty(hostName))
{
throw new ArgumentNullException($"{nameof(hostName)}, Parameter cannot be null or empty");
}

if (credential == null)
{
throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null");
}

var tokenCredential = new DigitalTwinTokenCredential(credential);
return new DigitalTwinClient(hostName, tokenCredential, handlers);
}

private DigitalTwinClient(Uri uri, IotServiceClientCredentials credentials, params DelegatingHandler[] handlers)
/// <summary>
/// Creates an instance of <see cref="DigitalTwinClient"/>.
/// </summary>
/// <param name="hostName">IoT hub host name.</param>
/// <param name="credential">Credential that generates a SAS token to authenticate with IoT hub. See <see cref="AzureSasCredential"/>.</param>
/// <param name="handlers">The delegating handlers to add to the http client pipeline. You can add handlers for tracing, implementing a retry strategy, routing requests through a proxy, etc.</param>
/// <returns>An instance of <see cref="DigitalTwinClient"/>.</returns>
public static DigitalTwinClient Create(
string hostName,
AzureSasCredential credential,
params DelegatingHandler[] handlers)
{
_client = new IotHubGatewayServiceAPIs(uri, credentials, handlers);
_protocolLayer = new DigitalTwin(_client);
if (string.IsNullOrEmpty(hostName))
{
throw new ArgumentNullException($"{nameof(hostName)}, Parameter cannot be null or empty");
}

if (credential == null)
{
throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null");
}

var sasCredential = new DigitalTwinSasCredential(credential);
return new DigitalTwinClient(hostName, sasCredential, handlers);
}

/// <summary>
Expand Down Expand Up @@ -69,9 +127,9 @@ public async Task<HttpOperationResponse<T, DigitalTwinGetHeaders>> GetDigitalTwi
/// <param name="cancellationToken">The cancellationToken.</param>
/// <returns>The http response.</returns>
public Task<HttpOperationHeaderResponse<DigitalTwinUpdateHeaders>> UpdateDigitalTwinAsync(
string digitalTwinId,
string digitalTwinUpdateOperations,
DigitalTwinUpdateRequestOptions requestOptions = default,
string digitalTwinId,
string digitalTwinUpdateOperations,
DigitalTwinUpdateRequestOptions requestOptions = default,
CancellationToken cancellationToken = default)
{
return _protocolLayer.UpdateDigitalTwinWithHttpMessagesAsync(digitalTwinId, digitalTwinUpdateOperations, requestOptions?.IfMatch, null, cancellationToken);
Expand All @@ -87,19 +145,19 @@ public Task<HttpOperationHeaderResponse<DigitalTwinUpdateHeaders>> UpdateDigital
/// <param name="cancellationToken">The cancellationToken.</param>
/// <returns>The application/json command invocation response and the http response. </returns>
public async Task<HttpOperationResponse<DigitalTwinCommandResponse, DigitalTwinInvokeCommandHeaders>> InvokeCommandAsync(
string digitalTwinId,
string commandName,
string payload = default,
DigitalTwinInvokeCommandRequestOptions requestOptions = default,
string digitalTwinId,
string commandName,
string payload = default,
DigitalTwinInvokeCommandRequestOptions requestOptions = default,
CancellationToken cancellationToken = default)
{
using HttpOperationResponse<string, DigitalTwinInvokeRootLevelCommandHeaders> response = await _protocolLayer.InvokeRootLevelCommandWithHttpMessagesAsync(
digitalTwinId,
commandName,
payload,
requestOptions?.ConnectTimeoutInSeconds,
requestOptions?.ResponseTimeoutInSeconds,
null,
digitalTwinId,
commandName,
payload,
requestOptions?.ConnectTimeoutInSeconds,
requestOptions?.ResponseTimeoutInSeconds,
null,
cancellationToken)
.ConfigureAwait(false);
return new HttpOperationResponse<DigitalTwinCommandResponse, DigitalTwinInvokeCommandHeaders>
Expand All @@ -122,21 +180,21 @@ public async Task<HttpOperationResponse<DigitalTwinCommandResponse, DigitalTwinI
/// <param name="cancellationToken">The cancellationToken.</param>
/// <returns>The application/json command invocation response and the http response. </returns>
public async Task<HttpOperationResponse<DigitalTwinCommandResponse, DigitalTwinInvokeCommandHeaders>> InvokeComponentCommandAsync(
string digitalTwinId,
string componentName,
string commandName,
string payload = default,
DigitalTwinInvokeCommandRequestOptions requestOptions = default,
string digitalTwinId,
string componentName,
string commandName,
string payload = default,
DigitalTwinInvokeCommandRequestOptions requestOptions = default,
CancellationToken cancellationToken = default)
{
using HttpOperationResponse<string, DigitalTwinInvokeComponentCommandHeaders> response = await _protocolLayer.InvokeComponentCommandWithHttpMessagesAsync(
digitalTwinId,
componentName,
commandName,
payload,
requestOptions?.ConnectTimeoutInSeconds,
requestOptions?.ResponseTimeoutInSeconds,
null,
digitalTwinId,
componentName,
commandName,
payload,
requestOptions?.ConnectTimeoutInSeconds,
requestOptions?.ResponseTimeoutInSeconds,
null,
cancellationToken)
.ConfigureAwait(false);
return new HttpOperationResponse<DigitalTwinCommandResponse, DigitalTwinInvokeCommandHeaders>
Expand Down
4 changes: 2 additions & 2 deletions iothub/service/src/JobClient/JobClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public static JobClient Create(

if (credential == null)
{
throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty");
throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null");
}

var tokenCredentialProperties = new IotHubTokenCrendentialProperties(hostName, credential);
Expand All @@ -94,7 +94,7 @@ public static JobClient Create(

if (credential == null)
{
throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty");
throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null");
}

var sasCredentialProperties = new IotHubSasCredentialProperties(hostName, credential);
Expand Down
4 changes: 2 additions & 2 deletions iothub/service/src/RegistryManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public static RegistryManager Create(

if (credential == null)
{
throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty");
throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null");
}

var tokenCredentialProperties = new IotHubTokenCrendentialProperties(hostName, credential);
Expand All @@ -100,7 +100,7 @@ public static RegistryManager Create(

if (credential == null)
{
throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty");
throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null");
}

var sasCredentialProperties = new IotHubSasCredentialProperties(hostName, credential);
Expand Down
4 changes: 2 additions & 2 deletions iothub/service/src/ServiceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public static ServiceClient Create(

if (credential == null)
{
throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty");
throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null");
}

var tokenCredentialProperties = new IotHubTokenCrendentialProperties(hostName, credential);
Expand Down Expand Up @@ -120,7 +120,7 @@ public static ServiceClient Create(

if (credential == null)
{
throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty");
throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null");
}

var sasCredentialProperties = new IotHubSasCredentialProperties(hostName, credential);
Expand Down

0 comments on commit bd19fac

Please sign in to comment.