サードパーティのトークン サーバーに対する Azure IoT クライアント SDK のサポート

Azure IoT
Azure IoT Hub

IoT Hub へのアクセスの制御」の記事では、サードパーティのトークン サービスと IoT Hub をどのように統合できるかについて説明しています。 この記事では、各 Azure IoT クライアント SDK での Shared Access Signature (SAS) トークン認証のサポートについて概説しています。 また、各言語に対応する SDK を使用してデバイス アプリケーションに何を実装する必要があるか、そして、DeviceConnect または ModuleConnect の共有アクセス ポリシーに対してデバイス スコープまたはモジュール スコープのトークンを使用する方法についても概説しています。

コンテキストと問題

Azure IoT Hub セキュリティに関する最新のドキュメントでは、IoT Hub での SAS 認証 (Azure IoT クライアント SDK を使用した IoT デバイスによる認証) 用のサードパーティのトークン サーバー パターンについて説明していますが、 さらに明確に説明しなければ、Azure IoT クライアント SDK に既定で実装されるサポートのレベルについて、間違った印象を与える可能性があります。このことは、最近のエンタープライズ エンゲージメント中にお客様が間違った思い込みをしていることにも示されています。

この記事では、そのエンゲージメントから得られた教訓について説明し、デバイスでサードパーティのトークン サーバー認証を実現するために、各 SDK で何を実行する必要があるかを明確にしています。 この記事では、Azure IoT クライアント SDK でのサードパーティのトークン サーバー パターンのサポートについて、同様の間違った思い込みをしないようにしてください。

解決策

Azure IoT クライアント SDK では、SAS トークン認証にさまざまなレベルのサポートが提供され、認証とトークン管理機能を実行するためにカスタム コードがそれぞれ必要です。

トークンの評価頻度は、選択したトランスポート プロトコル (MQTT、AMQP、または HTTPS) によって異なります。 この差は、トークンとセッション タイムアウトのプロアクティブな更新をサポートするプロトコルの機能によって決まります。 プロアクティブな更新のサポートが実装されているのは AMQP のみです。 つまり、SAS トークン認証エラーが発生すると、他のトランスポートで接続が閉じられ、新しい接続操作を実行する必要があります。 これは、クライアントの接続操作にコストがかかる可能性があります。

SAS 認証が失敗すると、"接続の状態が変更されました" イベント ハンドラーによってデバイス アプリケーション内で処理できるトランスポート実装によってエラーが発生します。 このようなハンドラーを実装しない場合、通常、エラーが原因でデバイス アプリケーションが停止します。 イベント ハンドラーとトークンの更新機能を正しく実装すれば、トランスポートは接続を再試行できます。

次の図は、サードパーティのトークン サーバー パターンを示しています。

Illustration of the third-party token-server pattern

次の図は、Azure IoT クライアント SDK と Mobile Net Operator の統合の実装サポートを示しています。

Flowchart of implementation support in the Azure IoT client SDK with Mobile Net Operator integration

サンプルの実装は、GitHub の Azure サンプルに記載されています。

問題と注意事項

このパターンを実装するかどうかを決定する際は、以下の点を考慮してください。

  • Azure IoT Hub Device Provisioning Service (Azure DPS) クライアント SDK では、SAS トークン認証はサポートされていません。 Azure DPS REST API では、SAS トークン認証がサポート "されています"。 そのため、SAS 認証にサードパーティのトークン サービスで Azure DPS を使用するには、デバイス アプリケーションで Azure DPS REST API を使用してデバイス DPS プロセスを実装する必要があります。

  • これは、初期登録要求操作と、DPS プロセスが成功または失敗するまでの操作状態 API のポーリングで構成されます。 成功した場合は、Azure DPS REST API ランタイム登録に要求して、デバイス プロビジョニングの詳細を取得できます。

参考資料:

このパターンを使用する状況

さまざまな Azure IoT クライアント SDK を使用して IoT デバイスから Azure IoT Hub に対して認証を行う場合は、このパターンを使用する必要があります。 SAS トークン認証にクライアント SDK を使用する代わりに、Azure DPS REST API を使用して、すべてのトランスポート メカニズムに対するプロアクティブな更新サポートを確実に実装します。

