Azure SignalR Service の REST API では URL区切り文字を含んだユーザーIDがあて先のメッセージを送れない

Azure SignalR Service は serverless モードにして Azure Functions と使うことができる。その場合には Azure Functions から REST API1 で Azure SignalR Service へ メッセージを発行する。 現時点(2019/12/29)の Azure SignalR Service では URL区切り文字を含んだユーザーIDが有効である。しかし、REST API から URL区切り文字を含んだユーザーIDをあて先としたメッセージ送信ができない。 URL区切り文字を含んだユーザーIDとは例えば、"user/1"のような文字列。

原因は、Azure SignalR Service が REST API のパラメーターとして送られた URLエンコードされたユーザーIDをデコードせずにあて先ユーザーIDとするから。

ユーザーIDが user/1 の場合、REST API へ URL {prefix}/user%2F1 としてリクエストするとメッセージがユーザーIDが user%2F1 のクライアントへメッセージが送信される。 URLエンコードをせずに REST API へ URL {prefix}/user/1 としてリクエストすると、URL が API のルートに一致せずに 404 Not Found レスポンスが返る。

なお、この REST API では リクエスト URL は ヘッダーで送信する JWT の aud claim と一致する必要がある。ユーザーIDを URLエンコードした場合であっても、aud claim はエンコード前の文字列と一致して有効な API リクエストとして扱われる。

Azure SignalR Service へは、REST API 以外に websocket で接続してメッセージ送信することができる。その場合にはユーザーID に / スラッシュなどの URL区切り文字が含まれていてもメッセージのあて先にできる。

接続したクライアントのユーザーID は Hub の接続イベントで確認できる。Azure SignalR Service では EventGrid を通して Hub への connected イベントが購読できる。

ためしたこと

Azure SignalR Service SDK のコードを変更して Azure SignalR Service へ接続して試してみた。Microsoft.Azure.SignalR.Management.ServiceTransportType.Transient を指定した ServiceManager が REST API を使用する。API への HTTP リクエスト URL を組み立てるコードを変更して、ユーザーID を URLエンコードして実行した。

public RestApiEndpoint GetSendToUserEndpoint(string userId, TimeSpan? lifetime = null)
{
    var path_token = $"/users/{userId}";

    // user/1 -> user%2F1
    var path_url = $"/users/{Uri.EscapeDataString(userId)}";
    
    var token = _restApiAccessTokenGenerator.Generate($"{_audiencePrefix}{path_token}", lifetime);
    return new RestApiEndpoint($"{_requestPrefix}{path_url}", token);
}

https://github.com/Azure/azure-signalr/blob/c0875fcb8c90befb8dc07d69a826cb66219127de/src/Microsoft.Azure.SignalR.Management/RestApiProvider.cs#L44-L47

  • "user/1"だとする
  • Connected イベントでは、"user/1"
  • access_token.id では、 "{prefix}/user/1"
  • rest api
    • url = {prefix}/user/1 , token.aud = "{prefix}/user/1"
    • url = {prefix}/user/1 , token.aud = "{prefix}/user%2F1"
    • url = {prefix}/user%2F1 , token.aud = "{prefix}/user%2F1"
      • client "user%2F1" でメッセージを受信した
    • url = {prefix}/user%2F1 , token.aud = "{prefix}/user/1"
      • client "user%2F1" でメッセージを受信した