Firebase Auth は Google アカウントなしで使える
Firebase を使うと聞くと、「利用者に Google でログインしてもらう仕組み」だと思いがちだった。自分もそう思っていた。でも実際は違う。Firebase Authentication は、いろいろなサインインを受け付けて、利用者ごとに uid を配る仕組みだ。Google アカウントは、そのサインインの選び方の1つにすぎない。
iPhone の集中アプリ「DODO」では、Sign in with Apple だけで Firebase Auth を使っている。利用者は Google にログインしないし、Google アカウントも要らない。その仕組みを書く。
Firebase Auth が配るのは「uid」
Firebase Auth の「ログイン方法」には、Google・Apple・メールとパスワード・電話番号・匿名などが並んでいる。どれでサインインしても、Firebase の中では同じように uid を持つユーザーが1人できる。
Sign in with Apple ─┐
Google ログイン ─┼─→ Firebase Auth ─→ uid(この人の ID)
匿名ログイン ─┘ ├─ Firestore のルールで本人だけ読める
└─ サーバーで ID トークンを確かめられる
Google アカウントが要るのは、開発者が Firebase のコンソールや CLI を使うときだけだ。利用者には関係ない。
💡 以前の記事 Firebase 匿名ログインで「とりあえず使える」を実現する の匿名ログインも、同じ考え方。サインインの方法が違うだけで、どれも uid が手に入る。
なぜ Sign in with Apple だけにしたか
DODO では、PC や AI エージェント(Claude Code など)から iPhone にやることを送る機能に、アカウントが要った。送られてきたやることを、その人の所にだけ届けるためだ。
- iPhone のアプリなので、Face ID で一瞬で終わる Sign in with Apple がいちばん楽。
- アプリの中でほかのサードパーティのログイン(Google など)を出すと、App Store の決まりで Sign in with Apple も並べる必要がある。なら最初から Apple だけでいい。
- メールも名前も受け取らない設定にできる。個人の情報を持たずに済む。
コンソールでやること
Firebase コンソールの Authentication → ログイン方法 → Apple を有効にするだけ。
iOS アプリの中でサインインするだけなら、Services ID も要らない(Web でのサインインに使うもの)。bundle ID が Firebase に登録したアプリと合っていればいい。Xcode では、ターゲットに「Sign in with Apple」の Capability を足しておく。
アプリのコード
SwiftUI の SignInWithAppleButton を使う。流れは3つ。
- ボタンを押したら、ランダムな nonce を作り、そのハッシュを Apple に渡す。
- Apple から返ってきた ID トークンと、元の nonce を Firebase に渡す。
- Firebase がそれを確かめて、uid を持つユーザーとしてサインインさせる。
/// SignInWithAppleButton の onRequest で呼ぶ。
func prepare(_ request: ASAuthorizationAppleIDRequest) {
let nonce = Self.randomNonce()
currentNonce = nonce
request.requestedScopes = [] // メールも名前も受け取らない
request.nonce = Self.sha256(nonce) // Apple にはハッシュを渡す
}
/// SignInWithAppleButton の onCompletion で呼ぶ。
func complete(_ result: Result<ASAuthorization, Error>) async throws {
let auth = try result.get()
guard let credential = auth.credential as? ASAuthorizationAppleIDCredential,
let tokenData = credential.identityToken, let idToken = String(data: tokenData, encoding: .utf8),
let nonce = currentNonce else { throw PushInError.signInFailed }
// Firebase には「元の」nonce を渡す
let firebaseCredential = OAuthProvider.appleCredential(withIDToken: idToken, rawNonce: nonce, fullName: credential.fullName)
_ = try await Auth.auth().signIn(with: firebaseCredential)
}
SignInWithAppleButton(.signIn) { service.prepare($0) } onCompletion: { result in
Task {
do { try await service.complete(result) } catch { self.error = error.localizedDescription }
}
}
.frame(height: 44)
nonce は何のためか
nonce は、その場かぎりのランダムな文字列だ。Apple にはハッシュを渡し、Firebase には元の文字列を渡す。Firebase は、Apple の ID トークンの中のハッシュと、元の文字列のハッシュが一致するかを確かめる。これで、どこかで盗まれた ID トークンを使い回してサインインする「リプレイ攻撃」を防げる。
private static func randomNonce(length: Int = 32) -> String {
let charset = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
var bytes = [UInt8](repeating: 0, count: length)
_ = SecRandomCopyBytes(kSecRandomDefault, length, &bytes)
return String(bytes.map { charset[Int($0) % charset.count] })
}
private static func sha256(_ input: String) -> String {
SHA256.hash(data: Data(input.utf8)).map { String(format: "%02x", $0) }.joined()
}
メールも名前も受け取らない
request.requestedScopes = [] にすると、Apple はメールアドレスも名前も渡してこない。サインインの画面にも、「名前」「メール」を共有する欄が出ない。
DODO は uid さえあれば十分なので、これにした。持っていない情報は、漏れることもない。プライバシーポリシーにも「メールアドレスと名前は受け取りません」と書ける。
uid でユーザーを分ける
Firestore のルール
アプリは Firestore を直接読む。ルールで「自分の uid の所だけ読める」にしておけば、他の人のデータは見えない。
match /users/{uid}/lists/{id} {
allow read: if request.auth != null && request.auth.uid == uid;
}
match /{document=**} {
allow read, write: if false;
}
サーバーでは ID トークンを確かめる
Cloud Functions の API を呼ぶときは、Firebase の ID トークンを付ける。サーバーは Admin SDK でそれを確かめて、uid を取り出す。
// アプリ側
let idToken = try await Auth.auth().currentUser!.getIDToken()
req.setValue("Bearer \(idToken)", forHTTPHeaderField: "Authorization")
// Cloud Functions 側
async function appAuth(req, res, next) {
const token = bearer(req.header("authorization"));
if (!token) return res.status(401).json({ error: "missing bearer token" });
try {
req.uid = (await getAuth().verifyIdToken(token)).uid;
next();
} catch {
res.status(401).json({ error: "invalid id token" });
}
}
Apple の ID トークンを直接サーバーで確かめる必要はない。Firebase の ID トークンにまとまっているので、サーバーは Firebase だけを信じればいい。
DODO では、この uid を使って AI エージェント用の API トークンを発行している。そちらの話は Cloud Functions だけでリモート MCP サーバーを作った に書いた。
入れ直しても、同じ uid に戻る
アプリを消して入れ直しても、もう一度「Apple でサインイン」を押せば前と同じ uid に戻る。別の iPhone でも同じだ。
- Apple は、同じ Apple ID・同じ開発者(チーム)のアプリに対して、いつも同じユーザー ID を返す。
- Firebase は、その Apple のユーザー ID と Firebase の uid を結びつけて覚えている。
DODO では、入れ直してサインインし直せば、サーバーにある「届いているやること」がそのまま見える。発行した API トークンもサーバー側で uid に結びついているので、Claude Code に登録したトークンもそのまま使える。
同じ uid にならないのは、次のとき。
- アカウントを削除したとき。Firebase のユーザーごと消すので、次は新しい uid になる。
- 匿名ログインのとき。匿名ログインは、端末に残っている情報だけで同じ人だと判断する。それが消えれば戻せず、別の端末にも引き継げない。Sign in with Apple との大きな違いだ。
📝 戻るのは、サーバーに置いてあるものだけ。DODO の記録(やった DO・ケツイ・設定)は端末の中だけに保存しているので、アプリを消すと、サインインし直しても戻らない。
アカウントの削除
Sign in with Apple を使うアプリは、アプリの中からアカウントを削除できるようにしないと、App Store の審査で断られる(ガイドライン 5.1.1(v))。削除では3つやる。
- サーバーのデータを消す。
- Firebase のユーザーを消す。
- Apple との連携を取り消す(Sign in with Apple の「このアプリの利用をやめる」と同じ状態にする)。
3 には、その場で Apple から受け取る authorization code が要る。なので、削除のときにもう一度 Sign in with Apple をしてもらい、そのコードで取り消している。
let code = (try? result.get().credential as? ASAuthorizationAppleIDCredential)?
.authorizationCode.flatMap { String(data: $0, encoding: .utf8) }
try await service.complete(result) // サインインし直す
try await service.deleteAccount(authorizationCode: code)
func deleteAccount(authorizationCode: String?) async throws {
_ = try await call("DELETE", path: "v1/account") // サーバーのデータと Firebase のユーザーを消す
if let code = authorizationCode { try? await Auth.auth().revokeToken(withAuthorizationCode: code) }
signOut()
}
⚠️ revokeToken が Apple に取り消しを頼めるようにするには、Apple Developer で Sign in with Apple 用の**キー(.p8)**を作り、Firebase の Apple の設定の「OAuth コードフローの構成」に、チーム ID・キー ID・秘密鍵を入れておく必要がある。入れていないと、取り消しだけが黙って失敗する。
📝 プレミアムの機能でサインインする作りだと、プレミアムが切れたときに削除の画面へたどり着けなくなりがち。DODO では、削除の欄はプレミアムかどうかに関係なく出すようにした。
まとめ
- Firebase Auth は「Google ログイン」ではなく、いろいろなサインインを受け付けて uid を配る仕組み。
- Sign in with Apple だけでも、Firestore のルールとサーバーの ID トークン検証で、ちゃんとユーザーを分けられる。
requestedScopes = []で、メールも名前も受け取らずに済む。- 入れ直しても、別の端末でも、サインインし直せば同じ uid に戻る(匿名ログインとの違い)。
- nonce はハッシュを Apple に、元の文字列を Firebase に。
- アカウント削除では、Apple との連携の取り消しまでやる(.p8 のキーを忘れずに)。
利用者に Google アカウントを作らせたり、パスワードを覚えさせたりしなくていい。iPhone のアプリなら、これがいちばん手軽だと思う。