以下のセクションでは、Embedded C、.NET、Java、Python など、さまざまなプログラミング言語に使用できる例を示します。

C 用 Azure IoT Hub デバイス SDK および Embedded C 用 Azure IoT Hub デバイス SDK

次のアプローチは、Azure IoT C SDK または Azure IoT Embedded C SDK を使用して構築されたデバイス アプリケーションで使用できます。 どちらの SDK でも SAS トークンの有効期間管理は提供されないため、SAS トークンの有効期間マネージャー機能を実装する必要があります。

SAS トークンは deviceSasToken メンバーをトークンに設定し、deviceKey を null にして、IOTHUB_CLIENT_CONFIG 構造体を介して使用できます。 protocolGatewayHostName などの他の未使用の値も null に設定する必要があります。

IOTHUB_CLIENT_CONFIG* CONFIG = (IOTHUB_CLIENT_CONFIG*)malloc(sizeof(IOTHUB_CLIENT_CONFIG));

CONFIG->PROTOCOL = PROTOCOL;
CONFIG->DEVICEID = DEVICEID;
CONFIG->IOTHUBNAME = IOTHUBNAME;
CONFIG->IOTHUBSUFFIX = IOTHUBSUFFIX;
CONFIG->DEVICEKEY = 0;
CONFIG->DEVICESASTOKEN = TOKEN;
CONFIG->PROTOCOLGATEWAYHOSTNAME = 0;

// The created IOTHUB_CLIENT_CONFIG can then be provided to the IoTHubDeviceClient_Create function to establish a DeviceClient instance.
if ((IOTHUBCLIENTHANDLE = IoTHubDeviceClient_Create(CONFIG)) == NULL) {
    (void)printf("ERROR: IOTHUBCLIENTHANDLE IS NULL!\r\n");
}

// To capture SAS token authentication failures, a handler needs to be implemented for the IoTHubDeviceClient_SetConnectionStatusCallback.
(void)IoTHubDeviceClient_SetConnectionStatusCallback(IOTHUBCLIENTHANDLE, CONNECTION_STATUS_CALLBACK, NULL);

connection_status_callback は、IOTHUB_CLIENT_CONNECTION_EXPIRED_SAS_TOKEN の IOTHUB_CLIENT_CONNECTION_STATUS_REASON をキャッチして、サードパーティのトークン サービスを介して SAS トークンの更新をトリガーすることができます。 これは、すべてのトランスポートで接続の問題をキャプチャするために必要ですが、特に、SAS トークンのプロアクティブな更新をサポートしていないトランスポートで必要です。 プロアクティブ SAS トークンの有効期間管理は、デバイス アプリケーションの "操作" ループ中に繰り返し実行される関数として実装できます。 トークンの有効期間が頻繁に評価され、必要に応じてトークンの更新をプロアクティブに実行できるようにします。

C SDK の SAS トークン認証の実装の概要:

  1. ConnectionStatusCallback ハンドラーを実装して、IOTHUB_CLIENT_CONNECTION_EXPIRED_SAS_TOKEN イベントをキャプチャし、トークンの更新をトリガーします。

  2. IOTHUB_CLIENT_CONFIG を使用して、デバイス SAS トークンを IoTHubDeviceClient_Create に指定します。

  3. デバイス アプリケーションの操作ループの一部として、プロアクティブ SAS トークンの有効期間管理を実装します。

.NET 用 Azure IoT Hub デバイス SDK

.NET 用 Azure IoT クライアント SDK では、DeviceAuthenticationWithTokenRefresh 抽象クラスを使用した SAS トークンの有効期間管理のサポートが実装されています。 このクラスの具体的な実装は、トークンの更新機能を追加して、認証方法として DeviceClient.Create メソッドに指定できます。 トランスポート実装では、必要に応じて認証方法を使用してトークンが自動的に更新されます。 接続の変更をキャプチャし、トランスポートによって例外が発生するのを防ぐには、ConnectionStatusChangesHandler が必要です。

