← 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's Hidden Gem: Building Powerful Linux Desktop Apps with Ease

Flutter's Hidden Gem: Building Powerful Linux Desktop Apps with Ease

Flutter is gaining traction as a robust framework for Linux desktop development, offering a smoother experience compared to traditional toolkits like GTK and Qt. This post will explore why Flutter excels for Linux apps, highlight its advantages, and provide practical insights for developers looking to leverage Flutter for their desktop projects.

Cover image for Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows

Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows

Deploying Flutter desktop apps on Windows can be challenging, especially regarding code signing and preventing Windows from blocking installations. This post will guide developers through the process of signing their Flutter desktop applications, exploring alternatives to the Microsoft Store like creating executable installers, and covering the necessary steps to ensure a smooth user experience.

Cover image for Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows

Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows

Developers frequently voice frustrations about Flutter SDK version management and installation processes, particularly on Windows. This post will explore best practices for managing multiple Flutter versions using tools like FVM, discuss integrating Flutter with package managers like Winget for a smoother Windows setup, and offer strategies to ensure a consistent development environment across projects and teams.