本文へ移動

ホーム / App / 第4部 通信とバックエンド連携

第4部 通信とバックエンド連携・第8章

ログインと認証

★★☆ 重要7トピック・面接 4問

アプリでのログインと、トークンを安全に扱う方法を押さえる。

ひとことで

アプリは秘密を持てないので、PKCE で認可コードの横取りを防ぐ。ログイン画面はシステムのブラウザで開く。

OAuth 2.0 は、利用者がパスワードをアプリに渡さずに、別のサービス(認可サーバー。Google や自社の ID 基盤など)でログインして、アプリにアクセストークン(API を使ってよいという許可証)を渡すための決まりです。「誰がログインしたか」を伝える部分は、OAuth の上に作られた OpenID Connect(OIDC)が受け持ちます。

作り方を間違えると、トークンを盗まれて他人になりすまされる事故につながるので、アプリでの正しい作り方を押さえておきます。

アプリで「Google でログイン」や自社の ID 基盤でのログインを作るときは、OAuth 2.0 の認可コードフローに PKCE を付けたものを使います。アプリは中身を解析されるので、秘密の値(クライアントシークレット)を安全に持てません。PKCE はその代わりに、ログインのたびに使い捨ての合言葉を作る仕組みです。

  • アプリがランダムな文字列 code_verifier を作り、そのハッシュ code_challenge(S256)をつけてログイン画面を開く。
  • 利用者がログインすると、決めておいた戻り先(リダイレクト URI)に認可コードが返る。
  • アプリは認可コードと元の code_verifier をトークンの窓口に送り、アクセストークンとリフレッシュトークンを受け取る。
ログインはシステムのブラウザーで、交換は verifier 付きでアプリシステムのブラウザー認可サーバーAPIverifier を作る① 開く(code_challenge)② ログイン画面③ 認可コード④ 戻り先 URL でアプリへ⑤ コード+verifier⑥ アクセス・リフレッシュトークン⑦ アクセストークンで呼ぶ利用者がログイン③のコードを盗まれても、アプリしか知らない verifier がないとトークンに交換できない
PKCE 付きの認可コードフロー。コードを盗まれても verifier がなければ交換できない

途中で認可コードを盗まれても、code_verifier を知らなければトークンに交換できません。

(SRE)認証と状態管理(Cookie・セッション・トークン・OAuth)

下の「ブラウザで試す」の図は、Web サービスのサーバーが「Google でログイン」を受け持つ場合の流れです。認可サーバーへリダイレクト → ログインと同意 → 認可コードを持って戻る → コードを code_verifier と一緒にトークンに交換、という順番はアプリでも同じです。違うのは交換する側で、図ではサーバーがクライアントシークレットも付けて交換しますが、アプリではアプリ自身がシークレットなしで code_verifier を付けて交換します。

面接ではこう答える

アプリでのログインは、OAuth 2.0 の認可コードフローに PKCE を付けて使います。アプリは中身を解析されるので秘密の値を持てず、その代わりにログインのたびに使い捨ての code_verifier を作り、そのハッシュを付けてログイン画面を開きます。戻ってきた認可コードをトークンに交換するときに元の code_verifier を送るので、途中で認可コードを盗まれても、verifier を知らない人はトークンに交換できません。

別のページで開く →

ネットワーク図を読み込んでいます…

ひとことで

ログイン画面は WebView ではなく、ASWebAuthenticationSession や Custom Tabs で開く。アプリから入力を見られないため。

OAuth のログイン画面をどこで開くかは、安全性に直結します。

ログイン画面はアプリの外で開くWebView(アプリの中)アプリが入力を読める多くの ID 基盤が断っているシステムのブラウザーASWebAuthenticationSession・Custom Tabsログイン状態も共有できるログインが終わると、決めておいた戻り先の URL でアプリに認可コードが返る
WebView ではアプリが入力を盗み見られる。システムのブラウザーならアプリから中は見えない

