diff --git a/docs/06-concepts/11-authentication/04-providers/05-firebase/01-setup.md b/docs/06-concepts/11-authentication/04-providers/05-firebase/01-setup.md new file mode 100644 index 00000000..0038834e --- /dev/null +++ b/docs/06-concepts/11-authentication/04-providers/05-firebase/01-setup.md @@ -0,0 +1,258 @@ +# 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. + +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. + +:::caution +You need to install the auth module before you continue, see [Setup](../../setup). +::: + +## Prerequisites + +Before setting up Firebase authentication, ensure you have: + +- A Firebase project (create one at [Firebase Console](https://console.firebase.google.com/)) +- Firebase CLI installed (`npm install -g firebase-tools`) +- FlutterFire CLI installed (`dart pub global activate flutterfire_cli`) +- At least one authentication method enabled in your Firebase project + +## Create your credentials + +### Generate Service Account Key + +The server needs service account credentials to verify Firebase ID tokens. To create a new key: + +1. Go to the [Firebase Console](https://console.firebase.google.com/) +2. Select your project +3. Navigate to **Project settings** > **Service accounts** +4. Click **Generate new private key**, then **Generate key** + +![Service account](/img/authentication/providers/firebase/1-server-key.png) + +This downloads a JSON file containing your service account credentials. + +### Enable Authentication Methods + +In the Firebase Console, enable the authentication methods you want to support: + +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 + +![Auth provider](/img/authentication/providers/firebase/2-auth-provider.png) + +## Server-side configuration + +### Store the Service Account Key + +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. + +```yaml +development: + firebaseServiceAccountKey: | + { + "type": "service_account", + "project_id": "your-project-id", + "private_key_id": "...", + "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n", + "client_email": "firebase-adminsdk-xxxxx@your-project-id.iam.gserviceaccount.com", + "client_id": "...", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token" + } +``` + +:::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. +::: + +### Configure the Firebase Identity Provider + +In your main `server.dart` file, configure 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'; + +void run(List args) async { + final pod = Serverpod( + args, + Protocol(), + Endpoints(), + ); + + pod.initializeAuthServices( + tokenManagerBuilders: [ + JwtConfigFromPasswords(), + ], + identityProviderBuilders: [ + FirebaseIdpConfig( + credentials: FirebaseServiceAccountCredentials.fromJsonString( + pod.getPassword('firebaseServiceAccountKey')!, + ), + ), + ], + ); + + await pod.start(); +} +``` + +:::tip +You can use `FirebaseIdpConfigFromPasswords()` to automatically load credentials from `config/passwords.yaml` or the `SERVERPOD_PASSWORD_firebaseServiceAccountKey` environment variable: + +```dart +identityProviderBuilders: [ + FirebaseIdpConfigFromPasswords(), +], +``` + +::: + +### Expose the Endpoint + +Create an endpoint that extends `FirebaseIdpBaseEndpoint` to expose the Firebase authentication API: + +```dart +import 'package:serverpod_auth_idp_server/providers/firebase.dart'; + +class FirebaseIdpEndpoint extends FirebaseIdpBaseEndpoint {} +``` + +### Generate and Migrate + +Run the following commands to generate client code and create the database migration: + +```bash +serverpod generate +``` + +Then create and apply the migration: + +```bash +serverpod create-migration +docker compose up --build --detach +dart run bin/main.dart --role maintenance --apply-migrations +``` + +More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration). + +### Basic configuration options + +- `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. + +## Client-side configuration + +### Install Required Packages + +Add the Firebase and Serverpod authentication packages to your Flutter project: + +```bash +flutter pub add firebase_core firebase_auth serverpod_auth_idp_flutter_firebase +``` + +If you want to use Firebase's pre-built UI components, also add: + +```bash +flutter pub add firebase_ui_auth +``` + +### Configure FlutterFire + +Run the FlutterFire CLI to configure Firebase for your Flutter project: + +```bash +flutterfire configure +``` + +This generates a `firebase_options.dart` file with your platform-specific Firebase configuration. + +### Initialize Firebase and Serverpod + +In your `main.dart`, initialize both Firebase and the Serverpod client: + +```dart +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/material.dart'; +import 'package:serverpod_flutter/serverpod_flutter.dart'; +import 'package:serverpod_auth_idp_flutter_firebase/serverpod_auth_idp_flutter_firebase.dart'; +import 'package:your_client/your_client.dart'; +import 'firebase_options.dart'; + +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 + +:::info +The `initializeFirebaseSignIn()` call in the client setup automatically signs out from Firebase when the user signs out from Serverpod, keeping both systems in sync. +::: + +## Present the authentication UI + +### Using FirebaseAuthController + +The `FirebaseAuthController` handles syncing Firebase authentication state with your Serverpod backend. After a user signs in with Firebase, pass the Firebase user to the controller: + +```dart +import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; +import 'package:serverpod_auth_idp_flutter_firebase/serverpod_auth_idp_flutter_firebase.dart'; + +// Create the controller +final 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')), + ); + }, +); + +// After user signs in with Firebase +final firebaseUser = firebase_auth.FirebaseAuth.instance.currentUser; +if (firebaseUser != null) { + await controller.login(firebaseUser); +} +``` + +For details on building custom authentication UIs and integrating with `firebase_ui_auth`, see the [customizing the UI section](./customizing-the-ui). diff --git a/docs/06-concepts/11-authentication/04-providers/05-firebase/02-configuration.md b/docs/06-concepts/11-authentication/04-providers/05-firebase/02-configuration.md new file mode 100644 index 00000000..2816bb3c --- /dev/null +++ b/docs/06-concepts/11-authentication/04-providers/05-firebase/02-configuration.md @@ -0,0 +1,125 @@ +# Configuration + +This page covers configuration options for the Firebase identity provider beyond the basic setup. + +## Configuration options + +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. + +### Loading Firebase Credentials + +You can load Firebase service account credentials in several ways: + +**From JSON string (recommended for production):** + +```dart +final firebaseIdpConfig = FirebaseIdpConfig( + credentials: FirebaseServiceAccountCredentials.fromJsonString( + pod.getPassword('firebaseServiceAccountKey')!, + ), +); +``` + +**From JSON file:** + +```dart +import 'dart:io'; + +final firebaseIdpConfig = FirebaseIdpConfig( + credentials: FirebaseServiceAccountCredentials.fromJsonFile( + File('config/firebase_service_account_key.json'), + ), +); +``` + +**From JSON map:** + +```dart +final firebaseIdpConfig = FirebaseIdpConfig( + credentials: FirebaseServiceAccountCredentials.fromJson({ + 'type': 'service_account', + 'project_id': 'your-project-id', + 'private_key_id': '...', + 'private_key': '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n', + 'client_email': 'firebase-adminsdk-xxxxx@your-project-id.iam.gserviceaccount.com', + 'client_id': '...', + 'auth_uri': 'https://accounts.google.com/o/oauth2/auth', + 'token_uri': 'https://oauth2.googleapis.com/token', + }), +); +``` + +### 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: + +```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(); + } +} +``` + +To customize validation, provide your own `firebaseAccountDetailsValidation` function: + +```dart +final firebaseIdpConfig = FirebaseIdpConfig( + credentials: FirebaseServiceAccountCredentials.fromJsonString( + pod.getPassword('firebaseServiceAccountKey')!, + ), + firebaseAccountDetailsValidation: (accountDetails) { + // Require verified email (even for phone auth) + if (accountDetails.verifiedEmail != true) { + throw Exception('Email must be verified'); + } + + // Restrict to specific email domain + if (accountDetails.email != null && + !accountDetails.email!.endsWith('@example.com')) { + throw Exception('Only @example.com emails allowed'); + } + }, +); +``` + +### FirebaseAccountDetails + +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'); + } +}, +``` + +:::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. +::: diff --git a/docs/06-concepts/11-authentication/04-providers/05-firebase/03-customizing-the-ui.md b/docs/06-concepts/11-authentication/04-providers/05-firebase/03-customizing-the-ui.md new file mode 100644 index 00000000..7b21ded1 --- /dev/null +++ b/docs/06-concepts/11-authentication/04-providers/05-firebase/03-customizing-the-ui.md @@ -0,0 +1,118 @@ +# 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. + +:::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`. +::: + +## Using the FirebaseAuthController + +The `FirebaseAuthController` manages the synchronization between Firebase authentication state and Serverpod sessions. + +```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 + }, +); +``` + +### FirebaseAuthController State Management + +Your widget should render the appropriate UI based on the controller's state: + +```dart +// Check current state +final state = controller.state; // FirebaseAuthState enum + +// Check if loading +final isLoading = controller.isLoading; + +// Check if authenticated +final isAuthenticated = controller.isAuthenticated; + +// Get error message +final errorMessage = controller.errorMessage; + +// Get error object +final error = controller.error; + +// Listen to state changes +controller.addListener(() { + setState(() { + // Rebuild UI when controller state changes + }); +}); +``` + +### 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 login method + +The `login` method accepts a `firebase_auth.User` object and syncs it with Serverpod: + +```dart +// Get the current Firebase user after sign-in +final firebaseUser = firebase_auth.FirebaseAuth.instance.currentUser; + +if (firebaseUser != null) { + await controller.login(firebaseUser); +} +``` + +## Integration patterns + +### Using firebase_auth directly + +For full control over the authentication UI, use the `firebase_auth` package directly. The basic flow is: + +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 + +Refer to the [firebase_auth documentation](https://pub.dev/packages/firebase_auth) for available authentication methods. + +### Using firebase_ui_auth + +For a pre-built UI experience, use the `firebase_ui_auth` package. This provides ready-made screens for various authentication methods. + +1. Add the package: `flutter pub add firebase_ui_auth` +2. Use `firebase_ui.SignInScreen` with your desired providers +3. Add `AuthStateChangeAction` handlers for `SignedIn` and `UserCreated` events +4. In each handler, call `controller.login()` with the Firebase user + +```dart +actions: [ + firebase_ui.AuthStateChangeAction((context, state) async { + final user = firebase_auth.FirebaseAuth.instance.currentUser; + if (user != null) { + await controller.login(user); + } + }), +], +``` + +Refer to the [firebase_ui_auth documentation](https://pub.dev/packages/firebase_ui_auth) for configuration details and available providers. + +### 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. + +:::note +When using the auth state listener pattern, check the Serverpod auth state before calling `login()` to prevent re-syncing an already authenticated user. +::: diff --git a/docs/06-concepts/11-authentication/04-providers/05-firebase/04-admin-operations.md b/docs/06-concepts/11-authentication/04-providers/05-firebase/04-admin-operations.md new file mode 100644 index 00000000..f24df302 --- /dev/null +++ b/docs/06-concepts/11-authentication/04-providers/05-firebase/04-admin-operations.md @@ -0,0 +1,114 @@ +# 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. + +## Accessing the FirebaseIdpAdmin + +You can access the admin operations through the `AuthServices.instance.firebaseIdp` property: + +```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 + +The admin API provides methods for managing Firebase-authenticated accounts. + +### Finding Accounts + +```dart +// Find an account by email +final account = await admin.findAccountByEmail( + session, + email: 'user@example.com', +); + +// Find an account by Serverpod auth user ID +final account = 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 + +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, +); +``` + +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: + +```dart +// Fetch account details from a Firebase ID token +final accountDetails = await admin.fetchAccountDetails( + session, + idToken: firebaseIdToken, +); + +// Then link the account +await admin.linkFirebaseAuthentication( + session, + authUserId: existingUserId, + accountDetails: accountDetails, +); +``` + +### Deleting Accounts + +```dart +// Delete a Firebase account by Firebase UID +final deletedAccount = await admin.deleteFirebaseAccount( + session, + userIdentifier: 'firebase-uid', +); + +// Delete all Firebase accounts for a Serverpod user +final deletedAccount = 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. +::: + +## FirebaseIdpUtils + +The `FirebaseIdpUtils` class provides utility functions for working with Firebase authentication: + +```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 +); +``` + +:::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/05-firebase/_category_.json b/docs/06-concepts/11-authentication/04-providers/05-firebase/_category_.json new file mode 100644 index 00000000..ef488ac2 --- /dev/null +++ b/docs/06-concepts/11-authentication/04-providers/05-firebase/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Firebase", + "collapsed": true +}