Firebase

Firebase Authentication in Flutter: Complete Guide 2026

Muhammad Shakil Muhammad Shakil
Mar 31, 2026
14 min read
Firebase Authentication in Flutter — complete guide for 2026
Back to Blog

Every Flutter app I've built in the last four years has used Firebase Auth. Not because it's the only option — there's Supabase, Appwrite, Auth0, custom JWT solutions — but because Firebase Auth is free for most use cases, deeply integrated with Firestore and Cloud Functions, and handles the ugly parts of authentication (token refresh, session persistence, multi-provider linking) without you touching them. It's also the auth system most Flutter tutorials use, which means your team already knows it.

This guide isn't a "hello world" Firebase Auth tutorial. It covers every auth method you'll actually need in production, plus the stuff tutorials skip: auth state routing, role-based access with custom claims, Firestore security rules, and handling edge cases like account linking when a user signs in with Google after already registering with email.

Project Setup and Firebase Configuration

If you haven't set up Firebase in a Flutter project before, the FlutterFire CLI makes it painless. No more manually downloading google-services.json and GoogleService-Info.plist.

# Install FlutterFire CLI (one-time)
dart pub global activate flutterfire_cli

# Configure Firebase for your Flutter project
flutterfire configure

The CLI generates a firebase_options.dart file with your project's configuration for all platforms. Then add the required packages:

dependencies:
  firebase_core: ^3.8.0
  firebase_auth: ^5.5.0
  google_sign_in: ^6.3.0
  sign_in_with_apple: ^6.1.0

Initialize Firebase in your main.dart:

import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

That's it. No platform-specific configuration files to manage, no manual SHA-1 fingerprint registration (the CLI handles it). If you're still manually configuring Firebase, stop — the CLI approach is faster and less error-prone.

Email/Password Authentication

Email/password is the foundation. Every app should support it, even if social sign-in is your primary method. Some users don't have Google accounts on their device (yes, really — Huawei users, for example). Others just prefer email.

Registration

Future<UserCredential?> registerWithEmail(String email, String password) async {
  try {
    final credential = await FirebaseAuth.instance
        .createUserWithEmailAndPassword(
      email: email,
      password: password,
    );
    // Send email verification
    await credential.user?.sendEmailVerification();
    return credential;
  } on FirebaseAuthException catch (e) {
    switch (e.code) {
      case 'email-already-in-use':
        throw 'An account already exists with this email.';
      case 'weak-password':
        throw 'Password must be at least 6 characters.';
      case 'invalid-email':
        throw 'Please enter a valid email address.';
      default:
        throw 'Registration failed. Please try again.';
    }
  }
}

The error handling isn't optional. I've reviewed production apps that just catch Exception and show "Something went wrong." Firebase Auth throws specific error codes — use them. Your users shouldn't have to guess why registration failed.

Sign In

Future<UserCredential?> signInWithEmail(String email, String password) async {
  try {
    return await FirebaseAuth.instance.signInWithEmailAndPassword(
      email: email,
      password: password,
    );
  } on FirebaseAuthException catch (e) {
    switch (e.code) {
      case 'user-not-found':
        throw 'No account found with this email.';
      case 'wrong-password':
        throw 'Incorrect password.';
      case 'user-disabled':
        throw 'This account has been disabled.';
      case 'too-many-requests':
        throw 'Too many attempts. Please try again later.';
      default:
        throw 'Sign in failed. Please try again.';
    }
  }
}

Don't Forget Password Reset

Every app with email auth needs a password reset flow. It's one line: await FirebaseAuth.instance.sendPasswordResetEmail(email: email); — Firebase handles the email, the reset link, and the token validation. All you need is a text field for the user's email and a button to trigger it. No custom backend, no email templates to manage.

Google Sign-In

Google Sign-In is the highest-converting social auth method on Android (where 90%+ of devices have a Google account already signed in). The google_sign_in package handles the OAuth flow and returns credentials that Firebase Auth accepts directly.

Future<UserCredential?> signInWithGoogle() async {
  // Trigger the Google Sign-In flow
  final googleUser = await GoogleSignIn().signIn();
  if (googleUser == null) return null; // User cancelled

  // Get auth details from the Google user
  final googleAuth = await googleUser.authentication;

  // Create a Firebase credential
  final credential = GoogleAuthProvider.credential(
    accessToken: googleAuth.accessToken,
    idToken: googleAuth.idToken,
  );

  // Sign in to Firebase
  return await FirebaseAuth.instance.signInWithCredential(credential);
}