DeviceAuthenticationWithTokenRefreash クラスに基づく実装例:

internal class StsDeviceAuthenticationWithTokenRefresh : DeviceAuthenticationWithTokenRefresh
{

    private readonly string _stsConnectUrl = "http://localhost:8080/sts/azure/token/operations?sr={0}/devices/{1}";

    private const int DEFAULTTIMETOLIVESECONDS = 1 * 60 * 60;

    private const int DEFAULTBUFFERPERCENTAGE = 15;

    public StsDeviceAuthenticationWithTokenRefresh(string deviceId, int suggestedTimeToLiveSeconds, int timeBufferPercentage) : BASE(deviceId, suggestedTimeToLiveSeconds, timeBufferPercentage)
    {
        If(String.IsNullOrWhitespace(deviceId)){
            throw new ArgumentNullException(nameof(deviceId));
        }
    }

    protected override async Task<string> SafeCreateNewToken(string iotHub, int suggestedTimeToLive)
    {
        string result;
        string url = string.Format(_stsConnectUrl, iotHub, deviceId);

        using (HttpClientHandler handler = new HttpClientHandler())
        using (HttpClient client = new HttpClient(handler))
        {
            try
            {
                HttpResponseMessage response = await client.GetAsync(url);
                if (response.IsSuccessStatusCode)
                {
                    result = await response.Content.ReadAsStringAsync();
                }
                else
                {
                    throw new HttpRequestException($"Request failed with status code {response.StatusCode}.");
                }
            }
            catch (HttpRequestException)
            {
                result = null;
            }
        }

        return result;
    }
}

.NET 用 Azure IoT Hub デバイス SDK の SAS トークン認証の実装の概要:

  1. トークンの更新機能を実装する DeviceAuthenticationWithTokenRefresh 抽象クラスに基づいて、具象クラスを実装します。

  2. ConnectionStatusChangesHandler を実装して、トランスポート接続の状態をキャプチャし、トランスポート実装によって発生する例外を回避します。

参考資料:

Java 用 Azure IoT Hub デバイス SDK

Java 用 Azure IoT クライアント SDK では、SasTokenProvider インターフェイスを使用した SAS トークンの有効期間管理のサポートが実装されています。 SAS トークンの更新機能を使用してこのインターフェイスを実装するクラスは、DeviceClient コンストラクターで SecurityProvider として使用できます。 トランスポート実装では、必要に応じてセキュリティ プロバイダーを使用してトークンが自動的に更新されます。 接続の変更をキャプチャし、トランスポートによって例外が発生するのを防ぐには、ConnectionStatusChangeCallback を登録する必要があります。

SasTokenProvider インターフェイスを実装するセキュリティ プロバイダーの実装例:

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class StsSecurityProvider implements SasTokenProvider {
    private final String hostname;
    private final String deviceId;
    private int renewalBufferSeconds;
    private long expiryTimeSeconds;
    private char[] sasToken;

    public StsSecurityProvider(String hostname, String deviceId) {
        this.hostname = hostname;
        this.deviceId = deviceId;
        this.renewalBufferSeconds = 120;
        this.expiryTimeSeconds = (System.currentTimeMillis() / 1000);
    }

    @Override
    public char[] getSasToken() {
        long currentTimeSeconds = (System.currentTimeMillis() / 1000);
        try {
            if (this.sasToken == null || this.expiryTimeSeconds + this.renewalBufferSeconds >= currentTimeSeconds) {
                this.sasToken = stsGetToken();
                assert this.sasToken != null;
                String t = String.copyValueOf(this.sasToken);
                String[] bits = t.split("SE=");
                long l = Long.parseLong(bits[1]);
                this.expiryTimeSeconds = l; // the SE= number
                this.renewalBufferSeconds = (int)(l * 0.15); // renew within 15% of expiry
            }
        } catch (InterruptedException | IOException e) {
            e.printStackTrace();
        }
        return this.sasToken;
    }

    private char[] stsGetToken() throws IOException, InterruptedException {
        String stsUrl = String.format("http://localhost:8080/sts/azure/token/operations?sr=%s/devices/%s", this.hostname, this.deviceId);
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(stsUrl))
            .timeout(Duration.ofMinutes(2))
            .header("Content-Type", "application/json")
            .build();
        HttpClient client = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_1_1)
            .connectTimeout(Duration.ofSeconds(20))
            .build();
        HttpResponse < String > response = client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            return null;
        }
        if (response.body().isEmpty()) {
            return null;
        }
        return response.body().toCharArray();
    }
}

