← Back to posts Cover image for Flutter Hybrid Backend: Seamlessly Integrating Firebase Auth with Your Custom Server

Flutter Hybrid Backend: Seamlessly Integrating Firebase Auth with Your Custom Server

· 7 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

When building a Flutter app, you often find yourself at a crossroads for your backend: go all-in with a platform like Firebase, or build a custom server from scratch? Both paths have distinct advantages. Firebase offers quick setup and ease of use, especially for user authentication and real-time data. A custom server, on the other hand, gives you complete control over your data, business logic, and infrastructure—crucial for highly specific and complex needs.

But what if you could harness the strengths of both? This is where the hybrid backend approach becomes really useful. It allows you to leverage Firebase Authentication for its robust, secure, and easy-to-implement user management, while delegating all other data storage and intricate business logic to your own custom server. It’s a strong combination that provides both convenience and flexibility.

The Core Idea: How It Works

The secret sauce of this hybrid model lies in Firebase ID Tokens. Here’s the simplified flow:

  1. User Authentication (Flutter App): Your Flutter application uses the Firebase SDK to sign in a user (e.g., via email/password, Google, Apple, etc.).
  2. ID Token Acquisition (Flutter App): Upon successful authentication, Firebase issues an ID Token. This is a JSON Web Token (JWT) that cryptographically verifies the user’s identity.
  3. Authenticated Request (Flutter App to Custom Server): When your Flutter app needs to interact with your custom backend, it includes this Firebase ID Token in the Authorization header of its HTTP requests.
  4. Token Verification (Custom Server): Your custom backend receives the request, extracts the ID Token, and uses the Firebase Admin SDK to verify its authenticity and integrity. This SDK ensures the token hasn’t been tampered with and was genuinely issued by Firebase.
  5. User Identification & Authorization (Custom Server): Once verified, the Admin SDK provides the user’s unique Firebase User ID (UID) and other claims. Your server can then use this UID to identify the user, fetch their specific data from your custom database, and apply business logic, confident that the user is legitimately authenticated.

Client-Side (Flutter) Setup

First, ensure Firebase is set up in your Flutter project and firebase_auth is initialized. Your main.dart might look something like this:

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart'; // Generated by FlutterFire CLI

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

Next, let’s create a simple AuthService to handle user sign-in and retrieve the Firebase ID token.

import 'package:firebase_auth/firebase_auth.dart';

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  // Sign in with email and password
  Future<User?> signIn(String email, String password) async {
    try {
      UserCredential userCredential = await _auth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
      return userCredential.user;
    } on FirebaseAuthException catch (e) {
      // Handle specific Firebase authentication errors
      if (e.code == 'user-not-found') {
        print('No user found for that email.');
      } else if (e.code == 'wrong-password') {
        print('Wrong password provided for that user.');
      } else {
        print('Firebase Auth Error: ${e.message}'); // Added for general auth errors
      }
      // Re-throw to propagate the error to the UI for user feedback
      rethrow;
    } catch (e) {
      print('An unexpected error occurred during sign-in: $e');
      rethrow;
    }
  }

  // Get the current user's ID token. Forces a refresh to ensure it's valid.
  Future<String?> getCurrentUserToken() async {
    User? user = _auth.currentUser;
    if (user != null) {
      // Passing `true` forces a token refresh if it's expired or near expiration.
      return await user.getIdToken(true);
    }
    return null;
  }

  // Sign out
  Future<void> signOut() async {
    await _auth.signOut();
  }
}

Now, we’ll create an ApiClient that uses this token for making authenticated requests to your custom backend.

import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:your_app/services/auth_service.dart'; // Adjust path as needed

class ApiClient {
  final String _baseUrl = "https://api.your-custom-backend.com"; // Your backend URL
  final AuthService _authService = AuthService();

  Future<Map<String, dynamic>> makeAuthenticatedRequest(
      String endpoint, Map<String, dynamic> data) async {
    String? token = await _authService.getCurrentUserToken();

    if (token == null) {
      throw Exception("User not authenticated. Please sign in.");
    }

    final response = await http.post(
      Uri.parse('$_baseUrl/$endpoint'),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer $token', // Crucial: Attach the ID token here
      },
      body: jsonEncode(data),
    );

    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else {
      // Handle various error codes from your backend
      print('API Error: ${response.statusCode} - ${response.body}');
      throw Exception('Failed to process request: ${response.body}');
    }
  }
}

Server-Side (Conceptual)

On your custom backend (e.g., Node.js, Python, Go, Java), you’ll need to integrate the Firebase Admin SDK. This SDK provides the verifyIdToken method, which handles the complex cryptographic verification process.

Here’s a conceptual example using pseudo-code for a Node.js Express route, demonstrating the token verification:

// On your Node.js server (after initializing Firebase Admin SDK)
const admin = require('firebase-admin');

// --- Firebase Admin SDK Initialization (example, your setup might vary) ---
// You'll need to initialize the Admin SDK with your service account credentials.
// const serviceAccount = require('./path/to/your/serviceAccountKey.json');
// admin.initializeApp({
//   credential: admin.credential.cert(serviceAccount)
// });
// --------------------------------------------------------------------------

// Middleware to protect routes by verifying Firebase ID Token
async function authenticateFirebaseToken(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).send('Authorization header missing or malformed.');
  }

  const idToken = authHeader.split(' ')[1]; // Extract the token from "Bearer <token>"

  try {
    const decodedToken = await admin.auth().verifyIdToken(idToken);
    req.user = decodedToken; // Attach the decoded user info to the request object
    next(); // Proceed to the next middleware or route handler
  } catch (error) {
    console.error('Error verifying Firebase ID token:', error);
    // Token is invalid, expired, or other issues
    res.status(403).send('Invalid or expired authentication token.');
  }
}

// Example protected route in an Express app
app.post('/api/secure-profile-update', authenticateFirebaseToken, async (req, res) => {
  // If we reach here, req.user contains the decoded Firebase token payload
  const userId = req.user.uid; // The authenticated user's Firebase UID
  console.log(`Received secure request from user: ${userId}`);

  // Now, use 'userId' to interact with your custom database
  // e.g., update user profile data associated with this userId
  const { newEmail, newDisplayName } = req.body;
  // await myCustomDatabase.updateUserProfile(userId, { email: newEmail, displayName: newDisplayName });

  res.json({ message: 'Profile updated successfully!', userId: userId });
});

Handling Challenges & Common Mistakes

  • Token Expiration: Firebase ID tokens are short-lived (typically 1 hour). The getIdToken(true) call in Flutter’s AuthService automatically handles refreshing the token if it’s expired or about to expire, ensuring you always send a valid token to your backend.
  • Error Handling: Both Firebase Auth and your custom backend can throw errors. Implement robust try-catch blocks on the client-side to display meaningful messages to the user (e.g., “Invalid email or password,” “Network error”). Your backend should also return clear error codes and messages to help the client react appropriately.
  • Security:
    • Always use HTTPS for your custom backend API to encrypt all communication between your Flutter app and your server.
    • Never store Firebase ID tokens directly on the client if you can avoid it. Retrieving it via _auth.currentUser.getIdToken(true) on demand is generally safer and leverages Firebase’s SDK for token management.
    • Ensure your Firebase Admin SDK service account key is securely stored on your server and never exposed publicly in your client-side code or repositories.
  • User Logout: When a user signs out, ensure you call _auth.signOut() to clear the local Firebase session. This also effectively invalidates the token locally, preventing its further use.

Conclusion

Adopting a hybrid backend with Firebase Authentication and a custom server offers a compelling blend of advantages: the convenience, security, and ease of Firebase for user management, combined with the flexibility, control, and scalability of your own infrastructure for everything else. By understanding how to effectively pass and verify Firebase ID tokens, you can build powerful, secure, and customizable Flutter apps that fit your unique needs. Happy coding!

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Flutter for High-Performance Desktop: Is it Ready for CAD, Image Processing, and Complex GUIs?

Flutter for High-Performance Desktop: Is it Ready for CAD, Image Processing, and Complex GUIs?

Developers are curious about Flutter's capabilities beyond typical business apps, especially for demanding desktop applications like CAD/CAM or image/video processing. This post will explore Flutter's suitability for high-performance, viewport-based desktop GUIs, discussing Dart's memory model, the 60fps update loop, and real-world examples to gauge its readiness for 'serious' complex software.

Cover image for Debugging Flutter Web Navigation: Solving the Deep Link Refresh Bug

Debugging Flutter Web Navigation: Solving the Deep Link Refresh Bug

Flutter web applications often suffer from a frustrating 'deep link refresh bug' where refreshing the browser on a nested route (e.g., /home/details) bounces the user back to the root or an incorrect path. This post will diagnose the common causes of this issue, explain how Flutter's router handles web URLs, and provide practical solutions and best practices for building robust, refresh-proof navigation in your Flutter web apps.

Cover image for Mastering Internationalization in Flutter: Centralized Strings for Scalable Apps

Mastering Internationalization in Flutter: Centralized Strings for Scalable Apps

As Flutter applications grow, managing strings for multiple languages or just keeping text consistent becomes a challenge. This post will guide developers through effective strategies for centralizing strings, implementing robust internationalization (i18n) and localization (l10n), and leveraging tools to streamline the process for small to large-scale projects.