ログイン画面は、アプリの中の WebView ではなくシステムのブラウザーで開きます。WebView ではアプリが入力されたパスワードを盗み見られるので、多くの ID 基盤(Google など)は WebView でのログインを断っています。iOS は ASWebAuthenticationSession、Android は Custom Tabs を使います。ブラウザーのログイン状態を共有できるので、利用者がパスワードを打たずに済む利点もあります。自分で作るより、AppAuth などの実績のあるライブラリを使うのが安全です。

項目iOSAndroid
ログイン画面ASWebAuthenticationSessionCustom Tabs
戻り先(リダイレクト URI)ユニバーサルリンクか独自スキームApp Links か独自スキーム
トークンの保存先KeychainKeystore の鍵で暗号化して保存
よく使うライブラリAppAuth for iOSAppAuth for Android

面接ではこう答える

アプリのログイン画面は、アプリの中の WebView ではなく、iOS なら ASWebAuthenticationSession、Android なら Custom Tabs で開きます。WebView ではアプリが入力されたパスワードを盗み見られるので、Google など多くの ID 基盤は WebView でのログインを断っています。システムのブラウザーならログインの状態を共有でき、利用者がパスワードを打たずに済む利点もあります。自分で作るより AppAuth などの実績あるライブラリを使います。

ひとことで

期限の短いアクセストークンを、リフレッシュトークンで取り直す。同時の 401 では更新を1回にまとめる。

アクセストークンは漏れたときの被害を小さくするため短い期限にし、切れたらリフレッシュトークンで取り直します。ここでの失敗は「突然ログアウトされる」という分かりやすい不具合になります。

  • アクセストークンは短い期限(数分〜1時間ほど)にし、切れたらリフレッシュトークンで取り直します。どちらも Keychain・Keystore に保存します。
  • API が 401 を返したら、リフレッシュして1回だけ元の通信をやり直します。何度も繰り返さないようにします。
  • 複数の通信が同時に 401 になったとき、リフレッシュは1回だけにまとめます(排他制御)。リフレッシュトークンを使うたびに新しいものに変わる方式(ローテーション)では、同時に2回使うと片方が無効になり、ログアウトされてしまいます。
同時の 401 は、リフレッシュ1回にまとめる通信 A → 401通信 B → 401通信 C → 401リフレッシュ(1回)他は待つ新しいトークンでA・B・C をやり直すそれぞれがリフレッシュすると、ローテーションの方式では古いトークンの再使用と見なされ、ログアウトされる
リフレッシュは1回だけにまとめ、他の通信はその結果を待ってからやり直す

面接ではこう答える

アクセストークンは数分から1時間ほどの短い期限にし、切れたらリフレッシュトークンで取り直します。API が 401 を返したら、リフレッシュして元の通信を1回だけやり直します。注意点は複数の通信が同時に 401 になったときで、リフレッシュは排他制御で1回にまとめ、他の通信はその結果を待ちます。リフレッシュトークンが使うたびに変わる方式では、同時に2回使うと片方が無効になり、ログアウトされてしまうからです。

8.4生体認証

用語★★☆ 重要
ひとことで

Face ID・指紋で端末の鍵を使う。パスキーはフィッシングに強い。

Face ID・Touch ID・指紋認証などの生体認証は、「今この端末を持っているのが持ち主か」を端末の中で確かめる仕組みです。顔や指紋のデータは端末の安全な領域から出ず、アプリにもサーバーにも渡りません。アプリが受け取れるのは「確認できた/できなかった」だけです。

照合は端末の中だけ。通ったら鍵が使えるようになる顔・指紋端末の安全な領域照合はここだけKeychain の項目を開くKeystore の鍵を使うAPI を呼ぶtrue/false だけで判断改造した端末では結果を書き換えられる鍵の利用とひもづけるSecAccessControl・CryptoObject顔や指紋のデータはアプリにもサーバーにも渡らない。アプリが受け取るのは結果だけ
生体認証は端末の中の照合。結果を鍵の利用とひもづけて使う