That's the complete flow. The GoogleSignIn().signIn() call shows the native Google account picker on Android and the Google OAuth web view on iOS. If the user already has a Firebase account with the same email (from email/password registration), Firebase automatically links the accounts — no extra code needed.

One gotcha: on iOS, you need to add your REVERSED_CLIENT_ID (from GoogleService-Info.plist) as a URL scheme in Xcode. The FlutterFire CLI doesn't do this automatically. Without it, the Google Sign-In redirect fails silently.

Apple Sign-In

If your iOS app offers any social sign-in (Google, Facebook, Twitter), Apple requires you to also offer Sign in with Apple. It's a review rejection if you skip it. Apple Sign-In is also popular with privacy-conscious users because it lets them hide their real email behind a relay address.

Future<UserCredential?> signInWithApple() async {
  // Generate a nonce for security
  final rawNonce = generateNonce();
  final nonce = sha256ofString(rawNonce);

  // Request credential from Apple
  final appleCredential = await SignInWithApple.getAppleIDCredential(
    scopes: [
      AppleIDAuthorizationScopes.email,
      AppleIDAuthorizationScopes.fullName,
    ],
    nonce: nonce,
  );

  // Create Firebase credential
  final oauthCredential = OAuthProvider('apple.com').credential(
    idToken: appleCredential.identityToken,
    rawNonce: rawNonce,
  );

  // Sign in to Firebase
  final userCredential = await FirebaseAuth.instance
      .signInWithCredential(oauthCredential);

  // Apple only returns the name on first sign-in
  if (appleCredential.givenName != null) {
    await userCredential.user?.updateDisplayName(
      '${appleCredential.givenName} ${appleCredential.familyName}',
    );
  }

  return userCredential;
}

The nonce prevents replay attacks — Apple signs the nonce into the identity token, and Firebase verifies it matches what you sent. The name-on-first-sign-in quirk is important: Apple only sends the user's name the very first time they authorize your app. If you don't save it then, it's gone. The updateDisplayName call in the code above handles this.

Phone/OTP Authentication

Phone auth is essential if your app targets South Asia, the Middle East, or Africa. In Pakistan alone, phone-first registration is the norm — users expect to verify with an SMS code, not create an email account. Firebase handles the SMS delivery, code generation, and verification server-side.

Future<void> verifyPhoneNumber(String phoneNumber) async {
  await FirebaseAuth.instance.verifyPhoneNumber(
    phoneNumber: phoneNumber, // Include country code: +923001234567
    timeout: const Duration(seconds: 60),
    verificationCompleted: (PhoneAuthCredential credential) async {
      // Android-only: auto-verify when SMS is received
      await FirebaseAuth.instance.signInWithCredential(credential);
    },
    verificationFailed: (FirebaseAuthException e) {
      if (e.code == 'invalid-phone-number') {
        throw 'Invalid phone number format.';
      }
      throw 'Verification failed. Please try again.';
    },
    codeSent: (String verificationId, int? resendToken) {
      // Save verificationId — you'll need it to create a credential
      // Navigate to OTP input screen
    },
    codeAutoRetrievalTimeout: (String verificationId) {
      // Auto-retrieval timed out, user must enter code manually
    },
  );
}

Future<UserCredential> verifyOTP(String verificationId, String otp) async {
  final credential = PhoneAuthProvider.credential(
    verificationId: verificationId,
    smsCode: otp,
  );
  return await FirebaseAuth.instance.signInWithCredential(credential);
}

On Android, the verificationCompleted callback fires automatically when the device detects the incoming SMS — the user never types a code. This only works on Android with the SMS Retriever API. On iOS, users always enter the code manually.

Phone Auth Costs and Limits

Firebase gives you 10,000 free phone verifications per month on the Blaze plan. After that, it's $0.01–0.06 per verification depending on the destination country. Pakistan is on the lower end at ~$0.01. For an app with 50,000 monthly sign-ups via phone, you're looking at roughly $400-500/month — manageable but not free. Consider rate-limiting verification requests per device to prevent abuse, and use reCAPTCHA verification (enabled by default) to block bots.

Anonymous Authentication

Anonymous auth lets users interact with your app without creating an account. Firebase assigns a temporary UID that persists across sessions. When the user eventually signs up, you link the anonymous account to their real credentials — preserving their data, preferences, and activity history.

// Sign in anonymously
Future<UserCredential> signInAnonymously() async {
  return await FirebaseAuth.instance.signInAnonymously();
}

// Later: link anonymous account to email/password
Future<UserCredential> linkAnonymousToEmail(
  String email, String password,
) async {
  final credential = EmailAuthProvider.credential(
    email: email,
    password: password,
  );
  return await FirebaseAuth.instance.currentUser!
      .linkWithCredential(credential);
}

