LoginSignup
0
1

More than 1 year has passed since last update.

【C#】Firebase Admin SDKなしでFCMのHTTP v1 APIでプッシュ送信してみた

Last updated at Posted at 2022-04-27

はじめに

スマートフォンなどへプッシュ送信のテストがコンソール画面からお手軽にできるFCM(Firebase Cloud Messaging)ですが、サーバー側もテスト実装したい!となった場合、Firebase Admin SDKを使用すると、これまたお手軽に.NET + C#でWindowsPCでも作れちゃいます。

ところが、組込機器をサーバーにしてプッシュ送信したい、となるとFirebase Admin SDKが適用できるようなリッチ環境ではないですよね。でも、HTTPSならしゃべれるからHTTP v1 APIを使えばできんじゃね?というのが本件の背景です。
開発環境はVisual Studio 2022の.NET6 + C#ですが、組込みでも暗号化ライブラリがあればいけます。

手順

公式ドキュメントに書いてはありますが、ざっくり以下にまとめ。

  1. JSON WebToken(JWT)を作成する
  2. Google OAuth2.0アクセストークンをリクエストする
  3. JSON応答からOAuth2.0アクセストークンを抽出する
  4. App Instance Tokenをtopicに紐づける(Subscribed)※必要なら
  5. App instance Tokenをtopicから削除する(Unsubscribed)※必要なら
  6. topicにメッセージを送信する

ちなみにHTTP v1 APIを使用しているのは 6. だけです。token宛てにメッセージ送信するなら 6. だけでいいんですけど、topic宛てにメッセージ送信するとなると 4.(Subscribed)と 5.(Unsubscribed)が必要でレガシーHTTP APIを使用するしかなさそうです。
GoogleではレガシーHTTP APIからHTTP v1 APIへの移行を推奨しているのに、レガシーHTTP APIでしかtopic登録削除できないなんてどうなのこれ。
なお、Firebase Admin SDKを使えばもちろんHTTP v1 APIでtopic登録削除ができます。

JSON WebToken(JWT)を作成する

公式ドキュメントのまんまです。がんばって作っていきまっしょい。

JWT = {Base64url encoded header}.{Base64url encoded claim set}.{Base64url encoded signature}
  • header = {"alg":"RS256","typ":"JWT"}
  • claim set = {
    "iss": "FCMプロジェクトのFirebaseサービスアカウント",
    "scope": "https://www.googleapis.com/auth/firebase.messaging",
    "aud": "https://oauth2.googleapis.com/token",
    "exp": 有効期限(最大値は発行時刻から1時間後),
    "iat": 発行時刻のUNIX時間
    }
  • signature = {header}.{claim set}のSHA256ハッシュをRSASSA-PKCS1-v1_5秘密鍵で署名

scopeはfirebase.messagingを設定してね。
Base64urlエンコードはBase64エンコードをパディング(=)なしでURLアンセーフ文字( + と / )をURLセーフ文字( - と _ )に置き換えしたもの。
headerとclaim setはこんなかんじになります。

