なまえは まだ ない

思いついたことをアウトプットします

Sign In with Apple REST APIをGoで扱うためのライブラリを作っている話

この記事はフラー株式会社 Advent Calendar 2022の17日目の記事です。16日目は@Taip00nさんで「ターミナルでスターウォーズを見るべ」でした。

12日目と思いっきりネタ被りしてしまいました。氏が扱ってる言語が違うので許してください。

nnsnodnb.hatenablog.jp

Appleが定めるアカウント削除に関する要件について

さて、ご存知の方も多いと思いますが、2022年6月30日より新しいApp Store Reviewガイドラインが適用されました。新しいガイドラインでは、ユーザーからの申し出によってアカウントを削除する際の要件が追加されています。

developer.apple.com

色々気になる部分はありますが、今回特に気にしたい点は次の項目です。

Appleでサインインに対応したAppでは、AppleでサインインのREST APIを使用して、アカウントの削除時にユーザーのトークンを無効化する必要があること。

つまりSign In with Appleで連携しているユーザーがアカウント削除を申し出たとき、ユーザーデータを削除するだけでなく、Appleが提供するREST APIをリクエストして連携情報の無効化をしなくてはいけないようです。具体的に実装すべき機構の詳細は一旦置いておきますが、まずはこのREST APIをどうにか相手にする方法が必要です。弊社サーバーサイドはそのほとんどをGoで記述しているので、今回はGoでSign In with Apple REST APIをリクエストするためのクライアントを開発してみました。

Sign In with Apple REST API Client for Go の紹介

開発しているライブラリはGitHubにて公開しています。後述する事情によりまだ安定版とは言えないのでバージョンタグは切ってません。

github.com

使い方

※ この情報は執筆時点のものであり、今後のアップデートにより変更される可能性があります。

クライアントの初期化

まずはREST APIをリクエストするクライアントを初期化します。 クライアントを初期化するにはApple Developer Programからダウンロードできる、アプリや開発者に関するいくつかの情報が必要です。 初期化のためのパラメータが多くて辟易してしまいますが、こういうもんなんで諦めてください。 具体的な値についてはお近くのiOSアプリエンジニアにお問い合わせください。

client, err := siwarest.New(&siwarest.ClientConfig{
    Client:        http.DefaultClient,
    ClientID:      "client-id",
    KeyID:         "key-id",
    TeamID:        "team-id",
    PrivateKeyPEM: "private-key",
})
if err != nil {
    // TODO: error handling
}

トークンを取得する

Generate and validate tokens APIをリクエストしてリフレッシュトークンを入手するには、GenerateAndValidateTokens関数を実行します。

token, err := client.GenerateAndValidateTokens(
    ctx,
    siwarest.GenerateAndValidateTokensWithAuthorizationCode("auth-code"),
)
if err != nil {
    // TODO: error handling
}

引数に渡している認可コードはiOSアプリがSign In with Appleを実施した際に入手できる、比較的有効期限の短い情報です。 アプリ側でサインインが発生した直後、この認可コードをサーバーにPOSTし、サーバーサイドでこのAPIをリクエストしてリフレッシュトークンを入手するような実装を想定しています。

引数にはリフレッシュトークンも渡すことができます。サーバーで保存しているリフレッシュトークンが無効になっていないかチェックするために利用できます。

token, err := client.GenerateAndValidateTokens(
    ctx,
    siwarest.GenerateAndValidateTokensWithRefreshToken("refresh-token"),
)
if err != nil {
    // TODO: error handling
}

トークンを無効化する

この記事の本題です。 RevokeTokens 関数に対してリフレッシュトークンを渡すことでRevoke tokens APIをリクエストしてくれます。

err := client.RevokeTokens(
    ctx,
    siwarest.RevokeTokensWithRefreshToken("refresh-token"),
)
if err != nil {
    // TODO: error handling
}

引数にはGenerate and validate tokens APIをリクエストすることで入手したリフレッシュトークンを渡します。

工夫した点

Client Secretのリフレッシュ

このREST APIを利用する上で一番めんどくさいなと感じたのがClient Secretの取り扱いです。

ドキュメントによるとAPIリクエストに必要なClient Secretは利用者が自分でJWTを構築しなくてはいけません。 また、構築するJWTには有効期限が含まれているので、おそらく利用者自身で定期的なJWTのリフレッシュをかけなくてはなりません。

developer.apple.com

今回開発したライブラリでは、Client Secretの構築から有効期限の監視、リフレッシュまでを内部的に処理するようにしました。 ユーザーが適切な初期パラメータを渡してClientを初期化すれば、それ以降の利用でClient Secretの存在を意識しなくても良いような実装になってます。

http.Client の外部注入

APIリクエストの際に利用する *http.Client をClient初期化時のパラメータとして含められるようにしました。 通常利用の範囲であれば http.DefaultClient を使えば良いですが、特定環境下においてHTTPクライアントをカスタマイズしたい*1という要望はそれなりにあると考え、このような実装にしています。

Functional option patternの採用

対象にしているREST APIは、例えばGenerate and validate tokens APIでは認可コードまたはリフレッシュトークンを、Revoke tokens APIではアクセストークンまたはリフレッシュトークンをリクエストパラメータとして受け付けます。こういうパターンのパラメータを引数として受け付ける場合、手っ取り早い方法としては次のようにそれぞれのパラメータをポインターにした構造体を用意し

type GenerateAndValidateTokensInput struct {
    AuthorizationCode *string
    RefreshToken      *string
}

関数の中でnilチェックをして条件分岐するというのがあると思います。

if input.AuthorizationCode != nil {
    // 認可コードを利用したリクエストと見なす
}
if input.RefreshToken != nil {
    // リフレッシュトークンを利用したリクエストと見なす
}

しかしこの場合、認可コードとリフレッシュトークンが両方nilだった場合認可コードとリフレッシュトークンが両方渡された場合の挙動を考えなくてはいけません。前者は明らかなエラーとして扱えますが、後者の場合はどうでしょう?どちらかを優先すべきか、エラーとして落としてしまうか、実装者や利用者の意図・利用シーンによって使い勝手が変わってきそうですね。

こういった事を考えずにいずれか一方だけ指定できるインターフェースを実現したかったので、今回はFunctional option patternを採用しました。

イマイチな点

クライアントを初期化するときに必須パラメータとして *http.Client を要求するのはちょっとイマイチだったかなと思っています。 ここをカスタムするかはオプションとして実装者の裁量に委ねても良かったかもしれません。 また、Client Secretに設定する有効期限が現在ライブラリ内で固定値になっています。これも要件によってはカスタマイズしたい場合があるかもしれないので、今後ここもオプションとして渡せるようにするかもしれません。

あとは何より、まだ一回も動作確認できてないという点ですね!僕の周りでなんだかんだこのガイドラインに対応する機会がないのです。。

というわけで人柱絶賛募集中です!!!

まとめ

Sign In with Apple REST APIをGoで扱うためのライブラリを作成しました。

人柱、Pull Requestを絶賛募集しております。

関連リンク


次回は@ujikawa1026で「エンジニアリングマネージャーとしての今年の学びをまとめます」です。お楽しみに。

*1:例えば、弊社ではAWS X-Rayでトレースをするためにshogo82148/aws-xray-yasdk-goを利用しており、このライブラリがHTTPクライアントをカスタマイズします