はじめに
こんにちは。STORES ブランドアプリで Android エンジニアをしている Yuto Koguchi (@10llip0p) です。
STORES では現在 ID 基盤の統一に取り組んでおり、複数のプロダクトへ共通のアカウントでログインしてシームレスに利用できる体験を目指しています。
私が担当するブランドアプリにおいても今年からこの新しい ID 基盤への移行に取り組んできました。
STORES の ID 基盤とセキュリティ観点
STORES では Open ID Connect (OIDC) に準拠した ID 基盤を自前実装しており、様々なプロダクトやサービスに導入を進めています。
OIDC (もとい OAuth)では認可リクエストを送るクライアントと実際に認可するクライアントの同一性を保証するため、認証・認可のフローの中でstate
というパラメーターを付与・検証することで CSRF を防ぐ仕組みが使われています。またstate
の有効性を担保するために Session と紐づけてやりとりすることが推奨されています。
https://openid-foundation-japan.github.io/rfc6819.ja.html#anchor15
https://openid-foundation-japan.github.io/rfc6819.ja.html#link_state_uasession
ブランドアプリでの対応
OIDC に従ってブランドアプリでアクセストークンを取得するまでのざっくりとした流れは以下のようになります。(あくまでアプリから見た処理や通信の流れのみで OIDC の詳細なフローは省いています)
- ① API サーバーに認可リクエストの URL を取得するリクエストを送る
- ② 認可リクエストの URL を返す
- ③ Android Custom Tabs で認可リクエストの URL を開く
- ④ 認証完了後 DeepLink でブランドアプリに認可レスポンスをコールバックする
- ⑤ DeepLink から取得した認可コードを使ってアクセストークンをリクエストする
- ⑥ アクセストークンを返す
この際に先述のセキュリティ観点を考慮すると、①と⑥のリクエスト・レスポンスで同一の Session が共有されている必要があります。
OkHttp
の Cookie 管理
Android アプリの一般的な HTTP クライアントであるOkHttp
はデフォルトで Cookie 管理の仕組みを持っていません。WebView や Custom Tabs からアクセスする場合は特に考慮は必要ないのですが、今回のようにアプリからの HTTP リクエスト間でセッションを共有する場合には Cookie 管理の仕組みを追加する必要があります。
OkHttp
で Cookie を使うための実装(CookieJar)は公式からJavaNetCookieJar
というライブラリが公開されており導入はとても簡単です。JavaNetCookieJar
はokhttp3:okhttp-urlconnection
にバンドルされているため、依存関係を追加した上でOKHttpClient.Builder()
に下記のような実装を追加することで Cookie 管理の仕組みを構築できます。
OKHttpClient.Builder().apply {
val cookieManager = CookieManager()
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER)
cookieJar(JavaNetCookieJar(cookieManager))
}
ちなみにokhttp3
v5.0.0 以降ではJavaNetCookieJar
は 独立したライブラリとして公開されるようです。 https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp-java-net-cookiejar
JavaNetCookieJar
を使う実装の問題と解決策の調査
JavaNetCookieJar
を使うことで Session を共有する仕組みは実装完了!めでたし!といきたかったのですが、動作確認をしているとアクセストークンをリクエストする際に Session が消失している挙動が高頻度で発生することがわかりました。
この問題はブランドアプリの構造に起因しているのか、あるいは他に要因があるのか根本的な原因はわかっていません。ただJavaNetCookieJar
は Cookie をメモリ上で管理するため、Custom Tabs に遷移する際に Cookie の保持に失敗していたり、あるいは DeepLink でアプリに復帰する際に意図せず揮発してしまったり、などのことが起きているのではないかと疑いました。
そこで Cookie をSharedPreferences
に保存して永続化する戦略を検証してみることにしました。この方法に則った実装としてはPersistentCookieJar
というライブラリが OSS で公開されており、JavaNetCookieJar
と同様に簡単に仕組みを構築することができます。
https://github.com/franmontiel/PersistentCookieJar
そしてPersistentCookieJar
を使用して再度動作確認を行ったところ、Session が消失する問題は再現しなくなったため Cookie の永続化が有効なことがわかりました。
Cookie 永続化の実装
これらのことからブランドアプリの ID 基盤移行にあたっては、 Cookie の永続化を考慮した実装を行うことにしました。
Cookie 永続化の方法としては上記のPersistentCookieJar
を使った実装が最も手軽です。ただ PersistentCookieJar
は2025年時点で最新の commit が8年前であり、また 100% Java で実装されていることなどから今からプロダクションに導入するのは少々気が引けます。
そこでSharedPreferences
を使った Cookie 永続化の仕組みを持つ CookieJar を自分で実装することにしました。これはPersistentCookieJar
の実装を読んだ上でそれほど複雑な実装をしていなかったということや、現代においては AI の力を借りることで実装の難易度は高くないだろうということを考えての判断になります。
実装した CookieJarCustomCookieJar
のソースコードをほぼ原文ママで紹介します。
class CustomCookieJar( context: Context, ) : CookieJar { private val prefFacade = CookiePrefFacade(context) private val cookieCache = mutableMapOf<String, List<Cookie>>() override fun loadForRequest(url: HttpUrl): List<Cookie> { synchronized(this) { val cookies = cookieCache[url.host] ?: prefFacade.loadCookies(url).also { if (it.isNotEmpty()) { cookieCache[url.host] = it } } val validCookies = cookies.mapNotNull { cookie -> cookie.takeIf { it.expiresAt > System.currentTimeMillis() } } if (cookies != validCookies) { saveCookies(url, validCookies) } return validCookies } } override fun saveFromResponse( url: HttpUrl, cookies: List<Cookie>, ) { saveCookies(url, cookies) } private fun saveCookies( url: HttpUrl, cookies: List<Cookie>, ) { synchronized(this) { cookieCache[url.host] = cookies prefFacade.saveCookies(url, cookies) } } fun clearCache() { cookieCache.clear() prefFacade.clear() } }
CookiePrefFacade
は SharedPreferences にアクセスするためのデータソースです。詳細な実装の説明は割愛しますが、保存用のsaveCookies
と読み出し用のloadCookies
の2つのインタフェースを介して、Key-Value の形式で URL (host) と Cookie の対を読み書きする実装となっています。
Cookie はメモリ上にもキャッシュしており、リクエスト時に検証して必要に応じてセットする仕組みとしています。
またclearCache()
を実行することでログアウト等の任意のタイミングで Cookie をパージする機構を持たせています。すなわちCustomCookieJar
はシングルトンなインスタンスとして利用することを想定した設計となります。
CustomCookieJar
を使って動作確認をしたところ、PersistentCookieJar
と同様に問題は再現せず現状は安定して動作しています。またJavaNetCookieJar
やPersistentCookieJar
と比較して必要最小限のコンパクトな実装で済んだことから、メンテナンスの面でも特に懸念や不安がなく自作したメリットを感じています。
最後に
今回は STORES ブランドアプリの ID 基盤移行の取り組みにあたり、Cookie 永続化の仕組みの実装を紹介しました。
ID 基盤の統一によりますます便利につながっていく STORES のプロダクトにぜひご期待ください。