var headerStr = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
var headerBytes = Encoding.UTF8.GetBytes(headerStr);
var headerBase64url = Convert.ToBase64String(headerBytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
var ts = DateTimeOffset.Now.ToUnixTimeSeconds();
var claimStr = "{\"iss\":\"firebase-adminsdk-****@プロジェクト名.iam.gserviceaccount.com\"," +
               "\"scope\":\"https://www.googleapis.com/auth/firebase.messaging\"," +
               "\"aud\":\"https://oauth2.googleapis.com/token\"," +
              $"\"exp\":{ts+60*60}," +
              $"\"iat\":{ts}" +
               "}";
var claimBytes = Encoding.UTF8.GetBytes(claimStr);
var claimBase64url = Convert.ToBase64String(claimBytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');

あとはsignatureです。
署名生成にあたり.NETでRSACryptoServiceProviderにRSA秘密鍵をセットしたいんですが、これXML形式しか読み込まねぇという素敵仕様なんです。仕方がないので下記サイトで手動変換しましょう。

RSA秘密鍵はFCMコンソールから [プロジェクトの設定] > [サービスアカウント] > [新しい秘密鍵の生成] でダウンロードしたjsonファイル内の "private_key" です。
これがPEM形式なのでXML形式にコンバートします。文字列から \n を削除して -----BEGIN PRIVATE KEY----- と -----END PRIVATE KEY----- だけ改行しておけばサイト側でマーカーとして認識するので、PEM to XMLにコピペしてConvert。
変換出力された文字列をRSACryptoServiceProviderに放り込みましょう。

var xml = "XML形式の秘密鍵文字列";
var rsa = new RSACryptoServiceProvider();
rsa.FromXmlString(xml);
var token = headerBase64url + "." + claimBase64url;
var tokenBytes = Encoding.UTF8.GetBytes(token);
var tokenHash = SHA256.Create().ComputeHash(tokenBytes);
var rsaFormatter = new RSAPKCS1SignatureFormatter(rsa);
rsaFormatter.SetHashAlgorithm("SHA256");
var signBytes = rsaFormatter.CreateSignature(tokenHash);
var signBase64url = Convert.ToBase64String(signBytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
var jwt = token + "." + signBase64url;

JWTさえ作ればあとは簡単です。

Google OAuth2.0アクセストークンをリクエストする

JWTをPOSTしましょう。とりあえずこんなかんじで。

var httpClient = new HttpClient();
var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
request.RequestUri = new Uri("https://oauth2.googleapis.com/token");
var contentBody = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" + jwt;
request.Content = new StringContent(contentBody);
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
var response = httpClient.Send(request);

JSON応答からOAuth2.0アクセストークンを抽出する

responseからアクセストークンゲット。

if (response.IsSuccessStatusCode) {
    var resStr = response.Content.ReadAsStringAsync().Result;
    var keyStr = "\"access_token\":";
    var startIndex = resStr.IndexOf(keyStr);
    var endIndex = resStr.IndexOf(',', startIndex);
    accessToken = resStr.Substring(startIndex + keyStr.Length, endIndex - startIndex - keyStr.Length).Replace("\"", string.Empty);
}
else {
    Debug.WriteLine($"アクセストークンの取得に失敗しました... {response.StatusCode}");
    Debug.WriteLine($"{response.Content.ReadAsStringAsync().Result}");
}

App Instance Tokenをtopicに紐づける/削除する

topicのSubscribed/Unsubscribedは、レガシーHTTP APIでしかアクセスする方法が見つかりませんでした。レガシーHTTP APIではサーバーキーで認証するため、最大1時間有限のOAuth2.0アクセストークン認証より漏洩リスク大ですがどうすれば。たすけてドラえもん。

ドラえもんは助けてくれないので、レガシーHTTP APIを使うことにしましょう。サーバーキーを使って対象のtokenをPOSTしてあげればいいので、こんなかんじ。足りない部分は適宜補完を。
削除もURI以外は同じ。

List<string> registrationTokens = new List<string>();

registrationTokens.Clear();
/* フォームから対象のtoken文字列をセット */
for (var i = 0; i < TOKEN_NUM; i++) {
    if (checkBoxes[i].Checked) {
        var token = textBoxes[i].Text;
        if (token != String.Empty)
            registrationTokens.Add(token);
    }
}

if (registrationTokens.Any()) {
    var httpClient = new HttpClient();
    var request = new HttpRequestMessage();
    request.Method = HttpMethod.Post;
    request.RequestUri = new Uri("https://iid.googleapis.com/iid/v1:batchAdd");
    request.Headers.TryAddWithoutValidation("Authorization", "key=" + serverKey);
    var contentBody = "{" +
                     $"\"to\": \"/topics/{TOPIC}\"," +
                      "\"registration_tokens\": [";
    foreach(var token in registrationTokens) {
        contentBody = contentBody + "\"" + token.ToString() + "\",";
    }
    contentBody = contentBody + "],}";
    request.Content = new StringContent(contentBody);
    request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
    var response = httpClient.Send(request);
}

topicにメッセージを送信する

OAuth2.0アクセストークンを使ってPOSTします。

var httpClient = new HttpClient();
var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
request.RequestUri = new Uri("https://fcm.googleapis.com/v1/projects/my-push-test-notification/messages:send");
request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + accessToken);
var contentBody = "{ 公式ドキュメントに沿って送りたい内容を書いてください }";
request.Content = new StringContent(contentBody);
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = httpClient.Send(request);

まとめ

こんなかんじなので、組込機器からでもSHA256ハッシュの生成とRSA秘密鍵でのPKCS#1 v1.5署名さえできればFCMでプッシュ送信できます。本来はFirebase Admin SDKを使うべきなんでしょうけれども。
どなたかの参考になれば幸いです。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1