生体認証の位置づけ

なので生体認証は、サーバーへのログインそのものではなく、端末に保存した秘密を取り出すときの鍵として使います。

  • iOS:LocalAuthentication の結果の true/false だけで判断すると、改造した端末では結果を書き換えられます。Keychain の項目に SecAccessControl(.biometryCurrentSet など)を付けると、生体認証が通らないと OS が中身を渡しません。Info.plist に NSFaceIDUsageDescription が必要です。
  • Android:BiometricPrompt に Keystore の鍵(利用者の認証が必要な設定の鍵)を使う CryptoObject を渡し、認証が通ったときだけ鍵が使えるようにします。

指紋や顔の登録が変わったら鍵を無効にする設定もでき、他人の指紋を追加されて開かれるのを防げます。

(SRE)認証の種類と強化(MFA・SSO・パスキー)

面接ではこう答える

Face ID や指紋などの生体認証は、端末を持っているのが持ち主かを端末の中で確かめる仕組みで、顔や指紋のデータはアプリにもサーバーにも渡りません。アプリが受け取れるのは成功か失敗かだけなので、その結果の true だけで判断すると改造した端末で書き換えられます。そこで Keychain の SecAccessControl や Android の Keystore の鍵と組み合わせ、生体認証が通ったときだけ OS が秘密を渡すようにします。

8.5パスキー

用語★★☆ 重要
ひとことで

パスワードの代わりに、端末の秘密鍵とサーバーの公開鍵で本人を確かめる。ドメインに結びつくのでフィッシングに強い。

パスキーは、パスワードの代わりに公開鍵の仕組みで本人を確かめる方法です。Web とアプリで同じパスキーを使え、利用者は生体認証や画面ロックを解くだけでログインできます。

サーバーには公開鍵だけを置く端末秘密鍵(外に出ない)生体認証・画面ロックで使うサーバー公開鍵ドメインに結びつく登録:公開鍵を送るログイン:チャレンジ(使い捨ての値)秘密鍵で署名して返す偽のサイトはドメインが違うのでパスキーが使われない。漏れて困るパスワードもない
サーバーには公開鍵だけ。ログインでは端末の秘密鍵で署名し、サーバーが公開鍵で確かめる

パスキーは、パスワードの代わりに公開鍵の仕組みで本人を確かめる方法です。秘密鍵は端末(や iCloud キーチェーン・Google パスワードマネージャーで同期された端末)にあり、サーバーには公開鍵だけを置きます。使うときは生体認証や画面ロックで確かめるだけです。秘密鍵はそのサイト(ドメイン)でしか使えないので、偽のサイトに入力させるフィッシングに強いのが最大の利点です。

観点生体認証(端末に保存した秘密を開く)パスキー
確かめること端末を持っているのが持ち主かサーバーに対して本人か
サーバーに置くものなし(端末の中だけ)公開鍵
フィッシングログインそのものの守りではないドメインに結びつくので強い
端末を変えたとき登録し直し同期されたパスキーなら使える

アプリで使うには、アプリとドメインを結びつける設定が要ります。iOS は Associated Domains の webcredentials: と apple-app-site-association、Android は Credential Manager と assetlinks.json(Digital Asset Links)を使います。Web と同じパスキーをアプリでも使えるようになります。

生体認証(端末の中の確認)

面接ではこう答える

パスキーは、パスワードの代わりに公開鍵の仕組みで本人を確かめる方法です。秘密鍵は端末や、iCloud キーチェーン・Google パスワードマネージャーで同期された端末にあり、サーバーには公開鍵だけを置きます。ログインではサーバーが送る使い捨ての値に端末が秘密鍵で署名し、サーバーが公開鍵で確かめます。秘密鍵はそのドメインでしか使えないので、偽のサイトに入力させるフィッシングに強く、アプリでは Associated Domains や assetlinks.json でドメインと結びつけます。

ひとことで