Java 用 Azure IoT Hub デバイス SDK の SAS トークン認証の実装の概要:

  1. クラスに SasTokenProvider インターフェイスを実装し、トークンの更新機能を含めます。

  2. ConnectionStatusChangeCallback ハンドラーを実装して、トランスポート接続の状態をキャプチャし、トランスポート実装によって発生する例外を回避します。

参考資料:

Python 用 Azure IoT Hub デバイス SDK

Python 用 Azure IoT Hub デバイス SDK では、IoTHubDeviceClient オブジェクトのメソッドを使用して SAS トークンのサポートが実装されます。 これらのメソッドでは、トークンを使用してデバイス クライアントを作成し、デバイス クライアントの作成後に更新されたトークンを指定できます。 トークンの有効期間管理は実装されませんが、非同期操作として簡単に実装できます。

機能の概要を示す Python 3.7 の実装例:

import asyncio
import iothub_device_client

async def main():
    # Get a SAS token you generated
    sastoken = get_new_sastoken()
    # The client object is used to interact with your Azure IoT Hub.
    device_client = iothub_device_client.create_from_sastoken(sastoken)

    # Connect the client
    await device_client.connect()

    # Define behavior for providing new SAS tokens to prevent expiry
    async def sastoken_keepalive():
        while True:
            await asyncio.sleep(new_token_interval)
            sastoken = get_new_sastoken()
            await device_client.update_sastoken(sastoken)

    # Also run the SAS token keepalive in the event loop
    keepalive_task = asyncio.create_task(sastoken_keepalive())

    # Cancel the SAS token update task
    keepalive_task.cancel()

    # Finally, shut down the client
    await device_client.shutdown()

if __name__ == "main":
    asyncio.run(main())

Python 用 Azure IoT Hub デバイス SDK による SAS トークン認証の概要:

  1. SAS トークン生成関数を作成します。

  2. IoTHubDeviceClient.create_from_sastoken を使用して、デバイス クライアントを作成します。

  3. トークンの有効期間を別のアクティビティとして管理し、IoTHubDeviceClient.update_sastoken メソッドで必要になる場合、更新されたトークンをデバイス クライアントに指定します。

参考資料:

Node.JS/JavaScript 用 Azure IoT Hub デバイス SDK

Node.JS/JavaScript 用 Azure IoT Hub デバイス SDK では、デバイス クライアントに SAS トークンを提供し、IoT Hub で認証するためにトランスポートを提供する SharedAccessSignatureAuthenticationProvider を実装します。 トークンの更新機能は実装されません。 デバイス アプリケーションでは、トークンの有効期間を管理し、必要に応じてトークンを更新する必要があります。

デバイス クライアント メソッド fromSharedAccessSignature および updateSharedAccessSignature を使用して、IoT Hub との接続を開始し、更新されたトークンを SharedAccessSignatuteAuthenticationProvider に指定します。これにより、認証プロバイダーが newTokenAvailable イベントをトランスポートに出力します。

基本的な SAS トークンのサンプルは、simple_sample_device_with_sas.js の例に記載されています。

Node.JS/JavaScript 用 Azure IoT Hub デバイス SDK の概要:

  1. SAS トークンの有効期間の管理と更新を実装します。

  2. デバイス クライアント fromSharedAccessSignature を使用して、デバイス クライアント インスタンスを構成します。

  3. デバイス クライアント updateSharedAccessSignature を使用して、更新されたトークンを指定します。

参考資料:

次の手順