Firebase Auth (Swift/iOS)
認証ライブラリ
Firebase Auth (Swift/iOS)
概要
Firebase Auth for iOS(Swift)は、Googleが提供するFirebaseプラットフォームのiOS向け認証SDKです。2025年現在、Swift Package Managerによる完全な統合をサポートし、iOS、macOS、tvOS、watchOSアプリケーション向けに統一された認証体験を提供しています。メール・パスワード認証、OAuth(Google、Apple、Facebook等)、電話番号認証、匿名認証など豊富な認証方式をサポートし、SwiftUIとUIKitの両方で使用できます。Appleの最新セキュリティガイドラインに準拠し、企業レベルのユーザー管理機能をFirebase Consoleと連携して提供します。
詳細
Firebase Auth for iOS(Swift)は、Appleエコシステムに最適化された認証ソリューションです。主な特徴:
- Apple プラットフォーム最適化: iOS、macOS、tvOS、watchOSでの統一API
- Swift Package Manager対応: 2025年推奨のモダンな依存管理
- SwiftUI/UIKit統合: 両フレームワークでのネイティブサポート
- 豊富な認証方式: メール・パスワード、OAuth、電話番号、Face ID、Touch ID、カスタムトークン認証
- リアルタイム状態管理: AuthStateDidChangeListenerによる自動状態監視
- セキュリティ機能: 多要素認証、メール認証、HTTPS通信、カスタムクレーム
- Firebase統合: Firestore、Cloud Functions、Analytics等との完全統合
メリット・デメリット
メリット
- Googleが提供する企業グレードの認証基盤で信頼性が高い
- Apple公式ガイドラインに準拠したセキュリティ実装
- Swift Package Managerによるモダンで簡単な依存管理
- SwiftUIとUIKitの両方でネイティブサポート
- Firebase Consoleによる包括的なユーザー管理とアナリティクス
- Apple Sign In、Face ID、Touch IDとの完全統合
デメリット
- Firebaseエコシステムに依存するベンダーロックイン
- カスタム認証ロジックの実装に制約がある
- Firebaseの利用料金が発生(無料枠あり)
- Apple以外のプラットフォームでは使用できない
- インターネット接続が必要な機能がある
参考ページ
- Firebase Auth iOS Documentation
- GitHub - firebase/firebase-ios-sdk
- Firebase Console
- Firebase iOS API Reference
書き方の例
Swift Package Manager によるセットアップとインストール
// Package.swift への依存関係追加
// または Xcode の File > Add Packages から追加
// Firebase iOS SDK: https://github.com/firebase/firebase-ios-sdk.git
// AppDelegate.swift - Firebase初期化
import UIKit
import Firebase
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Firebase設定ファイル(GoogleService-Info.plist)を追加後に初期化
FirebaseApp.configure()
return true
}
}
// SwiftUI アプリの場合
// App.swift
import SwiftUI
import Firebase
@main
struct MyApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
// iOS シミュレーター用のエミュレーター設定(開発環境)
#if DEBUG
Auth.auth().useEmulator(withHost: "localhost", port: 9099)
#endif
基本的なメール・パスワード認証の実装
// AuthenticationViewModel.swift - 認証ロジック
import Foundation
import Firebase
import Combine
class AuthenticationViewModel: ObservableObject {
@Published var user: User?
@Published var isLoading = false
@Published var errorMessage = ""
private var authStateHandle: AuthStateDidChangeListenerHandle?
init() {
addAuthStateListener()
}
deinit {
removeAuthStateListener()
}
// 認証状態リスナーの設定
func addAuthStateListener() {
authStateHandle = Auth.auth().addStateDidChangeListener { [weak self] _, user in
DispatchQueue.main.async {
self?.user = user
}
}
}
func removeAuthStateListener() {
if let handle = authStateHandle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
// メール・パスワードでユーザー登録
func signUp(email: String, password: String) {
guard !email.isEmpty, !password.isEmpty else {
errorMessage = "メールアドレスとパスワードを入力してください"
return
}
isLoading = true
errorMessage = ""
Auth.auth().createUser(withEmail: email, password: password) { [weak self] result, error in
DispatchQueue.main.async {
self?.isLoading = false
if let error = error {
self?.handleAuthError(error)
return
}
// 登録成功後にメール認証を送信
self?.sendEmailVerification()
}
}
}
// メール・パスワードでサインイン
func signIn(email: String, password: String) {
guard !email.isEmpty, !password.isEmpty else {
errorMessage = "メールアドレスとパスワードを入力してください"
return
}
isLoading = true
errorMessage = ""
Auth.auth().signIn(withEmail: email, password: password) { [weak self] result, error in
DispatchQueue.main.async {
self?.isLoading = false
if let error = error {
self?.handleAuthError(error)
return
}
// サインイン成功
print("サインイン成功: \(result?.user.email ?? "")")
}
}
}
// メール認証の送信
func sendEmailVerification() {
guard let user = Auth.auth().currentUser else { return }
user.sendEmailVerification { [weak self] error in
DispatchQueue.main.async {
if let error = error {
self?.errorMessage = "メール認証の送信に失敗しました: \(error.localizedDescription)"
} else {
self?.errorMessage = "認証メールを送信しました。メールを確認してください。"
}
}
}
}
// パスワードリセット
func resetPassword(email: String) {
guard !email.isEmpty else {
errorMessage = "メールアドレスを入力してください"
return
}
Auth.auth().sendPasswordReset(withEmail: email) { [weak self] error in
DispatchQueue.main.async {
if let error = error {
self?.handleAuthError(error)
} else {
self?.errorMessage = "パスワードリセットメールを送信しました"
}
}
}
}
// サインアウト
func signOut() {
do {
try Auth.auth().signOut()
} catch {
errorMessage = "サインアウトに失敗しました: \(error.localizedDescription)"
}
}
// エラーハンドリング
private func handleAuthError(_ error: Error) {
if let authError = error as NSError?,
let errorCode = AuthErrorCode(rawValue: authError.code) {
switch errorCode {
case .emailAlreadyInUse:
errorMessage = "このメールアドレスは既に使用されています"
case .weakPassword:
errorMessage = "パスワードが弱すぎます"
case .invalidEmail:
errorMessage = "無効なメールアドレスです"
case .userNotFound:
errorMessage = "ユーザーが見つかりません"
case .wrongPassword:
errorMessage = "パスワードが間違っています"
case .userDisabled:
errorMessage = "このアカウントは無効化されています"
case .networkError:
errorMessage = "ネットワークエラーが発生しました"
default:
errorMessage = "認証エラー: \(error.localizedDescription)"
}
} else {
errorMessage = "予期しないエラー: \(error.localizedDescription)"
}
}
}
// SwiftUI認証画面の実装
// LoginView.swift
import SwiftUI
struct LoginView: View {
@StateObject private var authViewModel = AuthenticationViewModel()
@State private var email = ""
@State private var password = ""
@State private var isSignUpMode = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
// ヘッダー
VStack {
Image(systemName: "lock.shield")
.font(.system(size: 60))
.foregroundColor(.blue)
Text(isSignUpMode ? "アカウント作成" : "ログイン")
.font(.largeTitle)
.fontWeight(.bold)
}
.padding(.bottom, 30)
// フォーム
VStack(spacing: 15) {
TextField("メールアドレス", text: $email)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.emailAddress)
.autocapitalization(.none)
SecureField("パスワード", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
if !authViewModel.errorMessage.isEmpty {
Text(authViewModel.errorMessage)
.foregroundColor(.red)
.font(.caption)
.multilineTextAlignment(.center)
}
}
// アクションボタン
VStack(spacing: 10) {
Button(action: {
if isSignUpMode {
authViewModel.signUp(email: email, password: password)
} else {
authViewModel.signIn(email: email, password: password)
}
}) {
if authViewModel.isLoading {
ProgressView()
.scaleEffect(0.8)
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Text(isSignUpMode ? "アカウント作成" : "ログイン")
}
}
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.disabled(authViewModel.isLoading)
Button(action: {
isSignUpMode.toggle()
authViewModel.errorMessage = ""
}) {
Text(isSignUpMode ? "既にアカウントをお持ちですか?ログイン" : "アカウントをお持ちでない方は新規作成")
.font(.caption)
}
if !isSignUpMode {
Button("パスワードを忘れた場合") {
authViewModel.resetPassword(email: email)
}
.font(.caption)
.foregroundColor(.gray)
}
}
Spacer()
}
.padding()
.navigationBarHidden(true)
}
}
}
OAuth認証(Apple、Google等)の実装
// OAuth認証のセットアップ
// OAuthAuthenticationViewModel.swift
import Foundation
import Firebase
import AuthenticationServices
import CryptoKit
class OAuthAuthenticationViewModel: NSObject, ObservableObject {
@Published var user: User?
@Published var isLoading = false
@Published var errorMessage = ""
// Apple Sign In用のnonceを生成
private func randomNonceString(length: Int = 32) -> String {
precondition(length > 0)
let charset: [Character] = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
var result = ""
var remainingLength = length
while remainingLength > 0 {
let randoms: [UInt8] = (0 ..< 16).map { _ in
var random: UInt8 = 0
let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
if errorCode != errSecSuccess {
fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)")
}
return random
}
randoms.forEach { random in
if remainingLength == 0 {
return
}
if random < charset.count {
result.append(charset[Int(random)])
remainingLength -= 1
}
}
}
return result
}
// SHA256でハッシュ化
private func sha256(_ input: String) -> String {
let inputData = Data(input.utf8)
let hashedData = SHA256.hash(data: inputData)
let hashString = hashedData.compactMap {
String(format: "%02x", $0)
}.joined()
return hashString
}
// Apple Sign In実装
func signInWithApple() {
let nonce = randomNonceString()
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
request.nonce = sha256(nonce)
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.presentationContextProvider = self
authorizationController.performRequests()
// nonceを一時保存
UserDefaults.standard.set(nonce, forKey: "currentNonce")
}
// Google Sign In実装(Google SDK が必要)
func signInWithGoogle() {
// Google Sign In SDK の実装
// pod 'GoogleSignIn' を追加してから使用
/*
guard let clientID = FirebaseApp.app()?.options.clientID else { return }
let config = GIDConfiguration(clientID: clientID)
GIDSignIn.sharedInstance.configuration = config
guard let presentingViewController = UIApplication.shared.windows.first?.rootViewController else {
return
}
GIDSignIn.sharedInstance.signIn(withPresenting: presentingViewController) { [weak self] result, error in
if let error = error {
self?.errorMessage = "Google Sign In failed: \(error.localizedDescription)"
return
}
guard let user = result?.user,
let idToken = user.idToken?.tokenString else {
return
}
let credential = GoogleAuthProvider.credential(withIDToken: idToken,
accessToken: user.accessToken.tokenString)
Auth.auth().signIn(with: credential) { authResult, error in
if let error = error {
self?.errorMessage = "Firebase sign in failed: \(error.localizedDescription)"
} else {
self?.errorMessage = "Google Sign In successful"
}
}
}
*/
}
// Facebook Sign In実装(Facebook SDK が必要)
func signInWithFacebook() {
// Facebook SDK の実装
// pod 'FBSDKLoginKit' を追加してから使用
/*
let loginManager = LoginManager()
loginManager.logIn(permissions: ["email"], from: nil) { [weak self] result, error in
if let error = error {
self?.errorMessage = "Facebook Sign In failed: \(error.localizedDescription)"
return
}
guard let accessToken = AccessToken.current else {
self?.errorMessage = "Failed to get access token"
return
}
let credential = FacebookAuthProvider.credential(withAccessToken: accessToken.tokenString)
Auth.auth().signIn(with: credential) { authResult, error in
if let error = error {
self?.errorMessage = "Firebase sign in failed: \(error.localizedDescription)"
} else {
self?.errorMessage = "Facebook Sign In successful"
}
}
}
*/
}
// 匿名サインイン
func signInAnonymously() {
isLoading = true
Auth.auth().signInAnonymously { [weak self] authResult, error in
DispatchQueue.main.async {
self?.isLoading = false
if let error = error {
self?.errorMessage = "匿名サインインに失敗しました: \(error.localizedDescription)"
} else {
self?.errorMessage = "匿名サインインに成功しました"
}
}
}
}
}
// Apple Sign In デリゲート実装
extension OAuthAuthenticationViewModel: ASAuthorizationControllerDelegate {
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
guard let nonce = UserDefaults.standard.string(forKey: "currentNonce") else {
fatalError("Invalid state: A login callback was received, but no login request was sent.")
}
guard let appleIDToken = appleIDCredential.identityToken else {
print("Unable to fetch identity token")
return
}
guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
return
}
let credential = OAuthProvider.credential(withProviderID: "apple.com",
idToken: idTokenString,
rawNonce: nonce)
Auth.auth().signIn(with: credential) { [weak self] authResult, error in
DispatchQueue.main.async {
if let error = error {
self?.errorMessage = "Apple Sign In failed: \(error.localizedDescription)"
} else {
self?.errorMessage = "Apple Sign In successful"
// ユーザー情報を更新(初回のみ)
if let user = authResult?.user, user.displayName == nil {
self?.updateUserProfile(user: user, credential: appleIDCredential)
}
}
}
}
}
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
DispatchQueue.main.async {
self.errorMessage = "Apple Sign In error: \(error.localizedDescription)"
}
}
private func updateUserProfile(user: User, credential: ASAuthorizationAppleIDCredential) {
let changeRequest = user.createProfileChangeRequest()
if let fullName = credential.fullName {
let displayName = [fullName.givenName, fullName.familyName]
.compactMap { $0 }
.joined(separator: " ")
if !displayName.isEmpty {
changeRequest.displayName = displayName
}
}
changeRequest.commitChanges { error in
if let error = error {
print("Failed to update user profile: \(error.localizedDescription)")
}
}
}
}
// ASAuthorizationControllerPresentationContextProviding実装
extension OAuthAuthenticationViewModel: ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return UIApplication.shared.windows.first { $0.isKeyWindow } ?? UIWindow()
}
}
// SwiftUI OAuth認証ボタン
struct OAuthButtonsView: View {
@StateObject private var oauthViewModel = OAuthAuthenticationViewModel()
var body: some View {
VStack(spacing: 15) {
// Apple Sign In ボタン
Button(action: {
oauthViewModel.signInWithApple()
}) {
HStack {
Image(systemName: "applelogo")
Text("Appleでサインイン")
}
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color.black)
.foregroundColor(.white)
.cornerRadius(10)
}
// Google Sign In ボタン
Button(action: {
oauthViewModel.signInWithGoogle()
}) {
HStack {
Image(systemName: "globe")
Text("Googleでサインイン")
}
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(10)
}
// Facebook Sign In ボタン
Button(action: {
oauthViewModel.signInWithFacebook()
}) {
HStack {
Image(systemName: "f.circle")
Text("Facebookでサインイン")
}
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
// 匿名サインイン ボタン
Button(action: {
oauthViewModel.signInAnonymously()
}) {
HStack {
Image(systemName: "person.circle")
Text("匿名でサインイン")
}
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color.gray)
.foregroundColor(.white)
.cornerRadius(10)
}
if !oauthViewModel.errorMessage.isEmpty {
Text(oauthViewModel.errorMessage)
.foregroundColor(.red)
.font(.caption)
.multilineTextAlignment(.center)
}
}
.padding()
}
}
ユーザー情報管理とプロファイル更新
// UserProfileViewModel.swift - ユーザープロファイル管理
import Foundation
import Firebase
import Combine
class UserProfileViewModel: ObservableObject {
@Published var user: User?
@Published var displayName = ""
@Published var email = ""
@Published var isEmailVerified = false
@Published var isLoading = false
@Published var message = ""
private var cancellables = Set<AnyCancellable>()
init() {
// 認証状態を監視
Auth.auth().addStateDidChangeListener { [weak self] _, user in
DispatchQueue.main.async {
self?.user = user
self?.loadUserData()
}
}
}
private func loadUserData() {
guard let user = user else { return }
displayName = user.displayName ?? ""
email = user.email ?? ""
isEmailVerified = user.isEmailVerified
}
// プロフィール更新
func updateProfile() {
guard let user = user else { return }
isLoading = true
let changeRequest = user.createProfileChangeRequest()
changeRequest.displayName = displayName.isEmpty ? nil : displayName
changeRequest.commitChanges { [weak self] error in
DispatchQueue.main.async {
self?.isLoading = false
if let error = error {
self?.message = "プロフィール更新に失敗しました: \(error.localizedDescription)"
} else {
self?.message = "プロフィールを更新しました"
}
}
}
}
// パスワード変更
func changePassword(currentPassword: String, newPassword: String) {
guard let user = user, let email = user.email else { return }
isLoading = true
// 再認証が必要
let credential = EmailAuthProvider.credential(withEmail: email, password: currentPassword)
user.reauthenticate(with: credential) { [weak self] _, error in
if let error = error {
DispatchQueue.main.async {
self?.isLoading = false
self?.message = "現在のパスワードが間違っています: \(error.localizedDescription)"
}
return
}
// パスワード更新
user.updatePassword(to: newPassword) { error in
DispatchQueue.main.async {
self?.isLoading = false
if let error = error {
self?.message = "パスワード変更に失敗しました: \(error.localizedDescription)"
} else {
self?.message = "パスワードを変更しました"
}
}
}
}
}
// メール認証送信
func sendEmailVerification() {
guard let user = user else { return }
user.sendEmailVerification { [weak self] error in
DispatchQueue.main.async {
if let error = error {
self?.message = "認証メール送信に失敗しました: \(error.localizedDescription)"
} else {
self?.message = "認証メールを送信しました"
}
}
}
}
// アカウント削除
func deleteAccount(password: String, completion: @escaping (Bool) -> Void) {
guard let user = user, let email = user.email else {
completion(false)
return
}
isLoading = true
// 再認証が必要
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
user.reauthenticate(with: credential) { [weak self] _, error in
if let error = error {
DispatchQueue.main.async {
self?.isLoading = false
self?.message = "認証に失敗しました: \(error.localizedDescription)"
completion(false)
}
return
}
// アカウント削除
user.delete { error in
DispatchQueue.main.async {
self?.isLoading = false
if let error = error {
self?.message = "アカウント削除に失敗しました: \(error.localizedDescription)"
completion(false)
} else {
self?.message = "アカウントを削除しました"
completion(true)
}
}
}
}
}
// サインアウト
func signOut() {
do {
try Auth.auth().signOut()
message = "サインアウトしました"
} catch {
message = "サインアウトに失敗しました: \(error.localizedDescription)"
}
}
}
// SwiftUI ユーザープロファイル画面
struct UserProfileView: View {
@StateObject private var profileViewModel = UserProfileViewModel()
@State private var currentPassword = ""
@State private var newPassword = ""
@State private var showingDeleteConfirmation = false
@State private var deletePassword = ""
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 20) {
// ユーザー情報表示
VStack(spacing: 15) {
// プロフィール画像(仮)
Image(systemName: "person.circle.fill")
.font(.system(size: 80))
.foregroundColor(.gray)
// 基本情報
VStack(spacing: 10) {
HStack {
Text("メール:")
.fontWeight(.semibold)
Spacer()
Text(profileViewModel.email)
.foregroundColor(.gray)
}
HStack {
Text("認証状態:")
.fontWeight(.semibold)
Spacer()
HStack {
Image(systemName: profileViewModel.isEmailVerified ? "checkmark.circle.fill" : "xmark.circle.fill")
.foregroundColor(profileViewModel.isEmailVerified ? .green : .red)
Text(profileViewModel.isEmailVerified ? "認証済み" : "未認証")
.foregroundColor(profileViewModel.isEmailVerified ? .green : .red)
}
}
if !profileViewModel.isEmailVerified {
Button("認証メール送信") {
profileViewModel.sendEmailVerification()
}
.font(.caption)
.foregroundColor(.blue)
}
}
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(10)
// 表示名更新
VStack(alignment: .leading, spacing: 10) {
Text("表示名")
.fontWeight(.semibold)
TextField("表示名", text: $profileViewModel.displayName)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
profileViewModel.updateProfile()
}) {
Text("プロフィール更新")
.frame(maxWidth: .infinity)
.frame(height: 44)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
.disabled(profileViewModel.isLoading)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(10)
// パスワード変更
VStack(alignment: .leading, spacing: 10) {
Text("パスワード変更")
.fontWeight(.semibold)
SecureField("現在のパスワード", text: $currentPassword)
.textFieldStyle(RoundedBorderTextFieldStyle())
SecureField("新しいパスワード", text: $newPassword)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
profileViewModel.changePassword(currentPassword: currentPassword, newPassword: newPassword)
currentPassword = ""
newPassword = ""
}) {
Text("パスワード変更")
.frame(maxWidth: .infinity)
.frame(height: 44)
.background(Color.orange)
.foregroundColor(.white)
.cornerRadius(8)
}
.disabled(profileViewModel.isLoading || currentPassword.isEmpty || newPassword.isEmpty)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(10)
// 危険な操作
VStack(alignment: .leading, spacing: 15) {
Text("危険な操作")
.fontWeight(.semibold)
.foregroundColor(.red)
Button(action: {
profileViewModel.signOut()
}) {
Text("サインアウト")
.frame(maxWidth: .infinity)
.frame(height: 44)
.background(Color.gray)
.foregroundColor(.white)
.cornerRadius(8)
}
Button(action: {
showingDeleteConfirmation = true
}) {
Text("アカウント削除")
.frame(maxWidth: .infinity)
.frame(height: 44)
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(8)
}
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(10)
// メッセージ表示
if !profileViewModel.message.isEmpty {
Text(profileViewModel.message)
.foregroundColor(.blue)
.font(.caption)
.multilineTextAlignment(.center)
.padding()
}
}
.padding()
}
.navigationTitle("プロフィール")
.alert("アカウント削除", isPresented: $showingDeleteConfirmation) {
SecureField("パスワード", text: $deletePassword)
Button("削除", role: .destructive) {
profileViewModel.deleteAccount(password: deletePassword) { success in
if success {
// 削除成功時の処理
}
}
deletePassword = ""
}
Button("キャンセル", role: .cancel) {
deletePassword = ""
}
} message: {
Text("アカウントを削除すると、すべてのデータが失われます。この操作は取り消せません。パスワードを入力して確認してください。")
}
}
}
}
高度な認証機能とセキュリティ
// AdvancedAuthenticationService.swift - 高度な認証機能
import Foundation
import Firebase
import LocalAuthentication
class AdvancedAuthenticationService: ObservableObject {
@Published var user: User?
@Published var isLoading = false
@Published var errorMessage = ""
// カスタムクレームの取得
func getCustomClaims() async throws -> [String: Any]? {
guard let user = Auth.auth().currentUser else {
throw AuthError.userNotFound
}
let result = try await user.getIDTokenResult()
return result.claims
}
// IDトークンの取得
func getIDToken(forceRefresh: Bool = false) async throws -> String {
guard let user = Auth.auth().currentUser else {
throw AuthError.userNotFound
}
return try await user.getIDToken(forcingRefresh: forceRefresh)
}
// 多要素認証の設定
func enrollMultiFactor(phoneNumber: String) async throws {
guard let user = Auth.auth().currentUser else {
throw AuthError.userNotFound
}
let phoneCredential = PhoneAuthProvider.provider().credential(
withVerificationID: "verification_id",
verificationCode: "verification_code"
)
let assertion = PhoneMultiFactorGenerator.assertion(with: phoneCredential)
try await user.multiFactor.enroll(with: assertion, displayName: "電話番号")
}
// 生体認証(Face ID / Touch ID)の確認
func authenticateWithBiometrics() async throws -> Bool {
let context = LAContext()
var error: NSError?
// 生体認証の利用可能性を確認
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
throw BiometricError.notAvailable
}
do {
let reason = "アプリにアクセスするために生体認証を使用します"
return try await context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason)
} catch {
throw BiometricError.failed
}
}
// カスタムトークンでの認証
func signInWithCustomToken(_ token: String) async throws {
do {
let result = try await Auth.auth().signIn(withCustomToken: token)
DispatchQueue.main.async {
self.user = result.user
}
} catch {
throw error
}
}
// アカウントリンク
func linkAccount(with credential: AuthCredential) async throws {
guard let user = Auth.auth().currentUser else {
throw AuthError.userNotFound
}
do {
let result = try await user.link(with: credential)
DispatchQueue.main.async {
self.user = result.user
}
} catch {
throw error
}
}
// プロバイダーの解除
func unlinkProvider(_ providerID: String) async throws {
guard let user = Auth.auth().currentUser else {
throw AuthError.userNotFound
}
do {
let result = try await user.unlink(fromProvider: providerID)
DispatchQueue.main.async {
self.user = result.user
}
} catch {
throw error
}
}
// セキュリティイベントの監視
func monitorSecurityEvents() {
Auth.auth().addIDTokenDidChangeListener { [weak self] _, user in
if let user = user {
// IDトークンが変更された時の処理
print("ID token changed for user: \(user.uid)")
// セキュリティログの記録
Task {
await self?.logSecurityEvent("id_token_changed", userID: user.uid)
}
}
}
}
private func logSecurityEvent(_ event: String, userID: String) async {
// セキュリティイベントのログ記録
// Firestore や Cloud Functions を使用してログを保存
print("Security event: \(event) for user: \(userID)")
}
}
// カスタムエラー定義
enum AuthError: Error, LocalizedError {
case userNotFound
case tokenExpired
case networkError
var errorDescription: String? {
switch self {
case .userNotFound:
return "ユーザーが見つかりません"
case .tokenExpired:
return "トークンの有効期限が切れています"
case .networkError:
return "ネットワークエラーが発生しました"
}
}
}
enum BiometricError: Error, LocalizedError {
case notAvailable
case failed
var errorDescription: String? {
switch self {
case .notAvailable:
return "生体認証は利用できません"
case .failed:
return "生体認証に失敗しました"
}
}
}
// SecureAuthenticationView.swift - セキュア認証画面
struct SecureAuthenticationView: View {
@StateObject private var authService = AdvancedAuthenticationService()
@State private var showingBiometricPrompt = false
@State private var customToken = ""
var body: some View {
VStack(spacing: 20) {
Text("高度な認証機能")
.font(.title)
.fontWeight(.bold)
// 生体認証ボタン
Button(action: {
Task {
do {
let success = try await authService.authenticateWithBiometrics()
if success {
print("生体認証成功")
}
} catch {
authService.errorMessage = error.localizedDescription
}
}
}) {
HStack {
Image(systemName: "faceid")
Text("Face ID / Touch ID で認証")
}
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color.green)
.foregroundColor(.white)
.cornerRadius(10)
}
// カスタムトークン認証
VStack(spacing: 10) {
TextField("カスタムトークン", text: $customToken)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
Task {
do {
try await authService.signInWithCustomToken(customToken)
} catch {
authService.errorMessage = error.localizedDescription
}
}
}) {
Text("カスタムトークンで認証")
.frame(maxWidth: .infinity)
.frame(height: 44)
.background(Color.purple)
.foregroundColor(.white)
.cornerRadius(8)
}
.disabled(customToken.isEmpty)
}
// IDトークン取得
Button(action: {
Task {
do {
let token = try await authService.getIDToken(forceRefresh: true)
print("ID Token: \(token)")
} catch {
authService.errorMessage = error.localizedDescription
}
}
}) {
Text("IDトークン取得")
.frame(maxWidth: .infinity)
.frame(height: 44)
.background(Color.indigo)
.foregroundColor(.white)
.cornerRadius(8)
}
// カスタムクレーム取得
Button(action: {
Task {
do {
let claims = try await authService.getCustomClaims()
print("Custom Claims: \(claims ?? [:])")
} catch {
authService.errorMessage = error.localizedDescription
}
}
}) {
Text("カスタムクレーム取得")
.frame(maxWidth: .infinity)
.frame(height: 44)
.background(Color.teal)
.foregroundColor(.white)
.cornerRadius(8)
}
if !authService.errorMessage.isEmpty {
Text(authService.errorMessage)
.foregroundColor(.red)
.font(.caption)
.multilineTextAlignment(.center)
}
Spacer()
}
.padding()
.onAppear {
authService.monitorSecurityEvents()
}
}
}