外部のログインを使うアプリは、情報を絞った別のログイン(Sign in with Apple など)も選べるようにする決まりがある。

「Google でログイン」のような外部のアカウントでのログインは、アプリでは審査の決まりにも関わります。App Store の審査ガイドライン(4.8)では、外部やソーシャルのログインを使うアプリは、集める情報を名前とメールに限り、メールを隠せて、同意なく広告の追跡に使わないログインの方法も同じように選べるようにすることが求められます。Sign in with Apple はこの条件を満たす代表です。自社のアカウントだけでログインするアプリなどには例外があるので、最新のガイドラインを確かめます。

気をつけること内容
メールが隠されるSign in with Apple では、中継用のメールアドレスが届くことがある。メールで同じ人かを判断しない
名前は最初の1回だけSign in with Apple は名前を最初の認証のときだけ返すので、そのときに保存する
同じ人の別のアカウントGoogle と Apple で別々に登録されることがある。あとから結びつける機能を用意する
サーバーで確かめるアプリが受け取ったトークンはサーバーに送り、署名と発行元・宛先を検証してからログインさせる

ソーシャルログインでも、ログイン画面の開き方(システムのブラウザーや OS の専用の画面)と、トークンの保存先の考え方は同じです。

面接ではこう答える

App Store の審査ガイドラインでは、Google などの外部のログインを使うアプリは、集める情報を名前とメールに限り、メールを隠せて、同意なく広告の追跡に使わないログインの方法も選べるようにする必要があり、Sign in with Apple がその代表です。実装では、メールが中継用のアドレスになることや名前が最初の1回しか返らないことに気をつけ、アプリが受け取ったトークンは必ずサーバーで検証してからログインさせます。

ログアウトは「トークンを消すだけ」と思われがちですが、片付けが足りないと、次にログインした別の人に前の人の通知やデータが見えるという深刻な不具合になります。

ログアウトで片付けるものサーバーに知らせる失効させるトークンを消す鍵の保存先から通知の登録を外すデバイストークンデータを消す前の人のデータ最初の画面へ戻るで戻れない
トークンだけでなく、利用者に結びつくものを全部消す。次の人に前の人のデータが見えないように
片付けるもの理由
サーバーのセッション・リフレッシュトークン失効させないと、漏れていた場合に使われ続ける
端末のトークンKeychain は、アプリを消しても残る場合がある
プッシュ通知の登録前の人あての通知が届き続ける
DB・画像のキャッシュ・下書き共有の端末や、アカウントの切り替えで前の人のデータが見える
メモリ上の状態ViewModel や共有の状態に前の人の情報が残る

また App Store では、アカウントを作れるアプリはアプリの中からアカウントの削除を始められるようにする必要があります。Google Play でも、アカウントの削除を頼める方法(アプリの中と Web)を用意して申告する決まりがあります。削除は、ログアウトの片付けに加えて、サーバー側でデータを消す流れまで含めて設計します。

この章のまとめ

🖨 まとめシート(印刷・PDF)
  • OAuth + PKCE でのログイン、トークンの保存と更新
  • 生体認証・パスキー
  • アプリでの OAuth(PKCE):アプリは秘密を持てないので、PKCE で認可コードの横取りを防ぐ。ログイン画面はシステムのブラウザで開く。
  • ログイン画面はシステムのブラウザーで:ログイン画面は WebView ではなく、ASWebAuthenticationSession や Custom Tabs で開く。アプリから入力を見られないため。
  • トークンの更新(リフレッシュ):期限の短いアクセストークンを、リフレッシュトークンで取り直す。同時の 401 では更新を1回にまとめる。
  • 生体認証:Face ID・指紋で端末の鍵を使う。パスキーはフィッシングに強い。
  • パスキー:パスワードの代わりに、端末の秘密鍵とサーバーの公開鍵で本人を確かめる。ドメインに結びつくのでフィッシングに強い。
  • Sign in with Apple とソーシャルログイン:外部のログインを使うアプリは、情報を絞った別のログイン(Sign in with Apple など)も選べるようにする決まりがある。

