diff --git a/docs/06-concepts/11-authentication/04-providers/06-firebase/01-setup.md b/docs/06-concepts/11-authentication/04-providers/06-firebase/01-setup.md index 13fd2a2c..8de45bb2 100644 --- a/docs/06-concepts/11-authentication/04-providers/06-firebase/01-setup.md +++ b/docs/06-concepts/11-authentication/04-providers/06-firebase/01-setup.md @@ -1,46 +1,76 @@ # Setup -Firebase authentication works differently from other identity providers in Serverpod. Instead of handling authentication directly, Serverpod's Firebase integration acts as a bridge between Firebase Authentication and your Serverpod backend. Firebase handles the actual sign-in process through its own SDKs and UI components, while Serverpod syncs the authenticated user and manages the server-side session. +Firebase authentication lets you use any Firebase sign-in method (email/password, phone, Google, Apple, Facebook, etc.) with your Serverpod backend. Firebase handles the sign-in flow through its own SDKs, while Serverpod syncs the authenticated user and manages the server-side session. -This approach allows you to use any authentication method supported by Firebase (email/password, phone, Google, Apple, Facebook, etc.) while maintaining a unified user system in your Serverpod backend. +## Prerequisites -:::caution -You need to install the auth module before you continue, see [Setup](../../setup). -::: +Before you start, make sure you have: -## Create your credentials +- A Serverpod project with the new auth module installed. New projects created with `serverpod create` (Serverpod 3.4 and later) include it by default. If you are upgrading an older project, follow the [auth module setup guide](../../setup) first. +- A Google account with access to the [Firebase Console](https://console.firebase.google.com/). +- The Firebase CLI installed (`npm install -g firebase-tools`) and the FlutterFire CLI activated (`dart pub global activate flutterfire_cli`). You will use both later in the guide. -### Generate Service Account Key +## Get your credentials -The server needs service account credentials to verify Firebase ID tokens. To create a new key: +### Create a Firebase project -1. Go to the [Firebase Console](https://console.firebase.google.com/) (create a new project if you don't have one). -2. Select your project. -3. Navigate to **Project settings** > **Service accounts**. -4. Click **Generate new private key**, then **Generate key**. +1. Go to the [Firebase Console](https://console.firebase.google.com/) and click **Create a project** (or **Add project** if you already have projects). -![Service account](/img/authentication/providers/firebase/1-server-key.png) +2. Enter a project name, accept the terms, and click **Continue**. -This downloads a JSON file containing your service account credentials. + ![Enter project name](/img/authentication/providers/firebase/2-project-name.png) -### Enable Authentication Methods +3. Follow the remaining prompts (Google Analytics is optional) and click **Create project**. -In the Firebase Console, enable the authentication methods you want to support: +### Generate a service account key -1. Go to **Authentication** > **Sign-in method**. -2. Enable your desired providers (Email/Password, Phone, Google, Apple, etc.). -3. Configure each provider according to Firebase's documentation. +The server needs service account credentials to verify Firebase ID tokens. -![Auth provider](/img/authentication/providers/firebase/2-auth-provider.png) +1. In the Firebase Console, navigate to **Project settings** > **Service accounts**. -## Server-side configuration + ![Service accounts page](/img/authentication/providers/firebase/4-service-accounts.png) + +2. Click **Generate new private key**. + + ![Generate new private key button](/img/authentication/providers/firebase/5-generate-key.png) + +3. In the confirmation dialog, click **Generate key**. + + ![Generate key confirmation dialog](/img/authentication/providers/firebase/6-generate-key-confirm.png) + +4. A JSON file downloads to your machine. This file contains your service account credentials. You will need it in the next step. + +### Enable authentication methods + +1. In the Firebase Console sidebar, navigate to **Product categories** > **Security** > **Authentication**. + + ![Navigate to Authentication](/img/authentication/providers/firebase/7-navigate-auth.png) + +2. If this is your first time, click **Get started** to enable the Authentication service. -### Store the Service Account Key + ![Enable Authentication](/img/authentication/providers/firebase/8-enable-auth.png) -This can be done by pasting the contents of the JSON file into the `firebaseServiceAccountKey` key in the `config/passwords.yaml` file or setting as value of the `SERVERPOD_PASSWORD_firebaseServiceAccountKey` environment variable. Alternatively, you can read the file contents directly using the `FirebaseServiceAccountCredentials.fromJsonFile()` method. +3. Select the **Sign-in method** tab. + +4. Click on the provider you want to enable (e.g., **Email/Password**) and toggle it on. Click **Save**. + + ![Enable a sign-in provider](/img/authentication/providers/firebase/9-enable-provider.png) + +5. Repeat for each provider you want to support (Phone, Google, Apple, etc.). Configure each provider according to the instructions shown in the Firebase Console. + + ![Sign-in providers list](/img/authentication/providers/firebase/10-sign-in-methods.png) + +:::note +While enabling providers, you may see a **Firebase App Check** option in the sidebar or as a checkbox during provider configuration. **Leave App Check disabled while you are getting started.** Enabling it before your client integration is ready will cause every sign-in to fail with an App Check token error. You can turn it on later once everything works end-to-end. +::: + +### Store the service account key + +Open `config/passwords.yaml` in your server project. Projects created with `serverpod create` already have `development:`, `staging:`, and `production:` sections; if yours doesn't, add a `development:` section. Then add the `firebaseServiceAccountKey` key under `development:` using the contents of the JSON file you downloaded: ```yaml development: + # ... existing keys (database, redis, serviceSecret, etc.) ... firebaseServiceAccountKey: | { "type": "service_account", @@ -54,57 +84,49 @@ development: } ``` +Production credentials are covered in [Publishing to production](#publishing-to-production) below. + +:::warning +**Never commit `config/passwords.yaml` to version control.** It contains your service account key, which gives admin access to your Firebase project. Use environment variables or a secrets manager in production. +::: + :::warning -The service account key gives admin access to your Firebase project and should not be version controlled. Store it securely using environment variables or secret management. +**Indent the JSON consistently under the `|` block scalar.** Any indentation error will silently break the JSON parser, and authentication will fail at runtime. Mixing tabs and spaces is a common cause. ::: -### Configure the Firebase Identity Provider +## Server-side configuration -In your main `server.dart` file, configure the Firebase identity provider: +### 1. Add the Firebase identity provider -```dart -import 'package:serverpod/serverpod.dart'; -import 'package:serverpod_auth_idp_server/core.dart'; -import 'package:serverpod_auth_idp_server/providers/firebase.dart'; +Open your server's `server.dart` file (e.g., `my_project_server/lib/server.dart`). It should already contain a `pod.initializeAuthServices()` call from the project template. -void run(List args) async { - final pod = Serverpod( - args, - Protocol(), - Endpoints(), - ); +:::note +If `initializeAuthServices` is missing, the auth module is not installed. Verify `serverpod_auth_idp_server` is in your server's `pubspec.yaml` and follow the [auth module setup](../../setup) before continuing. +::: - pod.initializeAuthServices( - tokenManagerBuilders: [ - JwtConfigFromPasswords(), - ], - identityProviderBuilders: [ - FirebaseIdpConfig( - credentials: FirebaseServiceAccountCredentials.fromJsonString( - pod.getPassword('firebaseServiceAccountKey')!, - ), - ), - ], - ); +Add the Firebase import and `FirebaseIdpConfigFromPasswords()` to the existing `identityProviderBuilders` list: - await pod.start(); -} +```dart +import 'package:serverpod_auth_idp_server/providers/firebase.dart'; ``` -:::tip -You can use `FirebaseIdpConfigFromPasswords()` to automatically load credentials from `config/passwords.yaml` or the `SERVERPOD_PASSWORD_firebaseServiceAccountKey` environment variable: - ```dart -identityProviderBuilders: [ - FirebaseIdpConfigFromPasswords(), -], +pod.initializeAuthServices( + tokenManagerBuilders: [ + JwtConfigFromPasswords(), + ], + identityProviderBuilders: [ + // ... any existing providers (e.g., EmailIdpConfigFromPasswords) ... + FirebaseIdpConfigFromPasswords(), + ], +); ``` -::: +`FirebaseIdpConfigFromPasswords()` automatically loads the service account key from the `firebaseServiceAccountKey` key in `config/passwords.yaml` (or the `SERVERPOD_PASSWORD_firebaseServiceAccountKey` environment variable). For loading credentials from other sources (file, JSON map, project ID only), see the [Configuration](./configuration) page. -### Expose the Endpoint +### 2. Create the endpoint -Create an endpoint that extends `FirebaseIdpBaseEndpoint` to expose the Firebase authentication API: +Create a new endpoint file in your server project (e.g., `my_project_server/lib/src/auth/firebase_idp_endpoint.dart`) alongside the existing auth endpoints. Extending the base class registers the sign-in methods with your server so the Flutter client can call them to complete the authentication flow: ```dart import 'package:serverpod_auth_idp_server/providers/firebase.dart'; @@ -112,22 +134,25 @@ import 'package:serverpod_auth_idp_server/providers/firebase.dart'; class FirebaseIdpEndpoint extends FirebaseIdpBaseEndpoint {} ``` -### Generate and Migrate +### 3. Generate code and apply migrations -Finally, run `serverpod generate` to generate the client code and create a migration to initialize the database for the provider. More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration). +Run the following commands from your server project directory (e.g., `my_project_server/`) to generate client code and apply the database migration: -### Basic configuration options +```bash +serverpod generate +serverpod create-migration +dart run bin/main.dart --apply-migrations +``` -- `credentials`: Required. Firebase service account credentials for verifying ID tokens. See the [configuration section](./configuration) for different ways to load credentials. -- `firebaseAccountDetailsValidation`: Optional. Validation function for Firebase account details. By default, this validates that the email is verified when present (phone-only authentication is allowed). See the [configuration section](./configuration#custom-account-validation) for customization options. +:::note +Skipping the migration will cause the server to crash at runtime when the Firebase provider tries to read or write user data. More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration). +::: ## Client-side configuration -The client-side setup uses the official Firebase packages (`firebase_core`, `firebase_auth`, and optionally `firebase_ui_auth`) for authentication. The steps below follow standard Firebase usage - for troubleshooting, refer to the [official Firebase Flutter documentation](https://firebase.google.com/docs/flutter/setup). +### 1. Install required packages -### Install Required Packages - -Add the Firebase and Serverpod authentication packages to your Flutter project: +From your Flutter project directory (e.g., `my_project_flutter/`), add the Firebase and Serverpod authentication packages: ```bash flutter pub add firebase_core firebase_auth serverpod_auth_idp_flutter_firebase @@ -139,26 +164,23 @@ If you want to use Firebase's pre-built UI components, also add: flutter pub add firebase_ui_auth ``` -### Configure FlutterFire +### 2. Configure FlutterFire -If you haven't already, install the Firebase CLI and FlutterFire CLI: +Run the FlutterFire CLI from your Flutter project directory to configure Firebase for the platforms you support: ```bash -npm install -g firebase-tools -dart pub global activate flutterfire_cli +flutterfire configure ``` -Then run the FlutterFire CLI to configure Firebase for your Flutter project: +Select your Firebase project when prompted, and choose the platforms you want to support. -```bash -flutterfire configure -``` +![FlutterFire configure terminal output](/img/authentication/providers/firebase/11-flutterfire-configure.png) -This generates a `firebase_options.dart` file with your platform-specific Firebase configuration. +This generates a `firebase_options.dart` file with your platform-specific Firebase configuration, and registers each platform app with your Firebase project. -### Initialize Firebase and Serverpod +### 3. Initialize Firebase and Serverpod -In your `main.dart`, initialize both Firebase and the Serverpod client: +In your Flutter app's `main.dart` file (e.g., `my_project_flutter/lib/main.dart`), the template already sets up the `Client`. Initialize both Firebase and the Serverpod auth services: ```dart import 'package:firebase_core/firebase_core.dart'; @@ -173,107 +195,170 @@ late Client client; void main() async { WidgetsFlutterBinding.ensureInitialized(); - // Initialize Firebase await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); - // Create the Serverpod client client = Client('http://localhost:8080/') ..connectivityMonitor = FlutterConnectivityMonitor() ..authSessionManager = FlutterAuthSessionManager(); - // Initialize Serverpod auth await client.auth.initialize(); - // Initialize Firebase sign-in service (enables automatic sign-out sync) client.auth.initializeFirebaseSignIn(); runApp(const MyApp()); } ``` -## The authentication flow - -Understanding the Firebase authentication flow helps when building custom integrations: - -1. **User initiates sign-in** with Firebase using `firebase_auth` or `firebase_ui_auth`. -2. **Firebase authenticates** the user and returns a `firebase_auth.User` object. -3. **Your app calls** `FirebaseAuthController.login(user)` with the Firebase user. -4. **The controller extracts** the Firebase ID token from the user. -5. **Token is sent** to your server's `firebaseIdp.login()` endpoint. -6. **Server validates** the JWT using the service account credentials. -7. **Server creates or updates** the user account and issues a Serverpod session token. -8. **Client session is updated** and the user is authenticated with Serverpod in the Flutter app. - :::info -The `initializeFirebaseSignIn()` call in the client setup will ensure that the user gets automatically signed out from Firebase when signing out from Serverpod to keep both systems in sync. +The `initializeFirebaseSignIn()` call ensures that the user gets automatically signed out from Firebase when signing out from Serverpod, keeping both systems in sync. ::: ## Present the authentication UI -### Using firebase_ui_auth - -The easiest way to add Firebase authentication is using the `firebase_ui_auth` package with its pre-built `SignInScreen`: +The quickest way to get started is wrapping your app in a gate widget that uses [`firebase_ui_auth`](https://pub.dev/packages/firebase_ui_auth) together with [`FirebaseAuthController`](https://pub.dev/documentation/serverpod_auth_idp_flutter_firebase/latest/serverpod_auth_idp_flutter_firebase/FirebaseAuthController-class.html) to sync the Firebase user with Serverpod: ```dart +import 'package:flutter/material.dart'; import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; import 'package:firebase_ui_auth/firebase_ui_auth.dart' as firebase_ui; -import 'package:flutter/material.dart'; import 'package:serverpod_auth_idp_flutter_firebase/serverpod_auth_idp_flutter_firebase.dart'; -class SignInPage extends StatefulWidget { - const SignInPage({super.key}); +import '../main.dart'; + +/// A screen that manages sign-in flow and wraps the rest of the app after authentication. +class SignInScreen extends StatefulWidget { + /// The widget to display after authentication. + final Widget child; + + const SignInScreen({super.key, required this.child}); @override - State createState() => _SignInPageState(); + State createState() => _SignInScreenState(); } -class _SignInPageState extends State { +class _SignInScreenState extends State { + // Controller to help with Firebase authentication. late final FirebaseAuthController controller; @override void initState() { super.initState(); + // Initialize the controller with callbacks for authentication events. controller = FirebaseAuthController( client: client, - onError: (error) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error: $error')), - ); - }, + onAuthenticated: () => context.showSnackBar( + message: 'User authenticated.', + backgroundColor: Colors.green, + ), + onError: (error) => context.showSnackBar( + message: 'Authentication failed: $error', + backgroundColor: Colors.red, + ), ); } - @override - void dispose() { - controller.dispose(); - super.dispose(); + /// Handle changes in authentication state: Log in a new Firebase user with Serverpod, if any. + Future _handleAuthStateChange() async { + final user = firebase_auth.FirebaseAuth.instance.currentUser; + if (user != null) await controller.login(user); } @override Widget build(BuildContext context) { - return firebase_ui.SignInScreen( - providers: [ - firebase_ui.EmailAuthProvider(), - ], - actions: [ - firebase_ui.AuthStateChangeAction((context, state) async { - final user = firebase_auth.FirebaseAuth.instance.currentUser; - if (user != null) { - await controller.login(user); - } - }), - firebase_ui.AuthStateChangeAction((context, state) async { - final user = firebase_auth.FirebaseAuth.instance.currentUser; - if (user != null) { - await controller.login(user); - } - }), - ], + // If already authenticated, display the main app. + if (controller.isAuthenticated) return widget.child; + + // Otherwise, display the sign-in screen. + return Center( + child: firebase_ui.SignInScreen( + providers: [ + // Use email/password sign-in provider. + firebase_ui.EmailAuthProvider(), + ], + actions: [ + // Handle when a user has signed in. + firebase_ui.AuthStateChangeAction( + (context, state) => _handleAuthStateChange(), + ), + + // Handle when a user is created (first sign-in). + firebase_ui.AuthStateChangeAction( + (context, state) => _handleAuthStateChange(), + ), + ], + ), + ); + } +} + +// Extension to show a snackbar from a BuildContext. +extension SnackbarExtension on BuildContext { + /// Shows a [SnackBar] with a custom message and optional background color. + void showSnackBar({ + required String message, + Color? backgroundColor, + }) { + ScaffoldMessenger.of(this).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: backgroundColor, + duration: const Duration(seconds: 5), + ), ); } } ``` -For details on using the `FirebaseAuthController` directly and building custom authentication UIs, see the [customizing the UI section](./customizing-the-ui). +For a breakdown of the controller, action handlers, and how to build a custom authentication UI, see [Customizing the UI](./customizing-the-ui). + +:::tip +If you run into issues, see the [Troubleshooting](./troubleshooting) guide. +::: + +## Publishing to production + +Before going live, complete every step below. Skipping any of these is the most common cause of "works in dev, fails in prod" reports. + +### 1. Decide which Firebase project production should use + +Most teams keep a separate Firebase project for production so dev experiments do not affect real users. If that is your setup, create the production Firebase project now and generate a fresh service account key from it (same steps as in [Generate a service account key](#generate-a-service-account-key) above). You can also reuse your dev Firebase project in production; in that case the service account key carries over and you can skip straight to step 2. + +### 2. Add the production service account key + +Production credentials are added alongside your `development:` ones, not swapped in. Dev keeps using the `development:` key when you run locally, and production uses whatever you wire up below. Pick the path that matches your deployment: + +#### Self-hosted + +Add `firebaseServiceAccountKey` to the `production:` section of `config/passwords.yaml` using the same `|` block scalar shown earlier, or set it as the `SERVERPOD_PASSWORD_firebaseServiceAccountKey` environment variable on the production server. The env var path avoids committing the JSON to disk on the server. Whichever path you pick, keep `passwords.yaml` out of version control. + +#### Serverpod Cloud + +Use `scloud password set` and pass the JSON file with `--from-file`: + +```bash +scloud password set firebaseServiceAccountKey --from-file ./firebase-service-account.json +``` + +Run this from your linked server project directory, or pass `--project ` on each call. See the [Serverpod Cloud passwords guide](https://docs.serverpod.dev/cloud/guides/passwords) for project linking and the [passwords vs secrets vs variables](https://docs.serverpod.dev/cloud/guides/passwords#passwords-vs-secrets-vs-variables) note for when to use each. + +### 3. Authorize your production domain + +In the Firebase Console, go to **Authentication** > **Settings** > **Authorized domains** and add the domain your app runs on (e.g., `my-awesome-project.serverpod.space`). Without this entry, OAuth-based sign-in (Google, Apple, etc.) will fail with a redirect URI mismatch in production. + +### 4. Configure each platform app for production + +Depending on which sign-in methods and platforms you support, complete the steps that apply: + +- **Android (Google sign-in via Firebase):** Add your release SHA-1 fingerprint to the Android app in your Firebase project. If you use **Google Play App Signing** (the default for new apps), get the SHA-1 from the Play Console: **Setup** > **App integrity** > **App signing key certificate**. Use the **app signing key** SHA-1, not the upload key SHA-1. If you manage your own release keystore, run `keytool -list -v -keystore your-release-key.jks -alias your-key-alias`. Then re-run `flutterfire configure` so `google-services.json` is updated. +- **iOS (phone or Apple sign-in):** Verify your bundle identifier is registered with the Firebase iOS app, and that `GoogleService-Info.plist` is included in the Xcode project's Runner target. Phone authentication also requires you to upload an APNs authentication key in **Project settings** > **Cloud Messaging**. +- **Web:** Add every domain that will host your app (including custom domains and preview URLs) to the **Authorized domains** list from [step 3](#3-authorize-your-production-domain). + +:::warning +Forgetting the release SHA-1 is the single most common reason Google sign-in works in debug builds but silently fails after publishing to the Play Store. +::: + +### 5. Re-verify Firebase App Check + +If you disabled App Check for development, decide now whether to enable it. Enabling App Check requires extra integration on the client (attest tokens via Play Integrity, App Attest, or reCAPTCHA). Do not enable it without that integration in place, otherwise every authenticated request will be rejected. diff --git a/docs/06-concepts/11-authentication/04-providers/06-firebase/02-configuration.md b/docs/06-concepts/11-authentication/04-providers/06-firebase/02-configuration.md index e97a5d55..fb45f6a8 100644 --- a/docs/06-concepts/11-authentication/04-providers/06-firebase/02-configuration.md +++ b/docs/06-concepts/11-authentication/04-providers/06-firebase/02-configuration.md @@ -2,15 +2,13 @@ This page covers configuration options for the Firebase identity provider beyond the basic setup. -## Configuration options +## Loading credentials with FirebaseIdpConfig -Below is a non-exhaustive list of some of the most common configuration options. For more details on all options, check the `FirebaseIdpConfig` in-code documentation. +The [setup guide](./setup) uses `FirebaseIdpConfigFromPasswords`, which loads the service account key from `passwords.yaml` for you. When you need to load credentials from a different source (a file path, a secrets manager, or just a project ID), use `FirebaseIdpConfig` directly and pass a `FirebaseServiceAccountCredentials` instance. -### Loading Firebase Credentials +`FirebaseServiceAccountCredentials` provides four constructors. These are the only supported ways to construct it: -You can load Firebase service account credentials in several ways: - -**From JSON string (recommended for production):** +**From a JSON string** (use this when reading the JSON from a secrets manager or environment variable): ```dart final firebaseIdpConfig = FirebaseIdpConfig( @@ -20,7 +18,7 @@ final firebaseIdpConfig = FirebaseIdpConfig( ); ``` -**From JSON file:** +**From a JSON file** (useful for local development or when secrets are mounted as files): ```dart import 'dart:io'; @@ -32,7 +30,7 @@ final firebaseIdpConfig = FirebaseIdpConfig( ); ``` -**From JSON map:** +**From a JSON map** (useful when credentials are assembled programmatically): ```dart final firebaseIdpConfig = FirebaseIdpConfig( @@ -49,31 +47,28 @@ final firebaseIdpConfig = FirebaseIdpConfig( ); ``` -### Custom Account Validation - -You can customize the validation for Firebase account details before allowing sign-in. By default, the validation requires the email to be verified when present (phone-only authentication is allowed). - -The default validation logic: +**Project ID only** (only token verification, no admin operations like deleting Firebase accounts): ```dart -static void validateFirebaseAccountDetails( - final FirebaseAccountDetails accountDetails, -) { - // Firebase accounts may not have email if using phone auth - // Only validate verifiedEmail if email is present - if (accountDetails.email != null && accountDetails.verifiedEmail != true) { - throw FirebaseUserInfoMissingDataException(); - } -} +final firebaseIdpConfig = FirebaseIdpConfig( + credentials: const FirebaseServiceAccountCredentials( + projectId: 'your-project-id', + ), +); ``` +:::note +Only `projectId` is required to verify Firebase ID tokens. The full service account JSON is only needed if you also use the [admin operations](./admin-operations) on the server. +::: + +## Custom account validation + +You can customize the validation for Firebase account details before allowing sign-in. By default, the validation requires the email to be verified when present (phone-only authentication is allowed without an email). + To customize validation, provide your own `firebaseAccountDetailsValidation` function: ```dart -final firebaseIdpConfig = FirebaseIdpConfig( - credentials: FirebaseServiceAccountCredentials.fromJsonString( - pod.getPassword('firebaseServiceAccountKey')!, - ), +final firebaseIdpConfig = FirebaseIdpConfigFromPasswords( firebaseAccountDetailsValidation: (accountDetails) { // Require verified email (even for phone auth) if (accountDetails.verifiedEmail != true) { @@ -89,64 +84,63 @@ final firebaseIdpConfig = FirebaseIdpConfig( ); ``` -### FirebaseAccountDetails +### FirebaseAccountDetails properties The `firebaseAccountDetailsValidation` callback receives a `FirebaseAccountDetails` record with the following properties: -| Property | Type | Description | -|----------|------|-------------| -| `userIdentifier` | `String` | The Firebase user's unique identifier (UID) | -| `email` | `String?` | The user's email address (null for phone-only auth) | -| `fullName` | `String?` | The user's display name from Firebase | -| `image` | `Uri?` | URL to the user's profile image | -| `verifiedEmail` | `bool?` | Whether the email is verified | -| `phone` | `String?` | The user's phone number (for phone auth) | - -Example of accessing these properties: - -```dart -firebaseAccountDetailsValidation: (accountDetails) { - print('Firebase UID: ${accountDetails.userIdentifier}'); - print('Email: ${accountDetails.email}'); - print('Email verified: ${accountDetails.verifiedEmail}'); - print('Display name: ${accountDetails.fullName}'); - print('Profile image: ${accountDetails.image}'); - print('Phone: ${accountDetails.phone}'); - - // Custom validation logic - if (accountDetails.email == null && accountDetails.phone == null) { - throw Exception('Either email or phone is required'); - } -}, -``` +- `userIdentifier` (`String`): Firebase UID. +- `email` (`String?`): Email address, or `null` for phone-only sign-in. +- `fullName` (`String?`): Display name from Firebase. +- `image` (`Uri?`): Profile image URL. +- `verifiedEmail` (`bool?`): Whether the email is verified. +- `phone` (`String?`): Phone number, only populated for phone authentication. -:::info -The properties available depend on the Firebase authentication method used. For example, `phone` is only populated for phone authentication, and `email` may be null if the user signed in with phone only. -::: +Which properties are populated depends on the Firebase sign-in method the user chose. For example, `phone` is only populated for phone authentication, and `email` may be `null` if the user signed in with phone only. -### Reacting to account creation +## Reacting to auth user creation -You can use the `onAfterFirebaseAccountCreated` callback to run logic after a new Firebase account has been created and linked to an auth user. This callback is only invoked for new accounts, not for returning users. +[`onBeforeAuthUserCreated`](https://pub.dev/documentation/serverpod_auth_idp_server/latest/core/AuthUsersConfig/onBeforeAuthUserCreated.html) and [`onAfterAuthUserCreated`](https://pub.dev/documentation/serverpod_auth_idp_server/latest/core/AuthUsersConfig/onAfterAuthUserCreated.html) are global callbacks on `AuthUsersConfig`. They fire for every identity provider, not just Firebase. See [Working with users](../../working-with-users#reacting-to-the-user-created-event) for full details. -This callback is complimentary to the [core `onAfterAuthUserCreated` callback](../../working-with-users#reacting-to-the-user-created-event) to perform side-effects that are specific to a login on this provider - like storing analytics, sending a welcome email, or storing additional data. +The example below uses Firebase phone numbers as the trigger for assigning a `phone-verified` scope at sign-up, and persists the Firebase UID for later admin lookups: ```dart -final firebaseIdpConfig = FirebaseIdpConfigFromPasswords( - onAfterFirebaseAccountCreated: ( - session, - authUser, - firebaseAccount, { - required transaction, - }) async { - // e.g. store additional data, send a welcome email, or log for analytics - }, +pod.initializeAuthServices( + tokenManagerBuilders: [ + JwtConfigFromPasswords(), + ], + identityProviderBuilders: [ + FirebaseIdpConfigFromPasswords(), + ], + authUsersConfig: AuthUsersConfig( + onBeforeAuthUserCreated: ( + session, + scopes, + blocked, { + required transaction, + }) { + return ( + scopes: {...scopes, Scope('user')}, + blocked: blocked, + ); + }, + onAfterAuthUserCreated: ( + session, + authUser, { + required transaction, + }) async { + // e.g. send a welcome email, log for analytics + }, + ), ); ``` -:::info -This callback runs inside the same database transaction as the account creation. Throwing an exception inside this callback will abort the process. If you perform external side-effects, make sure to safeguard them with a try/catch to prevent unwanted failures. +:::warning +Both callbacks run inside the same database transaction as the account creation. Throwing an exception inside either callback aborts the sign-up. Wrap external side-effects (email sending, analytics) in `try`/`catch` so a third-party outage does not block new sign-ups. ::: -:::caution -If you need to assign Serverpod scopes based on provider account data, note that updating the database alone (via `AuthServices.instance.authUsers.update()`) is **not enough** for the current login session. The token issuance uses the in-memory `authUser.scopes`, which is already set before this callback runs. You would need to update `authUser.scopes` as well for the scopes to be reflected in the issued tokens. For assigning scopes at creation time, consider using `onBeforeAuthUserCreated` to set scopes based on data collected earlier in the flow. -::: +## FirebaseIdpConfig parameter reference + +| Parameter | Type | Required | Description | +| --- | --- | --- | --- | +| `credentials` | `FirebaseServiceAccountCredentials` | Yes | Firebase service account credentials for verifying ID tokens. Can be loaded via `fromJsonString`, `fromJsonFile`, or `fromJson`. When using `FirebaseIdpConfigFromPasswords`, this is loaded automatically from the `firebaseServiceAccountKey` key in `passwords.yaml` or the `SERVERPOD_PASSWORD_firebaseServiceAccountKey` environment variable. | +| `firebaseAccountDetailsValidation` | `FirebaseAccountDetailsValidation?` | No | Custom validation callback for Firebase account details before allowing sign-in. By default, validates that email is verified when present (phone-only auth is allowed). | diff --git a/docs/06-concepts/11-authentication/04-providers/06-firebase/03-customizing-the-ui.md b/docs/06-concepts/11-authentication/04-providers/06-firebase/03-customizing-the-ui.md index de367fec..32ced2eb 100644 --- a/docs/06-concepts/11-authentication/04-providers/06-firebase/03-customizing-the-ui.md +++ b/docs/06-concepts/11-authentication/04-providers/06-firebase/03-customizing-the-ui.md @@ -1,171 +1,176 @@ # Customizing the UI -When using the Firebase identity provider, you build your authentication UI using Firebase's own packages (`firebase_auth` or `firebase_ui_auth`). The `FirebaseAuthController` handles syncing the authenticated Firebase user with your Serverpod backend. +When using the Firebase identity provider, you build your authentication UI on top of [`firebase_auth`](https://pub.dev/packages/firebase_auth), optionally with [`firebase_ui_auth`](https://pub.dev/packages/firebase_ui_auth) for the pre-built sign-in screens. The [`FirebaseAuthController`](https://pub.dev/documentation/serverpod_auth_idp_flutter_firebase/latest/serverpod_auth_idp_flutter_firebase/FirebaseAuthController-class.html) handles syncing the authenticated Firebase user with your Serverpod backend. This page breaks down the gate widget shown in the [setup guide](./setup#present-the-authentication-ui) and then covers building a fully custom UI with `firebase_auth` directly. -:::info -Unlike other Serverpod identity providers, Firebase does not provide built-in sign-in widgets. You use Firebase's official packages for the UI, then sync the result with Serverpod using `FirebaseAuthController`. -::: +## Anatomy of the gate widget + +The [setup guide](./setup#present-the-authentication-ui) shows a `SignInScreen` widget that wraps your app and only renders the `child` once the user is authenticated. Three pieces drive that flow: -## Using firebase_ui_auth SignInScreen +### FirebaseAuthController -The `firebase_ui_auth` package provides a complete `SignInScreen` widget that handles the entire authentication flow. Here's a full example: +The controller manages the sync between Firebase authentication state and your Serverpod session. The `client` argument is the global Serverpod `Client` instance created in `main.dart` (see [Initialize Firebase and Serverpod](./setup#3-initialize-firebase-and-serverpod)). ```dart -import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; -import 'package:firebase_ui_auth/firebase_ui_auth.dart' as firebase_ui; -import 'package:flutter/material.dart'; -import 'package:serverpod_auth_idp_flutter_firebase/serverpod_auth_idp_flutter_firebase.dart'; +controller = FirebaseAuthController( + client: client, + onAuthenticated: () => context.showSnackBar( + message: 'User authenticated.', + backgroundColor: Colors.green, + ), + onError: (error) => context.showSnackBar( + message: 'Authentication failed: $error', + backgroundColor: Colors.red, + ), +); +``` + +:::warning +Do not navigate to a home screen inside `onAuthenticated`. This callback fires every time a sync succeeds, including on app restart. Instead, gate the UI on `controller.isAuthenticated` (as the setup example does), or listen to `client.auth.authInfoListenable`. +::: -class AuthScreen extends StatefulWidget { - const AuthScreen({super.key}); +### Action handlers - @override - State createState() => _AuthScreenState(); -} +`firebase_ui_auth` emits state changes through [`AuthStateChangeAction`](https://pub.dev/documentation/firebase_ui_auth/latest/firebase_ui_auth/AuthStateChangeAction-class.html). Two handlers cover the cases that need a Serverpod sync: -class _AuthScreenState extends State { - late final FirebaseAuthController controller; - - @override - void initState() { - super.initState(); - controller = FirebaseAuthController( - client: client, - onAuthenticated: () { - // User successfully synced with Serverpod - // NOTE: Do not navigate here - use client.auth.authInfoListenable instead - }, - onError: (error) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error: $error')), - ); - }, - ); - } +- [`SignedIn`](https://pub.dev/documentation/firebase_ui_auth/latest/firebase_ui_auth/SignedIn-class.html) -- a returning user signed in. +- [`UserCreated`](https://pub.dev/documentation/firebase_ui_auth/latest/firebase_ui_auth/UserCreated-class.html) -- a brand-new account was just created. - @override - void dispose() { - controller.dispose(); - super.dispose(); - } +Both call [`controller.login(user)`](https://pub.dev/documentation/serverpod_auth_idp_flutter_firebase/latest/serverpod_auth_idp_flutter_firebase/FirebaseAuthController/login.html) so the Firebase user is registered with Serverpod: - @override - Widget build(BuildContext context) { - return firebase_ui.SignInScreen( - providers: [ - firebase_ui.EmailAuthProvider(), - // Add more providers as needed: - // firebase_ui.PhoneAuthProvider(), - // firebase_ui.GoogleProvider(clientId: '...'), - ], - actions: [ - firebase_ui.AuthStateChangeAction((context, state) async { - final user = firebase_auth.FirebaseAuth.instance.currentUser; - if (user != null) { - await controller.login(user); - } - }), - firebase_ui.AuthStateChangeAction((context, state) async { - final user = firebase_auth.FirebaseAuth.instance.currentUser; - if (user != null) { - await controller.login(user); - } - }), - ], - ); - } +```dart +Future _handleAuthStateChange() async { + final user = firebase_auth.FirebaseAuth.instance.currentUser; + if (user != null) await controller.login(user); } ``` -The `SignInScreen` automatically handles the UI for all configured providers. You only need to connect the Firebase authentication result to Serverpod using the `FirebaseAuthController`. +### controller.isAuthenticated -Refer to the [firebase_ui_auth documentation](https://pub.dev/packages/firebase_ui_auth) for configuration details and available providers. +The `build` method checks `controller.isAuthenticated` to decide whether to show the sign-in UI or the wrapped `child`: -## Using the FirebaseAuthController +```dart +if (controller.isAuthenticated) return widget.child; +``` -The `FirebaseAuthController` manages the synchronization between Firebase authentication state and Serverpod sessions. +Add more providers to the `firebase_ui.SignInScreen` as needed (see the [firebase_ui_auth documentation](https://pub.dev/packages/firebase_ui_auth)): ```dart -import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; -import 'package:serverpod_auth_idp_flutter_firebase/serverpod_auth_idp_flutter_firebase.dart'; - -final controller = FirebaseAuthController( - client: client, - onAuthenticated: () { - // User successfully synced with Serverpod - // NOTE: Do not navigate here - use client.auth.authInfoListenable instead - }, - onError: (error) { - // Handle errors - }, -); +providers: [ + firebase_ui.EmailAuthProvider(), + // firebase_ui.PhoneAuthProvider(), + // firebase_ui.GoogleProvider(clientId: '...'), +], ``` -### FirebaseAuthController State Management +## Controller reference + +### The login method -Your widget should render the appropriate UI based on the controller's state: +After a user signs in through Firebase (using any method), pass the `firebase_auth.User` to the controller to sync with Serverpod: ```dart -// Check current state -final state = controller.state; // FirebaseAuthState enum +final firebaseUser = firebase_auth.FirebaseAuth.instance.currentUser; -// Check if loading -final isLoading = controller.isLoading; +if (firebaseUser != null) { + await controller.login(firebaseUser); +} +``` -// Check if authenticated -final isAuthenticated = controller.isAuthenticated; +### State management -// Get error message -final errorMessage = controller.errorMessage; +The controller is a `ChangeNotifier`, so it notifies your widget whenever its state changes. Register a listener inside `initState` and call `setState` to trigger a rebuild: -// Get error object -final error = controller.error; +```dart +@override +void initState() { + super.initState(); + controller = FirebaseAuthController(client: client); -// Listen to state changes -controller.addListener(() { - setState(() { - // Rebuild UI when controller state changes + controller.addListener(() { + if (mounted) setState(() {}); }); -}); +} ``` -### FirebaseAuthController States - -- `FirebaseAuthState.idle` - Ready for user interaction. -- `FirebaseAuthState.loading` - Processing authentication with Serverpod. -- `FirebaseAuthState.error` - An error occurred. -- `FirebaseAuthState.authenticated` - Successfully authenticated with Serverpod. +The controller exposes a few properties for your `build` method: -### The login method +- `controller.isLoading` -- Whether the controller is processing a request. +- `controller.isAuthenticated` -- Whether the user is authenticated. +- `controller.errorMessage` -- The error message string, if any. +- `controller.error` -- The raw error object, for advanced error handling. -The `login` method accepts a `firebase_auth.User` object and syncs it with Serverpod: +Use them in your `build` method to render the right UI for the current state: ```dart -// Get the current Firebase user after sign-in -final firebaseUser = firebase_auth.FirebaseAuth.instance.currentUser; +@override +Widget build(BuildContext context) { + if (controller.isLoading) { + return const Center(child: CircularProgressIndicator()); + } -if (firebaseUser != null) { - await controller.login(firebaseUser); + if (controller.errorMessage != null) { + return Center(child: Text('Error: ${controller.errorMessage}')); + } + + if (controller.isAuthenticated) { + return widget.child; + } + + // Otherwise, show the sign-in UI. + return const SignInForm(); +} +``` + +If you need finer-grained control, switch on `controller.state` directly: + +```dart +@override +Widget build(BuildContext context) { + return switch (controller.state) { + FirebaseAuthState.idle => const SignInForm(), + FirebaseAuthState.loading => const Center(child: CircularProgressIndicator()), + FirebaseAuthState.error => Center(child: Text('Error: ${controller.errorMessage}')), + FirebaseAuthState.authenticated => widget.child, + }; } ``` -## Integration patterns +## Using firebase_auth directly -### Using firebase_auth directly +For full control over the authentication UI, use the [`firebase_auth`](https://pub.dev/packages/firebase_auth) package directly. Build your own sign-in UI, call [`FirebaseAuth`](https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth-class.html) methods, then pass the result to `controller.login()`: -For full control over the authentication UI, use the `firebase_auth` package directly. The basic flow is: +```dart +import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; -1. Build your own sign-in UI with text fields and buttons. -2. Call Firebase authentication methods (e.g., `signInWithEmailAndPassword`). -3. On success, pass the `firebase_auth.User` to `controller.login()`. -4. Handle errors from both Firebase and the Serverpod sync. +final credential = await firebase_auth.FirebaseAuth.instance + .signInWithEmailAndPassword( + email: emailController.text, + password: passwordController.text, +); + +final user = credential.user; +if (user != null) { + await controller.login(user); +} +``` -Refer to the [firebase_auth documentation](https://pub.dev/packages/firebase_auth) for available authentication methods. +Refer to the [firebase_auth documentation](https://pub.dev/packages/firebase_auth) for all available authentication methods. ### Listening to Firebase auth state changes -For apps that need to react to Firebase auth state changes automatically, listen to `FirebaseAuth.instance.authStateChanges()` and call `controller.login()` when a user signs in. +For apps that need to react to Firebase auth state changes automatically (e.g., the user signs in on another device, or the session is restored after an app restart), subscribe to [`FirebaseAuth.instance.authStateChanges()`](https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/authStateChanges.html). The stream emits the current `User` whenever Firebase's auth state changes; pass that user to `controller.login()` to sync with Serverpod. + +```dart +firebase_auth.FirebaseAuth.instance.authStateChanges().listen((user) async { + if (user == null) return; + + // Guard against re-syncing an already authenticated user. + if (!controller.isAuthenticated) { + await controller.login(user); + } +}); +``` -:::note -When using the auth state listener pattern, check the Serverpod auth state before calling `login()` to prevent re-syncing an already authenticated user. +:::warning +Always check `controller.isAuthenticated` before calling `login()` inside the listener. The stream fires on every app start with the cached Firebase user, and `login()` would otherwise re-sync on every launch. ::: diff --git a/docs/06-concepts/11-authentication/04-providers/06-firebase/04-admin-operations.md b/docs/06-concepts/11-authentication/04-providers/06-firebase/04-admin-operations.md index f24df302..33b42de1 100644 --- a/docs/06-concepts/11-authentication/04-providers/06-firebase/04-admin-operations.md +++ b/docs/06-concepts/11-authentication/04-providers/06-firebase/04-admin-operations.md @@ -1,62 +1,53 @@ -# Admin Operations +# Admin operations -The Firebase identity provider provides admin operations through `FirebaseIdpAdmin` for managing Firebase-authenticated accounts. These operations are useful for administrative tasks and account management. +The Firebase identity provider provides admin operations through `FirebaseIdpAdmin` for managing Firebase-authenticated accounts on the server. Common use cases include linking an existing Serverpod user to a Firebase account, looking up accounts for support tools, and cleaning up orphaned accounts. + +:::warning +Admin operations should only be called from secure server-side code. Do not expose these methods directly through client endpoints without proper authorization checks. +::: ## Accessing the FirebaseIdpAdmin -You can access the admin operations through the `AuthServices.instance.firebaseIdp` property: +You can access the admin operations through the `AuthServices.instance.firebaseIdp` property. This requires that the Firebase identity provider is already configured (see [setup](./setup#1-add-the-firebase-identity-provider)). ```dart import 'package:serverpod_auth_idp_server/providers/firebase.dart'; import 'package:serverpod_auth_idp_server/core.dart'; -// Get the FirebaseIdp instance final firebaseIdp = AuthServices.instance.firebaseIdp; - -// Access admin operations final admin = firebaseIdp.admin; ``` -## Account Management +## Finding accounts -The admin API provides methods for managing Firebase-authenticated accounts. +Pick the finder that matches the identifier you have on hand: -### Finding Accounts +- `findAccountByEmail` when you have the user's email (e.g., from a support ticket). +- `findAccountByAuthUserId` when you have a Serverpod auth user ID and want the linked Firebase account. +- `findUserByFirebaseUserId` when you have a Firebase UID and want the Serverpod user it is linked to. ```dart -// Find an account by email -final account = await admin.findAccountByEmail( +final accountByEmail = await admin.findAccountByEmail( session, email: 'user@example.com', ); -// Find an account by Serverpod auth user ID -final account = await admin.findAccountByAuthUserId( +final accountByAuthUser = await admin.findAccountByAuthUserId( session, authUserId: authUserId, ); -// Find the Serverpod user ID by Firebase UID final userId = await admin.findUserByFirebaseUserId( session, userIdentifier: 'firebase-uid', ); ``` -### Linking Firebase Authentication +## Linking Firebase authentication -Link an existing Serverpod user to a Firebase account: - -```dart -// Link a Firebase account to an existing user -final firebaseAccount = await admin.linkFirebaseAuthentication( - session, - authUserId: authUserId, - accountDetails: accountDetails, -); -``` +Link an existing Serverpod user to a Firebase account. This is useful when migrating users from another auth provider to Firebase, or when manually linking accounts in an admin tool. -The `accountDetails` parameter is a `FirebaseAccountDetails` record containing the Firebase user information. You can obtain this from a Firebase ID token using the `fetchAccountDetails` method: +First, obtain the account details from a Firebase ID token, then link them to an existing user: ```dart // Fetch account details from a Firebase ID token @@ -65,7 +56,7 @@ final accountDetails = await admin.fetchAccountDetails( idToken: firebaseIdToken, ); -// Then link the account +// Link the account to an existing user await admin.linkFirebaseAuthentication( session, authUserId: existingUserId, @@ -73,42 +64,36 @@ await admin.linkFirebaseAuthentication( ); ``` -### Deleting Accounts +## Deleting accounts + +Use `deleteFirebaseAccount` when you have a Firebase UID, or `deleteFirebaseAccountByAuthUserId` to remove every Firebase link attached to a single Serverpod user: ```dart -// Delete a Firebase account by Firebase UID -final deletedAccount = await admin.deleteFirebaseAccount( +final deletedByUid = await admin.deleteFirebaseAccount( session, userIdentifier: 'firebase-uid', ); -// Delete all Firebase accounts for a Serverpod user -final deletedAccount = await admin.deleteFirebaseAccountByAuthUserId( +final deletedByAuthUser = await admin.deleteFirebaseAccountByAuthUserId( session, authUserId: authUserId, ); ``` -:::info -Deleting a Firebase account only removes the link between the Firebase authentication and the Serverpod user. It does not delete the user from your Serverpod database or from Firebase itself. +:::warning +Deleting a Firebase account only removes the link between Firebase authentication and the Serverpod user. The Serverpod user stays in your database, and the Firebase user stays in your Firebase project. You must delete those separately if that is what you want. ::: ## FirebaseIdpUtils -The `FirebaseIdpUtils` class provides utility functions for working with Firebase authentication: +The `FirebaseIdpUtils` class provides a lower-level `authenticate` method for when you need to verify a Firebase ID token and create or update the associated Serverpod user in custom endpoint logic (outside the normal sign-in flow): ```dart final utils = firebaseIdp.utils; -// Authenticate a user with a Firebase ID token -// This creates the account if it doesn't exist final authSuccess = await utils.authenticate( session, idToken: firebaseIdToken, - transaction: transaction, // optional + transaction: transaction, ); ``` - -:::warning -Admin operations should only be called from secure server-side code. Do not expose these methods directly through client endpoints without proper authorization checks. -::: diff --git a/docs/06-concepts/11-authentication/04-providers/06-firebase/05-troubleshooting.md b/docs/06-concepts/11-authentication/04-providers/06-firebase/05-troubleshooting.md new file mode 100644 index 00000000..02d2b4f8 --- /dev/null +++ b/docs/06-concepts/11-authentication/04-providers/06-firebase/05-troubleshooting.md @@ -0,0 +1,198 @@ +# Troubleshooting + +This page helps you identify common Firebase authentication failures with Serverpod, explains why they occur, and shows how to resolve them. For issues with Firebase itself, see the [Firebase Auth documentation](https://firebase.google.com/docs/auth). + +## Setup checklist + +Go through this before investigating a specific error. Most problems come from a missed step. + +#### Firebase Console + +- [ ] Create a **Firebase project** in the [Firebase Console](https://console.firebase.google.com/). +- [ ] Add an app for **every platform you support** (iOS, Android, Web) inside the Firebase project. Each platform needs its own registration. +- [ ] Generate a **service account key** from **Project settings** > **Service accounts**. +- [ ] Enable the **authentication methods** you want to use under **Authentication** > **Sign-in method**. +- [ ] Confirm **Firebase App Check** is **disabled** (enable it later only after the client integration is in place). +- [ ] In **Authentication** > **Settings** > **Authorized domains**, the list includes `localhost` for development and your production domain when you ship. + +#### Server + +- [ ] Paste the service account JSON into `firebaseServiceAccountKey` in `config/passwords.yaml`. See [Store the service account key](./setup#store-the-service-account-key). +- [ ] Confirm the `project_id` inside `firebaseServiceAccountKey` matches the Firebase project the client is using. +- [ ] Add `FirebaseIdpConfigFromPasswords()` to `identityProviderBuilders` in `server.dart`. +- [ ] Create a `FirebaseIdpEndpoint` file in `lib/src/auth/` extending `FirebaseIdpBaseEndpoint`. +- [ ] Run **`serverpod generate`**, then **`serverpod create-migration`**, then apply migrations with `dart run bin/main.dart --apply-migrations`. + +#### Client + +- [ ] Install **`firebase_core`**, **`firebase_auth`**, and **`serverpod_auth_idp_flutter_firebase`** in your Flutter project. +- [ ] Run **`flutterfire configure`** so `firebase_options.dart` is generated. +- [ ] Verify the platform config files are in place: `GoogleService-Info.plist` (iOS, in the Runner target) and `google-services.json` (Android, in `android/app/`). +- [ ] Import `firebase_options.dart` and call **`Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform)`** before creating the Serverpod client. +- [ ] Call **`client.auth.initializeFirebaseSignIn()`** after `client.auth.initialize()` in your Flutter app's `main.dart`. +- [ ] Call **`controller.login(user)`** after Firebase authentication completes. + +## Server crashes on first Firebase sign-in with "no such table" + +**Problem:** The server builds and starts, but crashes when a user tries Firebase sign-in. The error cites a missing table (like `serverpod_auth_idp_firebase_account`). + +**Cause:** `serverpod generate` has been run, but you didn't create or apply the accompanying database migration. + +**Resolution:** Create and apply the migration: + +```bash +serverpod generate +serverpod create-migration +dart run bin/main.dart --apply-migrations +``` + +## Token verification fails with "invalid signature" + +**Problem:** The server rejects Firebase ID tokens with a signature verification error. + +**Cause:** The service account key in `passwords.yaml` does not belong to the same Firebase project that the client is using, or the YAML indentation broke the JSON. + +**Resolution:** + +1. Verify the `project_id` in your `firebaseServiceAccountKey` matches the project in `firebase_options.dart`. +2. Check that the JSON in `passwords.yaml` is properly indented under the `|` block scalar. All lines must be indented consistently. + +## Token verification fails with "token expired" + +**Problem:** The server rejects Firebase ID tokens with a token expiration error. + +**Cause:** Firebase ID tokens expire after one hour. If the server's system clock is significantly off, valid tokens may appear expired. + +**Resolution:** Check that the server's system clock is accurate. If the client token is genuinely expired (e.g., the user's app was backgrounded for a long time), the client should re-authenticate with Firebase to obtain a fresh ID token before calling `controller.login()`. + +## Server fails to parse firebaseServiceAccountKey from passwords.yaml + +**Problem:** The server crashes on startup with a JSON parsing error related to `firebaseServiceAccountKey`. + +**Cause:** The YAML block scalar indentation is incorrect. The `firebaseServiceAccountKey` key uses `|` (literal block scalar), which requires every line of the JSON to be indented at the same level relative to the key. + +**Resolution:** Make sure the JSON block is indented consistently under the `|`: + +```yaml +development: + firebaseServiceAccountKey: | + { + "type": "service_account", + "project_id": "...", + "private_key": "..." + } +``` + +Every line of the JSON must be indented by at least one level more than `firebaseServiceAccountKey:`. Mixing tabs and spaces can also cause issues. + +## FirebaseAuth.instance.currentUser is null after sign-in + +**Problem:** After the Firebase sign-in flow completes, `FirebaseAuth.instance.currentUser` returns null, so `controller.login(user)` never gets called. + +**Cause:** `Firebase.initializeApp()` was not called before attempting authentication, or was called with the wrong options. + +**Resolution:** Make sure `Firebase.initializeApp()` is called in your `main()` function before any Firebase operations: + +```dart +await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, +); +``` + +If you haven't run `flutterfire configure`, do so to generate the `firebase_options.dart` file. + +## Sign-in succeeds in Firebase but fails to sync with Serverpod + +**Problem:** The user authenticates with Firebase (the Firebase UI shows success), but the Serverpod session is never created. The `onError` callback on `FirebaseAuthController` fires. + +**Cause:** The Firebase ID token could not be verified by the server. Check the server logs first, the error message will narrow this down to one of the following causes: + +1. **Missing service account key:** The `firebaseServiceAccountKey` is not present in `passwords.yaml`, or the JSON is invalid. +2. **Missing endpoint:** You did not create the endpoint class extending `FirebaseIdpBaseEndpoint`. Without it, the client has no endpoint to call. +3. **Missing migration:** The provider's database tables don't exist yet. Apply migrations with `dart run bin/main.dart --apply-migrations`. +4. **Project mismatch:** The service account key belongs to a different Firebase project than the one configured in your Flutter app. Compare `project_id` in `firebaseServiceAccountKey` against the project in `firebase_options.dart`. +5. **App Check enabled prematurely:** If you enabled Firebase App Check before the client integration is in place, every request will be rejected with an App Check assertion error. Disable App Check until the client is wired up. + +## Email validation rejects phone-only users + +**Problem:** Users who sign in with phone authentication are rejected with a `FirebaseUserInfoMissingDataException`. + +**Cause:** A custom `firebaseAccountDetailsValidation` callback requires a verified email, but phone-only users don't have an email. The default validation allows phone-only authentication. If you overrode the default with a stricter check, you need to account for phone-only sign-in. + +**Resolution:** Update your validation to allow phone-only authentication by checking for the presence of an email before requiring verification: + +```dart +firebaseAccountDetailsValidation: (accountDetails) { + if (accountDetails.email != null && accountDetails.verifiedEmail != true) { + throw FirebaseUserInfoMissingDataException(); + } +}, +``` + +## User signed out of Serverpod but still signed in to Firebase + +**Problem:** After calling sign-out on Serverpod, the user's Firebase session remains active. The next time the app opens, Firebase still has an authenticated user. + +**Cause:** `initializeFirebaseSignIn()` was not called during app initialization. This method sets up the automatic sign-out sync between Serverpod and Firebase. + +**Resolution:** Make sure you call `initializeFirebaseSignIn()` after initializing the Serverpod client: + +```dart +client.auth.initialize(); +client.auth.initializeFirebaseSignIn(); +``` + +## FlutterFire configure fails or generates wrong config + +**Problem:** `flutterfire configure` fails, or the generated `firebase_options.dart` has wrong project details. + +**Cause:** The Firebase CLI is not installed, not logged in, or is pointing to the wrong project. + +**Resolution:** + +1. Install the Firebase CLI: `npm install -g firebase-tools` +2. Log in: `firebase login` +3. Install FlutterFire CLI: `dart pub global activate flutterfire_cli` +4. Run `flutterfire configure` and select the correct project when prompted. + +See the [FlutterFire CLI documentation](https://firebase.flutter.dev/docs/cli/) for more details. + +## Firebase UI auth actions not firing + +**Problem:** The `AuthStateChangeAction` or `AuthStateChangeAction` actions on the `SignInScreen` never fire, so `controller.login()` is never called. + +**Cause:** The action types don't match the authentication state changes from the providers you configured. For example, using `EmailAuthProvider` but only listening for one of the two states. + +**Resolution:** Make sure you have actions for both `SignedIn` (returning users) and `UserCreated` (new users). See the [Customizing the UI](./customizing-the-ui#anatomy-of-the-gate-widget) page for the complete code example. + +## Platform-specific Firebase SDK configuration issues + +**Problem:** Firebase operations fail on a specific platform (iOS, Android, or Web) with errors about missing configuration. + +**Cause:** The `flutterfire configure` command may not have configured all platforms, or platform-specific files were not placed correctly. + +**Resolution:** + +1. Re-run `flutterfire configure` and ensure you select all platforms you want to support. +2. For **iOS**: Verify that `GoogleService-Info.plist` is included in the Xcode project's Runner target. +3. For **Android**: Verify that `google-services.json` is in `android/app/`. +4. For **Web**: Verify that the Firebase config is loaded in `web/index.html` or via `firebase_options.dart`. + +## `Firebase.initializeApp()` throws `channel-error` on web in production + +**Problem:** A Flutter web app works fine in `flutter run -d chrome` (debug), but the production build throws `channel-error` from `FirebaseCoreHostApi.initializeCore` as soon as `Firebase.initializeApp()` runs. The browser console shows obfuscated frames like `aJs.$2` and `JX.fI` in `main.dart.js` with no clear message. + +**Cause:** Flutter's incremental build cache under `.dart_tool/flutter_build/` can silently drop `firebase_core_web`'s plugin registration. The on-disk plugin registrant file still lists `FirebaseCoreWeb.registerWith(registrar)`, but stale kernel `.dill` artifacts mean `dart2js` compiles `main.dart.js` without the wiring. `FirebasePlatform.instance` then stays as the default `MethodChannelFirebase`, and `Firebase.initializeApp()` goes through a pigeon channel with no handler. The error sounds native, but it is purely a Flutter web build-cache issue. Debug mode (DDC) does not tree-shake or cache the same way as `dart2js` (release), so it hides the bug entirely. + +**Resolution:** Run `flutter clean` before building so the registration is included: + +```bash +flutter clean +flutter build web +``` + +For Serverpod Cloud projects, add `flutter clean` ahead of `flutter build web` in your `flutter_build` script (or whatever `scripts.pre_deploy` runs). + +:::tip +To make minified stack traces readable while you debug an issue like this, build **locally** with `flutter build web --source-maps` and load that build (or the resulting `.map` file) in your browser's dev tools. Do **not** deploy the `.map` file to a public-facing host: it contains your full Dart source. If you need readable production stack traces continuously, upload source maps to an error-monitoring service (Sentry, Datadog) rather than serving them next to `main.dart.js`. +::: diff --git a/static/img/authentication/providers/firebase/1-create-project.png b/static/img/authentication/providers/firebase/1-create-project.png new file mode 100644 index 00000000..196bdcec Binary files /dev/null and b/static/img/authentication/providers/firebase/1-create-project.png differ diff --git a/static/img/authentication/providers/firebase/10-sign-in-methods.png b/static/img/authentication/providers/firebase/10-sign-in-methods.png new file mode 100644 index 00000000..f7d611b5 Binary files /dev/null and b/static/img/authentication/providers/firebase/10-sign-in-methods.png differ diff --git a/static/img/authentication/providers/firebase/11-flutterfire-configure.png b/static/img/authentication/providers/firebase/11-flutterfire-configure.png new file mode 100644 index 00000000..ddb97290 Binary files /dev/null and b/static/img/authentication/providers/firebase/11-flutterfire-configure.png differ diff --git a/static/img/authentication/providers/firebase/2-project-name.png b/static/img/authentication/providers/firebase/2-project-name.png new file mode 100644 index 00000000..f7997314 Binary files /dev/null and b/static/img/authentication/providers/firebase/2-project-name.png differ diff --git a/static/img/authentication/providers/firebase/3-project-dashboard.png b/static/img/authentication/providers/firebase/3-project-dashboard.png new file mode 100644 index 00000000..0f50c02f Binary files /dev/null and b/static/img/authentication/providers/firebase/3-project-dashboard.png differ diff --git a/static/img/authentication/providers/firebase/4-service-accounts.png b/static/img/authentication/providers/firebase/4-service-accounts.png new file mode 100644 index 00000000..b8217bba Binary files /dev/null and b/static/img/authentication/providers/firebase/4-service-accounts.png differ diff --git a/static/img/authentication/providers/firebase/5-generate-key.png b/static/img/authentication/providers/firebase/5-generate-key.png new file mode 100644 index 00000000..8e7158e4 Binary files /dev/null and b/static/img/authentication/providers/firebase/5-generate-key.png differ diff --git a/static/img/authentication/providers/firebase/6-generate-key-confirm.png b/static/img/authentication/providers/firebase/6-generate-key-confirm.png new file mode 100644 index 00000000..b7825167 Binary files /dev/null and b/static/img/authentication/providers/firebase/6-generate-key-confirm.png differ diff --git a/static/img/authentication/providers/firebase/7-navigate-auth.png b/static/img/authentication/providers/firebase/7-navigate-auth.png new file mode 100644 index 00000000..dda31ece Binary files /dev/null and b/static/img/authentication/providers/firebase/7-navigate-auth.png differ diff --git a/static/img/authentication/providers/firebase/8-enable-auth.png b/static/img/authentication/providers/firebase/8-enable-auth.png new file mode 100644 index 00000000..c674f57b Binary files /dev/null and b/static/img/authentication/providers/firebase/8-enable-auth.png differ diff --git a/static/img/authentication/providers/firebase/9-enable-provider.png b/static/img/authentication/providers/firebase/9-enable-provider.png new file mode 100644 index 00000000..7846e7b1 Binary files /dev/null and b/static/img/authentication/providers/firebase/9-enable-provider.png differ