The linkWithCredential call merges the anonymous UID with the new account. Any Firestore data stored under the anonymous UID stays intact. This is how apps like Duolingo let you complete three lessons before asking you to create an account — the anonymous session's progress carries over seamlessly.

One warning: if you don't clean up orphaned anonymous accounts, they pile up. Firebase doesn't auto-delete them. Set up a Cloud Function that runs weekly to delete anonymous accounts older than 30 days using admin.auth().deleteUser(uid).

Auth State Management and Routing

Firebase Auth provides a stream of auth state changes. This is the backbone of your app's auth-aware routing — it fires when the user signs in, signs out, or when the app opens and restores a session from the device keychain.

class AuthGate extends StatelessWidget {
  const AuthGate({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User?>(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (context, snapshot) {
        // Still loading
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const SplashScreen();
        }
        // Authenticated
        if (snapshot.hasData) {
          return const HomeScreen();
        }
        // Not authenticated
        return const LoginScreen();
      },
    );
  }
}

Put this AuthGate widget at the root of your app (inside MaterialApp's home parameter). It handles everything: initial loading state, redirecting unauthenticated users to login, and routing authenticated users to the home screen. When the user signs out, the stream emits null, and the widget tree automatically rebuilds to show the login screen. No manual navigation needed.

For apps using GoRouter, you can use a redirect function instead:

final router = GoRouter(
  refreshListenable: GoRouterRefreshStream(
    FirebaseAuth.instance.authStateChanges(),
  ),
  redirect: (context, state) {
    final loggedIn = FirebaseAuth.instance.currentUser != null;
    final loggingIn = state.matchedLocation == '/login';

    if (!loggedIn && !loggingIn) return '/login';
    if (loggedIn && loggingIn) return '/';
    return null;
  },
  routes: [ /* your routes */ ],
);

Role-Based Access Control with Custom Claims

Custom claims are the Firebase-native way to implement roles (admin, moderator, premium user). They're set server-side and embedded in the user's ID token, which means they're available in Firestore Security Rules without any extra database reads.

Setting Claims (Server-Side)

You need a Cloud Function or server-side script using the Firebase Admin SDK:

// Cloud Function to set admin role
const admin = require('firebase-admin');
admin.initializeApp();

exports.setAdminRole = functions.https.onCall(async (data, context) => {
  // Verify the caller is already an admin
  if (!context.auth?.token?.admin) {
    throw new functions.https.HttpsError(
      'permission-denied', 'Only admins can assign admin roles.'
    );
  }
  await admin.auth().setCustomUserClaims(data.uid, { admin: true });
  return { message: 'Admin role assigned.' };
});

Reading Claims (Client-Side)

Future<bool> isAdmin() async {
  final user = FirebaseAuth.instance.currentUser;
  if (user == null) return false;

  // Force refresh to get latest claims
  final tokenResult = await user.getIdTokenResult(true);
  return tokenResult.claims?['admin'] == true;
}

The true parameter in getIdTokenResult(true) forces a token refresh, ensuring you get the latest claims. Without it, recently set claims won't appear until the token naturally refreshes (up to 1 hour). Always force-refresh after actions that might change claims, like upgrading to premium.

Custom Claims Limits

The ID token payload has a 1,000-byte limit for custom claims. Don't store complex data — stick to role flags like {"admin": true, "tier": "premium"}. For detailed permissions, store them in Firestore and reference the role claim only as a lookup key. If you hit the byte limit, you're storing too much in claims.

Firestore Security Rules for Authenticated Users

Authentication without security rules is like locking your front door but leaving the windows open. Firebase Auth verifies who the user is. Security Rules enforce what they can do.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Users can only read/write their own document
    match /users/{userId} {
      allow read, write: if request.auth != null
                         && request.auth.uid == userId;
    }

    // Only admins can access admin collection
    match /admin/{document=**} {
      allow read, write: if request.auth.token.admin == true;
    }

    // Public read, authenticated write
    match /posts/{postId} {
      allow read: if true;
      allow create: if request.auth != null;
      allow update, delete: if request.auth != null
                            && request.auth.uid == resource.data.authorId;
    }
  }
}

The request.auth.token.admin check uses the custom claim we set earlier. No Firestore reads needed — the claim is already in the JWT token. This is why custom claims exist: they're evaluated at the security rules layer, not the application layer, so they can't be bypassed by a malicious client.