この章のクイズ

面接ではこう聞かれる

まず答えを見ずに声に出して答え、そのあと開いて比べる。

Q1アプリでアクセストークンをどこに保存しますか?

模範回答

iOS は Keychain、Android は Keystore の鍵で暗号化して保存します。UserDefaults や SharedPreferences は暗号化されない普通のファイルで、端末のバックアップや、脱獄・root 化した端末から読まれる可能性があるので、トークンは入れません。Keychain と Keystore は OS が守る領域を使い、Keystore は鍵を取り出せない形で持てるので、ファイルを抜かれても中身を読まれにくくなります。

あわせて、iOS では再起動後に一度ロックを解除すれば読める ThisDeviceOnly の設定にして別の端末に移らないようにし、Android では自動バックアップからトークンを外します。アクセストークンは短い期限にし、漏れたときの被害を小さくします。ただし端末を完全に乗っ取られたら守り切れないので、サーバー側でリフレッシュトークンを無効にできるようにしておくことも大事です。

深掘りされたら

  • アクセストークンをメモリだけに持つのはどうですか?
  • アプリを消して入れ直したとき、Keychain に残ったトークンはどう扱いますか?

見られているポイントKeychain・Keystore と理由

Q2アプリのログインに WebView を使わないのはなぜですか? PKCE とは何ですか?

模範回答

WebView ではアプリが入力されたパスワードを盗み見られるので、利用者にも ID 基盤にも安全か分かりません。そのため Google など多くの基盤は WebView でのログインを断っています。代わりに iOS は ASWebAuthenticationSession、Android は Custom Tabs というシステムのブラウザーを使います。

PKCE は、アプリが秘密の値を安全に持てない問題への対策です。ログインのたびに使い捨ての code_verifier を作り、そのハッシュを付けてログインを始め、認可コードを交換するときに元の値を送ります。途中で認可コードを盗まれても、元の値を知らなければトークンに換えられません。

深掘りされたら

  • リダイレクト URI に独自スキームを使う危険は?
  • リフレッシュトークンはどう扱いますか?

見られているポイントWebView の危険と、PKCE が防ぐもの

Q3生体認証でのログインをどう設計しますか?

模範回答

生体認証は端末の中で「持ち主か」を確かめるもので、サーバーへのログインそのものではありません。なので、最初は普通にログインしてトークンを受け取り、それを生体認証が通らないと取り出せない形で保存します。iOS なら Keychain の項目に SecAccessControl を付け、Android なら BiometricPrompt に Keystore の鍵を使う CryptoObject を渡します。

結果の true/false だけで判断すると、改造した端末で書き換えられるので避けます。指紋や顔の登録が変わったら鍵を無効にする設定にし、生体認証が使えない人のためにパスコードなどの代わりの手段も用意します。

深掘りされたら

  • パスキーとの違いは?
  • 生体認証の登録が変わったら、どうなりますか?

見られているポイント生体認証を鍵の取り出しとひもづけられるか

Q4ログアウトのときに、アプリで何を片付けますか?

模範回答

トークンを消すだけでは足りません。まずサーバーにログアウトを伝えてリフレッシュトークンを失効させ、端末の Keychain や Keystore からトークンを消します。次に、プッシュ通知のデバイストークンとその利用者の結びつきを外し、前の人あての通知が届かないようにします。

さらに、DB・画像のキャッシュ・下書き・メモリ上の共有の状態など、利用者に結びつくデータを消して、最初の画面に戻るで戻れない形で移ります。共有の端末や、アカウントの切り替えで前の人のデータが見えるのが一番の事故なので、ログアウトの後に別のアカウントでログインするテストを入れておきます。

深掘りされたら

  • アプリを消して入れ直したとき、Keychain に残ったトークンはどう扱いますか?

見られているポイントトークン以外の片付けと、別の人に見える事故の防止