Azure Web PubSubのnegotiateをPHPで実装してみる

最近書いているとあるサービスでリアルタイム更新をしたいと思い、Azure SignalR ServiceAzure Web PubSubを試してます。

クイックスタートを参考にすれば、Azure Functionsで割と簡単に動作を試せます。

今回のサービスのバックエンドAPIPHPで書かれているため、 negotiate の処理をPHPAPIで実施したいと考えていたのですが、残念ながらAzure PubSubのPHP SDKは現時点で提供されていません。(多分この先も提供はされなそう... (;_; )

ということで、Azure Web PubSub service client library for JavaScript を参考に、negotiate が何をしているか調べてみました。

調べた結果、negotiate のレスポンスは以下のような内容になっていました。

{
  baseUrl: 'wss://[PubSubName].webpubsub.azure.com/client/hubs/[hubname]',
  token: 'JWT token',
  url: 'wss://[PubSubName].webpubsub.azure.com/client/hubs/[hubname]?access_token=[JWT Token]'
}

ふむふむ、JWTで認証しているよう...。生成されるJWTの中身は以下の様な内容でした。

{
  "header": {
    "typ": "JWT",
    "alg": "HS256"
  },
  "claims": {
    "iat": 1623618349,
    "exp": 1623621949,
    "aud": "https://[PubSubName].webpubsub.azure.com/client/hubs/[hubname]"
  },
  "signature": "sigunature...",
  "raw": "eyJ0eXAiOiJ..."
}

要は、接続文字列からこのJWTを生成できれば良さそうです。ということで、gree/joseを使ってざくっと書いてみたのが以下。

<?php
declare(strict_types=1);

class PubSubToken {
    protected $endpoint;
    protected $wssEndpoint;
    protected $accesskey;
    protected $version;
    protected $alg = 'HS256';

    public function __construct($connectionString)
    {
        $params = explode(';', $connectionString);
        foreach ($params as $param) {
            list($k, $v) = explode('=', $param, 2);

            $this->{strtolower($k)} = $v;
        }

        $this->wssEndpoint = preg_replace('/(http)(s?:\/\/)/i', 'ws$2', $this->endpoint);

        if ($this->endpoint === null || $this->accesskey === null || $this->version === null || $this->wssEndpoint === null) {
            throw new \Exception('Parameter error');            
        }
    }

    public function getAuthenticationToken(string $hub, string $userId = null, int $ttl = 3600): array
    {
        $now = time();

        $payload = [
            'iat' => $now,
            'exp' => $now + $ttl,
            'aud' => "{$this->endpoint}/client/hubs/{$hub}",
        ];
        if ($userId !== null) {
            $payload['sub'] = $userId;
        }

        $jwt = new \JOSE_JWT($payload);
        $jwt->header['alg'] = $this->alg;
        $jwt->header['typ'] = 'JWT';
        $jwt->sign($this->accesskey, $this->alg);
        
        $jws = new \JOSE_JWS($jwt);
        $jws = $jws->sign($this->accesskey, $this->alg);
        $token = $jws->toString();
        
        return [
            'baseUrl' => "{$this->wssEndpoint}/client/hubs/{$hub}",
            'token' => $token,
            'url' => "{$this->wssEndpoint}/client/hubs/{$hub}?access_token={$token}",
        ];
    }
}


$pubsub = new PubSubToken('Azure WebPubSubの接続文字列');
$token = $pubsub->getAuthenticationToken('test');

このtoken を使って無事subscribeできました。 ということで、このtokenをAPIで返してあげれば、クライアント側でsubscribeできそうです。

コードは、gistにも上げておきました。