Test your rules before deploying. The Firebase Console has a Rules Playground where you simulate reads and writes with different auth states. I've seen apps go live with allow read, write: if true; in production — don't be that app.

Production Checklist

Before shipping your auth implementation, verify every item on this list. I've skipped items on this list myself and paid for it with emergency hotfixes at 2 AM.

  1. Enable email verification. Check user.emailVerified before granting full access. Unverified accounts are spam magnets.
  2. Handle account linking. When a user signs in with Google after registering with email, Firebase links the accounts if the email matches. Test this flow explicitly — edge cases in account linking cause confusing UX.
  3. Set up password requirements. Firebase's default minimum is 6 characters. That's not enough for 2026. Enforce at least 8 characters with a mix of letters and numbers on your client-side validation.
  4. Add rate limiting. Firebase Auth rate-limits by default, but you should add your own rate limiting on sensitive endpoints (like phone verification requests) to prevent abuse.
  5. Clean up anonymous accounts. Schedule a Cloud Function to delete anonymous accounts older than 30 days. They accumulate fast and clutter your user database.
  6. Test on real devices. Google Sign-In behaves differently on emulators vs physical Android devices. Apple Sign-In only works on real iOS devices (not the simulator for the full flow). Test both before submitting to app stores.
  7. Enable App Check. Firebase App Check prevents unauthorized clients from accessing your Firebase resources. It doesn't protect against all attacks, but it stops casual API abuse from tools like Postman or curl.
  8. Monitor failed sign-ins. Use Firebase Analytics or Cloud Functions to track failed authentication attempts. A spike in failed sign-ins often indicates a credential stuffing attack.

Related Guides

🔗 Official Resources

Frequently Asked Questions

What Firebase Auth methods should I implement in 2026?

At minimum: email/password and Google Sign-In. These two cover 85%+ of users. Add Apple Sign-In if you're on iOS (Apple requires it if you offer any other social login). Phone/OTP authentication is essential for apps targeting South Asia, the Middle East, and Africa where phone-first registration dominates. Anonymous auth is useful for letting users try your app before committing to an account.

Is Firebase Auth free?

Firebase Auth is free for most use cases. You get unlimited email/password, Google, Apple, and anonymous sign-ins at no cost. Phone authentication costs $0.06 per successful verification in the US and $0.01-0.06 in other regions after the free tier of 10,000 verifications per month. For most apps, you'll never hit the paid tier. The Blaze (pay-as-you-go) plan is required for phone auth but you only pay for what you use beyond the free allowance.

Can I use Firebase Auth with a custom backend?

Yes. Firebase Auth issues JWT tokens that any backend can verify. Call FirebaseAuth.instance.currentUser.getIdToken() on the client, send the token in your API request's Authorization header, and verify it server-side using the Firebase Admin SDK (available for Node.js, Python, Go, Java, and C#). This gives you Firebase's auth UI and session management on the frontend with your own business logic on the backend.

How do I handle auth state persistence in Flutter?

Firebase Auth persists the auth state automatically across app restarts on mobile (using the device keychain on iOS and encrypted shared preferences on Android). On Flutter Web, it uses browser localStorage by default. You can change the persistence strategy with setPersistence() on web. For most Flutter mobile apps, you don't need to do anything — the user stays logged in until they explicitly sign out or you call signOut().

Should I use firebase_ui_auth or build custom auth screens?

Use firebase_ui_auth if you want a working auth flow in under an hour and don't care about custom design. It gives you pre-built sign-in, sign-up, forgot password, and profile screens. Build custom screens if your app's brand identity matters — which it does for any production app. The pre-built screens are functional but generic-looking, and customizing them beyond basic theming is more work than building your own with firebase_auth directly.

How do I add role-based access control with Firebase Auth?

Use Firebase Auth custom claims. Set claims server-side via the Admin SDK: admin.auth().setCustomUserClaims(uid, {role: 'admin'}). On the client, access them via user.getIdTokenResult().claims['role']. Custom claims are embedded in the JWT token and can be checked in Firestore Security Rules with request.auth.token.role == 'admin'. Limit custom claims to essential role data — the token payload has a 1,000-byte limit.

What happens when a Firebase Auth token expires?

Firebase Auth ID tokens expire after 1 hour. The Firebase SDK automatically refreshes them using the refresh token stored on the device. You don't need to handle this manually in most cases — the SDK's getIdToken() method returns a fresh token if the current one is expired. If the refresh token itself is revoked (via the Admin SDK or Firebase Console), the user will be forced to re-authenticate on their next token refresh attempt.

Share this article:

Have an App Idea?

Let our team turn your vision into reality with Flutter.