diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1a7421a3..7add5869 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -47,8 +47,13 @@ jobs:
pub-${{ matrix.dart-version }}-
pub-
- - name: Install dependencies
- run: dart pub get && cd example && dart pub get && cd -
+ - name: Install Melos
+ run: dart pub global activate melos
+ working-directory: .
+
+ - name: Bootstrap workspace
+ run: melos bootstrap
+ working-directory: .
- name: Check format
run: dart format --set-exit-if-changed .
@@ -70,10 +75,15 @@ jobs:
working-directory: packages/dart_firebase_admin
steps:
- - uses: actions/checkout@v3.1.0
+ - uses: actions/checkout@v4
with:
fetch-depth: 2
+ - uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+
- uses: actions/setup-node@v4
- uses: subosito/flutter-action@v2.7.1
@@ -102,30 +112,82 @@ jobs:
echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH
echo "PUB_CACHE=$HOME/.pub-cache" >> $GITHUB_ENV
+ - name: Install Melos
+ run: dart pub global activate melos
+ working-directory: .
+
+ - name: Bootstrap workspace
+ run: melos bootstrap
+ working-directory: .
+
- name: Install Firebase CLI
run: npm install -g firebase-tools
- - name: Install dependencies
- run: dart pub get && cd example && dart pub get && cd -
+ - name: Run dart_firebase_admin tests with coverage
+ run: ${{ github.workspace }}/scripts/coverage.sh
+
+ - name: Run google_cloud_firestore tests with coverage
+ run: ${{ github.workspace }}/scripts/firestore-coverage.sh
- - name: Setup gcloud credentials
+ - name: Merge coverage reports
run: |
- mkdir -p $HOME/.config/gcloud
- echo '${{ secrets.CREDS }}' > $HOME/.config/gcloud/application_default_credentials.json
-
- - name: Run tests with coverage
- run: ${{ github.workspace }}/scripts/coverage.sh
+ # Save individual package coverage files before merging
+ cp coverage.lcov coverage_admin.lcov
+ cp ../google_cloud_firestore/coverage.lcov coverage_firestore.lcov
+
+ # Merge coverage reports from all packages (relative to packages/dart_firebase_admin)
+ # Only merge files that exist
+ COVERAGE_FILES=""
+ [ -f coverage.lcov ] && COVERAGE_FILES="$COVERAGE_FILES coverage.lcov"
+ [ -f ../google_cloud_firestore/coverage.lcov ] && COVERAGE_FILES="$COVERAGE_FILES ../google_cloud_firestore/coverage.lcov"
+
+ if [ -n "$COVERAGE_FILES" ]; then
+ cat $COVERAGE_FILES > merged_coverage.lcov
+ mv merged_coverage.lcov coverage.lcov
+ else
+ echo "No coverage files found!"
+ exit 1
+ fi
- name: Check coverage threshold and generate report
if: matrix.dart-version == 'stable'
id: coverage
run: |
- dart pub global activate coverage
-
- # Generate coverage report
- dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage.lcov --packages=.dart_tool/package_config.json --report-on=lib
-
- # Calculate total coverage
+ # Calculate coverage for each package
+ calculate_coverage() {
+ local file=$1
+ if [ -f "$file" ]; then
+ local total=$(grep -E "^LF:" "$file" | awk -F: '{sum+=$2} END {print sum}')
+ local hit=$(grep -E "^LH:" "$file" | awk -F: '{sum+=$2} END {print sum}')
+ if [ "$total" -gt 0 ]; then
+ local pct=$(awk "BEGIN {printf \"%.2f\", ($hit/$total)*100}")
+ echo "$pct|$hit|$total"
+ else
+ echo "0.00|0|0"
+ fi
+ else
+ echo "0.00|0|0"
+ fi
+ }
+
+ # Get individual package coverage from saved copies
+ ADMIN_COV=$(calculate_coverage "coverage_admin.lcov")
+ FIRESTORE_COV=$(calculate_coverage "coverage_firestore.lcov")
+ STORAGE_COV=$(calculate_coverage "coverage_storage.lcov")
+
+ ADMIN_PCT=$(echo $ADMIN_COV | cut -d'|' -f1)
+ ADMIN_HIT=$(echo $ADMIN_COV | cut -d'|' -f2)
+ ADMIN_TOTAL=$(echo $ADMIN_COV | cut -d'|' -f3)
+
+ FIRESTORE_PCT=$(echo $FIRESTORE_COV | cut -d'|' -f1)
+ FIRESTORE_HIT=$(echo $FIRESTORE_COV | cut -d'|' -f2)
+ FIRESTORE_TOTAL=$(echo $FIRESTORE_COV | cut -d'|' -f3)
+
+ STORAGE_PCT=$(echo $STORAGE_COV | cut -d'|' -f1)
+ STORAGE_HIT=$(echo $STORAGE_COV | cut -d'|' -f2)
+ STORAGE_TOTAL=$(echo $STORAGE_COV | cut -d'|' -f3)
+
+ # Calculate total coverage from merged file
TOTAL_LINES=$(grep -E "^(DA|LF):" coverage.lcov | grep "^LF:" | awk -F: '{sum+=$2} END {print sum}')
HIT_LINES=$(grep -E "^(DA|LH):" coverage.lcov | grep "^LH:" | awk -F: '{sum+=$2} END {print sum}')
@@ -135,11 +197,21 @@ jobs:
COVERAGE_PCT="0.00"
fi
+ # Output for GitHub Actions
echo "coverage=${COVERAGE_PCT}" >> $GITHUB_OUTPUT
echo "total_lines=${TOTAL_LINES}" >> $GITHUB_OUTPUT
echo "hit_lines=${HIT_LINES}" >> $GITHUB_OUTPUT
- echo "Coverage: ${COVERAGE_PCT}% (${HIT_LINES}/${TOTAL_LINES} lines)"
+ echo "admin_coverage=${ADMIN_PCT}" >> $GITHUB_OUTPUT
+ echo "firestore_coverage=${FIRESTORE_PCT}" >> $GITHUB_OUTPUT
+ echo "storage_coverage=${STORAGE_PCT}" >> $GITHUB_OUTPUT
+
+ # Console output
+ echo "=== Coverage Report ==="
+ echo "dart_firebase_admin: ${ADMIN_PCT}% (${ADMIN_HIT}/${ADMIN_TOTAL} lines)"
+ echo "google_cloud_firestore: ${FIRESTORE_PCT}% (${FIRESTORE_HIT}/${FIRESTORE_TOTAL} lines)"
+ echo "----------------------"
+ echo "Total: ${COVERAGE_PCT}% (${HIT_LINES}/${TOTAL_LINES} lines)"
# Check threshold
if (( $(echo "$COVERAGE_PCT < 40" | bc -l) )); then
@@ -150,7 +222,7 @@ jobs:
fi
- name: Comment coverage on PR
- if: matrix.dart-version == 'stable' && github.event_name == 'pull_request'
+ if: matrix.dart-version == 'stable' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
uses: actions/github-script@v6
with:
script: |
@@ -158,14 +230,23 @@ jobs:
const status = '${{ steps.coverage.outputs.status }}';
const hitLines = '${{ steps.coverage.outputs.hit_lines }}';
const totalLines = '${{ steps.coverage.outputs.total_lines }}';
+ const adminCov = '${{ steps.coverage.outputs.admin_coverage }}';
+ const firestoreCov = '${{ steps.coverage.outputs.firestore_coverage }}';
+ const storageCov = '${{ steps.coverage.outputs.storage_coverage }}';
const body = `## Coverage Report
${status}
- **Coverage:** ${coverage}%
+ **Total Coverage:** ${coverage}%
**Lines Covered:** ${hitLines}/${totalLines}
+ ### Package Breakdown
+ | Package | Coverage |
+ |---------|----------|
+ | dart_firebase_admin | ${adminCov}% |
+ | google_cloud_firestore | ${firestoreCov}% |
+
_Minimum threshold: 40%_`;
// Find existing comment
@@ -207,10 +288,74 @@ jobs:
flags: unittests
fail_ci_if_error: false
+ test-integration:
+ name: Test - Integration (Dart ${{ matrix.dart-version }})
+ runs-on: ubuntu-latest
+ # Skip for fork PRs — secrets are not available
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
+ permissions:
+ contents: 'read'
+ id-token: 'write'
+
+ strategy:
+ fail-fast: false
+ matrix:
+ dart-version: [stable, beta]
+
+ defaults:
+ run:
+ working-directory: packages/dart_firebase_admin
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 2
+
+ - uses: subosito/flutter-action@v2.7.1
+ with:
+ channel: ${{ matrix.dart-version }}
+
+ - name: Authenticate to Google Cloud/Firebase
+ uses: google-github-actions/auth@v2
+ with:
+ workload_identity_provider: '${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}'
+ service_account: '${{ secrets.SERVICE_ACCOUNT }}'
+
+ - name: Cache pub dependencies
+ uses: actions/cache@v3
+ with:
+ path: ~/.pub-cache
+ key: pub-${{ matrix.dart-version }}-${{ hashFiles('**/pubspec.lock') }}
+ restore-keys: |
+ pub-${{ matrix.dart-version }}-
+ pub-
+
+ - name: Add pub cache bin to PATH
+ run: |
+ echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH
+ echo "PUB_CACHE=$HOME/.pub-cache" >> $GITHUB_ENV
+
+ - name: Install Melos
+ run: dart pub global activate melos
+ working-directory: .
+
+ - name: Bootstrap workspace
+ run: melos bootstrap
+ working-directory: .
+
+ - name: Run integration tests
+ run: dart test test/app/firebase_app_prod_test.dart --concurrency=1
+
build:
name: Build verification (Dart ${{ matrix.dart-version }})
runs-on: ubuntu-latest
- needs: [lint, test]
+ needs: [lint, test, test-integration]
+ # Run even if test-integration was skipped (fork PRs), but not if lint or test failed
+ if: |
+ always() &&
+ needs.lint.result == 'success' &&
+ needs.test.result == 'success' &&
+ (needs.test-integration.result == 'success' || needs.test-integration.result == 'skipped')
strategy:
fail-fast: false
@@ -237,8 +382,13 @@ jobs:
pub-${{ matrix.dart-version }}-
pub-
- - name: Install dependencies
- run: dart pub get && cd example && dart pub get && cd -
+ - name: Install Melos
+ run: dart pub global activate melos
+ working-directory: .
+
+ - name: Bootstrap workspace
+ run: melos bootstrap
+ working-directory: .
- name: Verify package
run: dart pub publish --dry-run
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 00000000..00b69d56
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,68 @@
+name: Deploy Documentation
+
+on:
+ push:
+ branches:
+ - next
+ pull_request:
+ workflow_dispatch:
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow only one concurrent deployment
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+jobs:
+ build:
+ name: Build Documentation
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: stable
+
+ - name: Cache pub dependencies
+ uses: actions/cache@v3
+ with:
+ path: ~/.pub-cache
+ key: pub-docs-${{ hashFiles('**/pubspec.lock') }}
+ restore-keys: |
+ pub-docs-
+ pub-
+
+ - name: Install Melos
+ run: dart pub global activate melos
+
+ - name: Bootstrap workspace
+ run: melos bootstrap
+
+ - name: Generate documentation
+ run: melos run docs --no-select
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: doc
+
+ deploy:
+ name: Deploy to GitHub Pages
+ if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ needs: build
+
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index ef87ca11..b4fc488a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1,14 @@
firebase-debug.log
ui-debug.log
firestore-debug.log
+coverage.lcov
node_modules
packages/dart_firebase_admin/test/client/package-lock.json
build
coverage
+doc/api
.DS_Store
.atom/
@@ -32,4 +34,4 @@ pubspec.lock
service-account.json
-**/pubspec_overrides.yaml
+**/pubspec_overrides.yaml
\ No newline at end of file
diff --git a/all_lint_rules.yaml b/all_lint_rules.yaml
index 0979f5cf..f91d1586 100644
--- a/all_lint_rules.yaml
+++ b/all_lint_rules.yaml
@@ -219,5 +219,4 @@ linter:
- use_string_in_part_of_directives
- use_super_parameters
- use_test_throws_matchers
- - use_to_and_as_if_applicable
- void_checks
\ No newline at end of file
diff --git a/analysis_options.yaml b/analysis_options.yaml
index ff3a681c..8ff51c0c 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,5 +1,7 @@
include: all_lint_rules.yaml
analyzer:
+ exclude:
+ - packages/dart_firebase_admin/example_server_app/**
language:
strict-casts: true
strict-inference: true
diff --git a/doc/index.html b/doc/index.html
new file mode 100644
index 00000000..ac3599bb
--- /dev/null
+++ b/doc/index.html
@@ -0,0 +1,70 @@
+
+
+
+
+
+ Firebase Admin SDK for Dart - Documentation
+
+
+
+ Firebase Admin SDK for Dart
+ Welcome to the API documentation for the Firebase Admin SDK for Dart.
+
+
+
+ dart_firebase_admin
+ Main Firebase Admin SDK package with support for Auth, Firestore, Messaging, App Check, Security Rules, and Functions.
+ View Documentation →
+
+
+
+ google_cloud_firestore
+ Standalone Cloud Firestore client library for Dart. Provides direct access to Firestore database operations including documents, collections, queries, transactions, and batch writes.
+ View Documentation →
+
+
+
+
diff --git a/gen-version.sh b/gen-version.sh
new file mode 100755
index 00000000..d108fd81
--- /dev/null
+++ b/gen-version.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+# Script to generate version.g.dart files for all packages
+# Finds all packages/*/pubspec.yaml files, extracts version, and writes to package/lib/version.g.dart
+
+set -e
+
+# Get the script directory (project root)
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+# Find all pubspec.yaml files in packages directory
+find "$SCRIPT_DIR/packages" -name "pubspec.yaml" -type f | while read -r pubspec_file; do
+ # Get the package directory (parent of pubspec.yaml)
+ package_dir="$(dirname "$pubspec_file")"
+ package_name="$(basename "$package_dir")"
+
+ # Extract version from pubspec.yaml (format: version: X.Y.Z)
+ version=$(grep -E "^version:" "$pubspec_file" | sed -E 's/^version:[[:space:]]*//' | tr -d '[:space:]')
+
+ if [ -z "$version" ]; then
+ echo "Warning: Could not find version in $pubspec_file, skipping..."
+ continue
+ fi
+
+ # Create lib directory if it doesn't exist
+ lib_dir="$package_dir/lib"
+ mkdir -p "$lib_dir"
+
+ # Write version.g.dart file
+ version_file="$lib_dir/version.g.dart"
+ cat > "$version_file" << EOF
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// This file is generated by gen-version.sh
+
+/// The current version of the package.
+const String packageVersion = '$version';
+EOF
+
+ echo "Generated $version_file with version: $version"
+done
+
+echo "Version generation complete!"
\ No newline at end of file
diff --git a/melos.yaml b/melos.yaml
deleted file mode 100644
index 3f32fce3..00000000
--- a/melos.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-name: dart_firebase_admin
-
-packages:
- - "packages/**"
\ No newline at end of file
diff --git a/packages/dart_firebase_admin/.firebaserc b/packages/dart_firebase_admin/.firebaserc
new file mode 100644
index 00000000..23fc90b9
--- /dev/null
+++ b/packages/dart_firebase_admin/.firebaserc
@@ -0,0 +1,5 @@
+{
+ "projects": {
+ "default": "dart-firebase-admin"
+ }
+}
diff --git a/packages/dart_firebase_admin/.gitignore b/packages/dart_firebase_admin/.gitignore
new file mode 100644
index 00000000..d524d600
--- /dev/null
+++ b/packages/dart_firebase_admin/.gitignore
@@ -0,0 +1,6 @@
+service-account-key.json
+
+# Test functions artifacts
+test/functions/node_modules/
+test/functions/lib/
+test/functions/package-lock.json
diff --git a/packages/dart_firebase_admin/CHANGELOG.md b/packages/dart_firebase_admin/CHANGELOG.md
index 04983473..a1191482 100644
--- a/packages/dart_firebase_admin/CHANGELOG.md
+++ b/packages/dart_firebase_admin/CHANGELOG.md
@@ -1,3 +1,63 @@
+## 0.5.0 - 2026-03-10
+
+### Breaking Changes
+
+- `initializeApp()` now accepts `AppOptions` instead of positional `projectId` and `credential` arguments. Project ID is auto-discovered from credentials, environment variables, or the GCE metadata server if not provided.
+- Firebase service constructors (`Auth(app)`, `Firestore(app)`, etc.) have been removed. Use the instance methods on `FirebaseApp` instead: `app.auth()`, `app.firestore()`, `app.messaging()`, etc. Service instances are now cached — calling `app.auth()` multiple times returns the same instance.
+- `ActionCodeSettings.dynamicLinkDomain` has been removed. Use `linkDomain` instead.
+- `Credential` is now a sealed class with `ServiceAccountCredential` and `ApplicationDefaultCredential` subtypes.
+
+### New Features
+
+**App**
+- Added multi-app support: `initializeApp(options, name: 'secondary')` and `FirebaseApp.getApp('name')`
+- Added `app.serviceAccountEmail()` and `app.sign()` extension methods on `FirebaseApp`
+- All outgoing SDK requests now include an `X-Firebase-Client: fire-admin-dart/` usage tracking header
+
+**Auth**
+- Added tenant support: `app.auth().tenantManager()` returns a `TenantManager`; use `tenantManager.authForTenant(tenantId)` for tenant-scoped auth operations
+- Added `ProjectConfigManager` for managing project-level auth configuration (email privacy, SMS regions, password policies, MFA, reCAPTCHA, mobile links) via `app.auth().projectConfigManager()`
+- Added TOTP multi-factor authentication support
+- Added `SessionCookieOptions` type for `createSessionCookie`
+- Added `linkDomain` to `ActionCodeSettings`
+- Added `Credential.getAccessToken()` to retrieve an OAuth2 access token
+- Added reCAPTCHA managed rules, key types, and toll fraud protection configuration in tenant settings
+
+**Firestore**
+- Added multi-database support: `app.firestore(databaseId: 'analytics-db')`
+- Added `Transaction.getQuery()` to execute queries within a transaction
+- Added `Transaction.getAggregateQuery()` to execute aggregation queries within a transaction
+- Added `BulkWriter` for high-throughput writes with automatic batching (20 ops/batch) and rate limiting using the 500/50/5 rule
+- Added `SetOptions` for merge operations, available on `WriteBatch`, `Transaction`, `BulkWriter`, and `DocumentReference`
+- Added Vector Search support: `FieldValue.vector()`, `VectorValue`, `query.findNearest()`, `VectorQuery`, `VectorQuerySnapshot`
+- Added Query Explain API: `query.explain()` and `vectorQuery.explain()`
+- Added query partitioning: `CollectionGroup.getPartitions(desiredPartitionCount)` returns a list of `QueryPartition` objects; call `partition.toQuery()` to execute each chunk in parallel
+- Added `Firestore.recursiveDelete(ref, bulkWriter)` for bulk deletion of documents and collections
+- `withConverter()` now accepts `null` to reset a typed reference back to `DocumentData`
+
+**Storage** _(new service)_
+- Added `app.storage()` with full `FirebaseApp` lifecycle integration
+- Added emulator support via `FIREBASE_STORAGE_EMULATOR_HOST`
+- Added `file.getSignedUrl()` for V2/V4 signed URL generation
+- Added `file.getDownloadURL()` for retrieving a permanent download URL
+
+**Messaging**
+- Added `messaging.subscribeToTopic(tokens, topic)` and `messaging.unsubscribeFromTopic(tokens, topic)`
+
+**Functions** _(new service)_
+- Added `app.functions()` for Cloud Functions admin operations
+- Added Task Queue API: `functions.taskQueue(functionName)` with `enqueue()` and `delete()`, supporting scheduling, deadlines, custom headers, and custom task IDs
+- Added Cloud Tasks emulator support via `CLOUD_TASKS_EMULATOR_HOST`
+
+### Bug Fixes
+
+- Fixed exceptions being silently swallowed instead of rethrown across all services
+- Fixed `Messaging.sendEach()` incorrectly returning `internalError` for invalid registration tokens; now correctly returns `invalidArgument`
+- Fixed JWT decode exceptions and integer division issues in `verifySessionCookie`
+- Fixed missing `INVALID_ARGUMENT` error code mapping in `SecurityRules`
+- Fixed `ExponentialBackoff` in Firestore not correctly tracking backoff completion state
+- Fixed Auth using `invalidProviderUid` instead of `invalidUid` in `getAccountInfoByFederatedUid`
+
## 0.4.1 - 2025-03-21
- Bump intl to `0.20.0`
diff --git a/packages/dart_firebase_admin/CONTRIBUTING.md b/packages/dart_firebase_admin/CONTRIBUTING.md
new file mode 100644
index 00000000..f15488d9
--- /dev/null
+++ b/packages/dart_firebase_admin/CONTRIBUTING.md
@@ -0,0 +1,271 @@
+# Contributing to Firebase Admin SDK for Dart
+
+Thank you for contributing to the Firebase community!
+
+- [Have a usage question?](#have-a-usage-question)
+- [Think you found a bug?](#think-you-found-a-bug)
+- [Have a feature request?](#have-a-feature-request)
+- [Want to submit a pull request?](#want-to-submit-a-pull-request)
+- [Need to get set up locally?](#need-to-get-set-up-locally)
+
+## Have a usage question?
+
+We get lots of those and we love helping you, but GitHub is not the best place for them. Issues which just ask about usage will be closed. Here are some resources to get help:
+
+- Go through the [Firebase Admin SDK setup guide](https://firebase.google.com/docs/admin/setup/)
+- Read the full [API reference](https://pub.dev/documentation/dart_firebase_admin/latest/)
+
+If the official documentation doesn't help, try asking on [Stack Overflow](https://stackoverflow.com/questions/tagged/firebase+dart).
+
+**Please avoid double posting across multiple channels!**
+
+## Think you found a bug?
+
+Search through [existing issues](https://github.com/invertase/dart_firebase_admin/issues) before opening a new one — your question may have already been answered.
+
+If your issue appears to be a bug and hasn't been reported, [open a new issue](https://github.com/invertase/dart_firebase_admin/issues/new) using the bug report template and include a minimal repro.
+
+If you are up to the challenge, [submit a pull request](#want-to-submit-a-pull-request) with a fix!
+
+## Have a feature request?
+
+Share your idea through our [feature request support channel](https://firebase.google.com/support/contact/bugs-features/).
+
+## Want to submit a pull request?
+
+Sweet, we'd love to accept your contribution! [Open a new pull request](https://github.com/invertase/dart_firebase_admin/pulls) and fill out the provided template.
+
+**If you want to implement a new feature, please open an issue with a proposal first so that we can figure out if the feature makes sense and how it will work.**
+
+### Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License Agreement (CLA). You (or your employer) retain the copyright to your contribution; the CLA gives us permission to use and redistribute your contributions as part of the project.
+
+Visit to see your current agreements on file or to sign a new one. You generally only need to submit a CLA once, so if you have already submitted one you probably don't need to do it again.
+
+### Code Reviews
+
+All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on pull requests.
+
+### Commit Messages
+
+- Write clear, descriptive commit messages.
+- Use imperative mood (e.g., "Add App Check token verification" not "Added App Check token verification").
+- Reference issues with `#number` where applicable.
+
+## Need to get set up locally?
+
+### Prerequisites
+
+- **Dart SDK** >= 3.9.0
+- **Java 21+** (required for the Firestore emulator)
+- **Node.js** (required for the Firebase Emulator and Cloud Tasks emulator)
+- **Melos** — Dart monorepo tool (`dart pub global activate melos`)
+- **Firebase CLI** (`npm install -g firebase-tools`)
+- **Google Cloud SDK** ([`gcloud`](https://cloud.google.com/sdk/downloads)) — required to authorise integration tests against a real Firebase project
+
+### Setup
+
+1. Fork and clone the repository.
+
+2. Install Melos and bootstrap the workspace from the repo root:
+
+ ```bash
+ dart pub global activate melos
+ melos bootstrap
+ ```
+
+3. Verify your setup by running the analyzer and formatter:
+
+ ```bash
+ cd packages/dart_firebase_admin
+ dart format --set-exit-if-changed .
+ dart analyze
+ ```
+
+### Repo Organization
+
+This repository is a monorepo managed by [Melos](https://melos.invertase.dev/).
+
+```
+dart_firebase_admin/ # Workspace root
+├── packages/
+│ ├── dart_firebase_admin/ # Main Firebase Admin SDK package
+│ │ ├── lib/
+│ │ │ ├── dart_firebase_admin.dart # Public API barrel file
+│ │ │ ├── auth.dart # Auth public exports
+│ │ │ ├── firestore.dart # Firestore public exports
+│ │ │ ├── messaging.dart # Messaging public exports
+│ │ │ ├── storage.dart # Storage public exports
+│ │ │ ├── app_check.dart # App Check public exports
+│ │ │ ├── security_rules.dart # Security Rules public exports
+│ │ │ ├── functions.dart # Functions public exports
+│ │ │ └── src/ # Private implementation
+│ │ └── test/
+│ │ ├── auth/ # Auth unit & integration tests
+│ │ ├── app/ # App unit & integration tests
+│ │ ├── firestore/ # Firestore tests
+│ │ ├── messaging/ # Messaging tests
+│ │ ├── storage/ # Storage tests
+│ │ └── helpers.dart # Shared test utilities
+│ └── google_cloud_firestore/ # Standalone Firestore package
+└── scripts/
+ ├── coverage.sh # Run tests with coverage
+ └── firestore-coverage.sh # Firestore package coverage
+```
+
+## Development Workflow
+
+### Running Tests
+
+Tests are split into unit/emulator tests and production integration tests.
+
+#### Unit and Emulator Tests
+
+```bash
+# From packages/dart_firebase_admin
+
+# Run all tests against emulators (requires Firebase CLI)
+firebase emulators:exec --project dart-firebase-admin --only auth,firestore,functions,tasks,storage \
+ "dart run coverage:test_with_coverage -- --concurrency=1"
+
+# Or use the convenience script from the repo root
+./scripts/coverage.sh
+
+# Run a specific test file
+dart test test/auth/auth_test.dart
+```
+
+#### Integration Tests with Emulator Suite
+
+Start the emulators, then run with the relevant environment variables:
+
+```bash
+firebase emulators:start --only firestore,auth
+
+export FIRESTORE_EMULATOR_HOST=localhost:8080
+export FIREBASE_AUTH_EMULATOR_HOST=localhost:9099
+dart test test/firestore/firestore_integration_test.dart
+```
+
+#### Production Integration Tests
+
+Requires a real Firebase project and Google application default credentials. Authorise `gcloud` first:
+
+```bash
+gcloud beta auth application-default login
+```
+
+Then run with `RUN_PROD_TESTS=true`:
+
+```bash
+RUN_PROD_TESTS=true dart test test/app/firebase_app_prod_test.dart --concurrency=1
+```
+
+See [`README.md`](README.md) for Firebase project setup details. You can create a project in the [Firebase Console](https://console.firebase.google.com) if you don't have one already.
+
+### Code Formatting and Analysis
+
+```bash
+# Check formatting (CI will reject unformatted code)
+dart format --set-exit-if-changed .
+
+# Apply formatting
+dart format .
+
+# Run the analyzer (must pass with zero issues)
+dart analyze
+```
+
+### License Headers
+
+All source files must include a license header. The project uses [addlicense](https://github.com/google/addlicense) to manage this automatically, with templates in `.github/licenses/`:
+
+- `default.txt` — plain text, applied to TypeScript (`/** */`) and shell (`#`) files
+- Dart files use the built-in `-l apache` type, which handles `//` style and year substitution natively
+
+**Install addlicense:**
+
+Download the binary for your platform from the [v1.2.0 release](https://github.com/google/addlicense/releases/tag/v1.2.0) and place it on your `PATH`. For example, on macOS ARM64:
+
+```bash
+curl -sL https://github.com/google/addlicense/releases/download/v1.2.0/addlicense_v1.2.0_macOS_arm64.tar.gz | tar xz
+sudo mv addlicense /usr/local/bin/
+```
+
+**Add headers to new files (run from repo root):**
+
+```bash
+.github/licenses/add-headers.sh
+```
+
+**Check headers (dry run, same as CI):**
+
+```bash
+.github/licenses/check-headers.sh
+```
+
+CI will fail if any source file is missing its license header.
+
+### Local Validation
+
+Run the full check suite locally before pushing:
+
+```bash
+dart format --set-exit-if-changed .
+dart analyze
+./scripts/coverage.sh
+```
+
+## Code Standards
+
+### Style
+
+The project uses strict analysis settings (`strict-casts`, `strict-inference`, `strict-raw-types`). Key conventions enforced by `analysis_options.yaml`:
+
+- **Use single quotes** for strings (`prefer_single_quotes`).
+- **Use `final` for local variables** where values are not reassigned.
+- **Prefer relative imports** within the package.
+- **Always declare return types** on functions and methods.
+
+### Public API
+
+- Each Firebase product has its own barrel file (e.g., `lib/auth.dart`, `lib/firestore.dart`). Only add exports there for types that users need directly.
+- The top-level `lib/dart_firebase_admin.dart` re-exports core types. Product-specific types belong in their respective barrel files.
+- Classes under `lib/src/` are implementation details and should not be exported from barrel files unless they are part of the public API.
+
+### Documentation
+
+- Add dartdoc (`///`) comments to all new public APIs.
+- Include code examples in doc comments where they help clarify usage.
+
+### Error Handling
+
+- Use typed exceptions (e.g., `FirebaseAuthException`, `FirebaseMessagingException`) with appropriate error codes for user-facing errors.
+- Match the behaviour of the Node.js Admin SDK where applicable.
+
+## Testing Requirements
+
+- **All new features and bug fixes must include tests.**
+- **Unit/emulator tests** go in the appropriate subdirectory under `test/`. Use the `helpers.dart` utilities and `mocktail` for mocking where needed.
+- **Integration tests** (files named `*_integration_test.dart`) run against the Firebase Emulator in CI.
+- **Production tests** (files named `*_prod_test.dart`) require real credentials and are not run in CI by default — gate them behind `RUN_PROD_TESTS`.
+- Maintain overall coverage above the **40% threshold** enforced by CI.
+
+## CI/CD
+
+The project uses a single **build.yml** GitHub Actions workflow:
+
+| Job | Trigger | What it does |
+|-----|---------|--------------|
+| `check-license-header` | PRs & schedule | Validates license headers on all source files |
+| `lint` | PRs & schedule | Runs `dart format` and `dart analyze` |
+| `test` | PRs & schedule | Runs tests against emulators with coverage reporting |
+| `test-integration` | PRs (non-fork) & schedule | Runs production integration tests with Workload Identity Federation |
+| `build` | After all above pass | Validates `dart pub publish --dry-run` |
+
+Tests run against both `stable` and `beta` Dart SDK channels. Coverage is reported as a PR comment and uploaded to Codecov. The minimum threshold is **40%**.
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the [Apache License 2.0](LICENSE).
diff --git a/packages/dart_firebase_admin/README.md b/packages/dart_firebase_admin/README.md
index e34e4c8d..a76a1988 100644
--- a/packages/dart_firebase_admin/README.md
+++ b/packages/dart_firebase_admin/README.md
@@ -1,418 +1,565 @@
-## Dart Firebase Admin
-
-Welcome! This project is a port of [Node's Firebase Admin SDK](https://github.com/firebase/firebase-admin-node) to Dart.
+# Firebase Admin Dart SDK
+
+## Table of Contents
+
+ - [Overview](#overview)
+ - [Installation](#installation)
+ - [Initalization](#initalization)
+ - [Usage](#usage)
+ - [Authentication](#authentication)
+ - [App Check](#app-check)
+ - [Firestore](#firestore)
+ - [Functions](#functions)
+ - [Messaging](#messaging)
+ - [Storage](#storage)
+ - [Security Rules](#security-rules)
+ - [Supported Services](#supported-services)
+ - [Additional Packages](#additional-packages)
+ - [Contributing](#contributing)
+ - [License](#license)
+
+## Overview
+
+[Firebase](https://firebase.google.com) provides the tools and infrastructure
+you need to develop your app, grow your user base, and earn money. The Firebase
+Admin Dart SDK enables access to Firebase services from privileged environments
+(such as servers or cloud) in Dart.
+
+For more information, visit the
+[Firebase Admin SDK setup guide](https://firebase.google.com/docs/admin/setup/).
+
+## Installation
+
+The Firebase Admin Dart SDK is available on [pub.dev](https://pub.dev/) as `dart_firebase_admin`:
+
+```bash
+$ dart pub add dart_firebase_admin
+```
-⚠️ This project is still in its early stages, and some features may be missing or bugged.
-Currently, only Firestore is available, with more to come (auth next).
+To use the SDK in your application, `import` it from any Dart file:
-- [Dart Firebase Admin](#dart-firebase-admin)
-- [Getting started](#getting-started)
- - [Connecting to the SDK](#connecting-to-the-sdk)
- - [Connecting using the environment](#connecting-using-the-environment)
- - [Connecting using a `service-account.json` file](#connecting-using-a-service-accountjson-file)
-- [Firestore](#firestore)
- - [Usage](#usage)
- - [Supported features](#supported-features)
-- [Auth](#auth)
- - [Usage](#usage-1)
- - [Supported features](#supported-features-1)
-- [Available features](#available-features)
-- [AppCheck](#appcheck)
- - [Usage](#usage-2)
- - [Supported features](#supported-features-2)
-- [Security rules](#security-rules)
- - [Usage](#usage-3)
- - [Supported features](#supported-features-3)
-- [Messaging](#messaging)
- - [Usage](#usage-4)
- - [Supported features](#supported-features-4)
+```dart
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+```
-## Getting started
+## Initalization
-### Connecting to the SDK
+### Initialize the SDK
-Before using Firebase, we must first authenticate.
+To initalize the Firebase Admin SDK, call the `initializeApp` method on the `Firebase`
+class:
-There are currently two options:
+```dart
+final app = FirebaseApp.initializeApp();
+```
-- You can connect using environment variables
-- Alternatively, you can specify a `service-account.json` file
+This will automatically initialize the SDK with [Google Application Default Credentials](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application). Because default credentials lookup is fully automated in Google environments, with no need to supply environment variables or other configuration, this way of initializing the SDK is strongly recommended for applications running in Google environments such as Firebase App Hosting, Cloud Run, App Engine, and Cloud Functions for Firebase.
-#### Connecting using the environment
+### Initialize the SDK in non-Google environments
-To connect using environment variables, you will need to have
-the [Firebase CLI](https://firebaseopensource.com/projects/firebase/firebase-tools/) installed.
+If you are working in a non-Google server environment in which default credentials lookup can't be fully automated, you can initialize the SDK with an exported service account key file.
-Once done, you can run:
+The `initializeApp` method allows for creating multiple named app instances and specifying a custom credential, project ID and other options:
-```sh
-firebase login
+```dart
+final app = FirebaseApp.initializeApp(
+ options: AppOptions(
+ credential: Credential.fromServiceAccount(File("path/to/credential.json")),
+ projectId: "custom-project-id",
+ ),
+ name: "CUSTOM_APP",
+);
```
-And log-in to the project of your choice.
+The following `Credential` constructors are available:
-From there, you can have your Dart program authenticate
-using the environment with:
+- `Credential.fromApplicationDefaultCredentials({String? serviceAccountId})` — Uses [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) (recommended for Google environments). Optionally accepts a `serviceAccountId` to override the service account email.
+- `Credential.fromServiceAccount(File)` — Loads credentials from a service account JSON file downloaded from the Firebase Console.
+- `Credential.fromServiceAccountParams({required String privateKey, required String email, required String projectId, String? clientId})` — Builds credentials from individual service account fields directly.
+
+## Usage
+
+Once you have initialized an app instance with a credential, you can use any of the [supported services](#supported-services) to interact with Firebase.
+
+### Authentication
```dart
import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+import 'package:dart_firebase_admin/auth.dart';
-void main() {
- final admin = FirebaseAdminApp.initializeApp(
- '',
- // This will obtain authentication information from the environment
- Credential.fromApplicationDefaultCredentials(),
- );
+final app = FirebaseApp.initializeApp();
+final auth = app.auth();
+```
- // TODO use the Admin SDK
- final firestore = Firestore(admin);
- firestore.doc('hello/world').get();
-}
+#### getUser / getUserByEmail
+
+```dart
+// Get user by UID
+final userById = await auth.getUser('');
+print(userById.displayName);
+
+// Get user by email
+final userByEmail = await auth.getUserByEmail('user@example.com');
+print(userByEmail.displayName);
```
-#### Connecting using a `service-account.json` file
+#### createUser
+
+```dart
+final user = await auth.createUser(
+ CreateRequest(
+ email: 'user@example.com',
+ password: 'password123',
+ displayName: 'John Doe',
+ ),
+);
+print(user.uid);
+```
-Alternatively, you can choose to use a `service-account.json` file.
-This file can be obtained in your firebase console by going to:
+#### updateUser
+```dart
+final updatedUser = await auth.updateUser(
+ '',
+ UpdateRequest(
+ displayName: 'Jane Doe',
+ disabled: false,
+ ),
+);
+print(updatedUser.displayName);
```
-https://console.firebase.google.com/u/0/project//settings/serviceaccounts/adminsdk
+
+#### deleteUser / listUsers
+
+```dart
+// Delete a user
+await auth.deleteUser('');
+
+// List users (max 1000 per page)
+final result = await auth.listUsers(maxResults: 100);
+final users = result.users;
+final nextPageToken = result.pageToken;
```
-Make sure to replace `` with the name of your project.
-One there, follow the steps and download the file. Place it anywhere you want in your project.
+#### verifyIdToken
-**⚠️ Note**:
-This file should be kept private. Do not commit it on public repositories.
+```dart
+// Verify an ID token from a client application (e.g. from request headers)
+final idToken = req.headers['Authorization'].split(' ')[1];
+final decodedToken = await auth.verifyIdToken(idToken, checkRevoked: true);
+print(decodedToken.uid);
+```
-After all of that is done, you can now authenticate in your Dart program using:
+#### createCustomToken
```dart
-import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+final customToken = await auth.createCustomToken(
+ '',
+ developerClaims: {'role': 'admin'},
+);
+```
-Future main() async {
- final admin = FirebaseAdminApp.initializeApp(
- '',
- // Log-in using the newly downloaded file.
- Credential.fromServiceAccount(
- File(''),
- ),
- );
+#### setCustomUserClaims
+
+```dart
+await auth.setCustomUserClaims('', customUserClaims: {'role': 'admin'});
+```
- // TODO use the Admin SDK
- final firestore = Firestore(admin);
- firestore.doc('hello/world').get();
+### App Check
- // Don't forget to close the Admin SDK at the end of your "main"!
- await admin.close();
-}
+```dart
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+
+final app = FirebaseApp.initializeApp();
```
-## Firestore
+#### verifyToken
-### Usage
+```dart
+final response = await app.appCheck().verifyToken('');
+print('App ID: ${response.appId}');
+```
+
+#### createToken
-First, make sure to follow the steps on [how to authenticate](#connecting-to-the-sdk).
-You should now have an instance of a `FirebaseAdminApp` object.
+```dart
+final result = await app.appCheck().createToken('');
+print('Token: ${result.token}');
+```
-You can now use this object to create a `Firestore` object as followed:
+### Firestore
```dart
-// Obtained in the previous steps
-FirebaseAdminApp admin;
-final firestore = Firestore(admin);
+import 'dart:async';
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+import 'package:google_cloud_firestore/google_cloud_firestore.dart';
+
+final app = FirebaseApp.initializeApp();
+final firestore = app.firestore();
```
-From this point onwards, using Firestore with the admin ADK
-is roughly equivalent to using [FlutterFire](https://github.com/firebase/flutterfire).
+#### set / update / delete
+
+```dart
+final ref = firestore.collection('users').doc('');
+
+// Set a document (creates or overwrites)
+await ref.set({'name': 'John Doe', 'age': 27});
-Using this `Firestore` object, you'll find your usual collection/query/document
-objects.
+// Update specific fields
+await ref.update({'age': 28});
-For example you can perform a `where` query:
+// Delete a document
+await ref.delete();
+```
+
+#### get / query
```dart
-// The following lists all users above 18 years old
-final collection = firestore.collection('users');
-final adults = collection.where('age', WhereFilter.greaterThan, 18);
+// Get a single document
+final snapshot = await firestore.collection('users').doc('').get();
+print(snapshot.data());
+
+// Query a collection
+final querySnapshot = await firestore
+ .collection('users')
+ .where('age', WhereFilter.greaterThan, 18)
+ .orderBy('age', descending: true)
+ .get();
+print(querySnapshot.docs);
+```
-final adultsSnapshot = await adults.get();
+#### getAll
-for (final adult in adultsSnapshot.docs) {
- print(adult.data()['age']);
+```dart
+// Fetch multiple documents at once
+final snapshots = await firestore.getAll([
+ firestore.collection('users').doc('user-1'),
+ firestore.collection('users').doc('user-2'),
+]);
+for (final snap in snapshots) {
+ print(snap.data());
}
```
-Composite queries are also supported:
+#### batch
+
+```dart
+final batch = firestore.batch();
+batch.set(firestore.collection('users').doc('user-1'), {'name': 'Alice'});
+batch.update(firestore.collection('users').doc('user-2'), {FieldPath(const ['age']): 30});
+batch.delete(firestore.collection('users').doc('user-3'));
+await batch.commit();
+```
+
+#### bulkWriter
```dart
-// List users with either John or Jack as first name.
-firestore
- .collection('users')
- .whereFilter(
- Filter.or([
- Filter.where('firstName', WhereFilter.equal, 'John'),
- Filter.where('firstName', WhereFilter.equal, 'Jack'),
- ]),
+final bulkWriter = firestore.bulkWriter();
+for (var i = 0; i < 10; i++) {
+ unawaited(
+ bulkWriter.set(
+ firestore.collection('items').doc('item-$i'),
+ {'index': i},
+ ),
);
+}
+await bulkWriter.close();
```
-Alternatively, you can fetch a specific document too:
-
-```dart
-// Print the age of the user with ID "123"
-final user = await firestore.doc('users/123').get();
-print(user.data()?['age']);
-```
-
-### Supported features
-
-| Firestore | |
-| ------------------------------------------------ | --- |
-| firestore.listCollections() | ✅ |
-| reference.id | ✅ |
-| reference.listCollections() | ✅ |
-| reference.parent | ✅ |
-| reference.path | ✅ |
-| reference.== | ✅ |
-| reference.withConverter | ✅ |
-| collection.listDocuments | ✅ |
-| collection.add | ✅ |
-| collection.get | ✅ |
-| collection.create | ✅ |
-| collection.delete | ✅ |
-| collection.set | ✅ |
-| collection.update | ✅ |
-| collection.collection | ✅ |
-| query.where('field', operator, value) | ✅ |
-| query.where('field.path', operator, value) | ✅ |
-| query.where(FieldPath('...'), operator, value) | ✅ |
-| query.whereFilter(Filter.and(a, b)) | ✅ |
-| query.whereFilter(Filter.or(a, b)) | ✅ |
-| query.startAt | ✅ |
-| query.startAtDocument | ✅ |
-| query.startAfter | ✅ |
-| query.startAfterDocument | ✅ |
-| query.endAt | ✅ |
-| query.endAtDocument | ✅ |
-| query.endAfter | ✅ |
-| query.endAfterDocument | ✅ |
-| query.select | ✅ |
-| query.orderBy | ✅ |
-| query.limit | ✅ |
-| query.limitToLast | ✅ |
-| query.offset | ✅ |
-| query.count() | ✅ |
-| query.sum() | ✅ |
-| query.average() | ✅ |
-| querySnapshot.docs | ✅ |
-| querySnapshot.readTime | ✅ |
-| documentSnapshots.data | ✅ |
-| documentSnapshots.readTime/createTime/updateTime | ✅ |
-| documentSnapshots.id | ✅ |
-| documentSnapshots.exists | ✅ |
-| documentSnapshots.data | ✅ |
-| documentSnapshots.get(fieldPath) | ✅ |
-| FieldValue.documentId | ✅ |
-| FieldValue.increment | ✅ |
-| FieldValue.arrayUnion | ✅ |
-| FieldValue.arrayRemove | ✅ |
-| FieldValue.delete | ✅ |
-| FieldValue.serverTimestamp | ✅ |
-| collectionGroup | ✅ |
-| GeoPoint | ✅ |
-| Timestamp | ✅ |
-| querySnapshot.docsChange | ⚠️ |
-| query.onSnapshot | ❌ |
-| runTransaction | ✅ |
-| BundleBuilder | ❌ |
-
-## Auth
-
-### Usage
-
-First, make sure to follow the steps on [how to authenticate](#connecting-to-the-sdk).
-You should now have an instance of a `FirebaseAdminApp` object.
-
-You can now use this object to create a `Auth` object as followed:
-
-```dart
-// Obtained in the previous steps
-FirebaseAdminApp admin;
-final auth = Auth(admin);
-```
-
-You can then use this `Auth` object to perform various
-auth operations. For example, you can generate a password reset link:
-
-```dart
-final link = await auth.generatePasswordResetLink(
- 'hello@example.com',
-);
+#### runTransaction
+
+```dart
+final balance = await firestore.runTransaction((tsx) async {
+ final ref = firestore.collection('users').doc('');
+ final snapshot = await tsx.get(ref);
+ final currentBalance = snapshot.exists ? snapshot.data()?['balance'] ?? 0 : 0;
+ final newBalance = currentBalance + 10;
+ tsx.update(ref, {'balance': newBalance});
+ return newBalance;
+});
```
-### Supported features
+### Functions
+
+```dart
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+import 'package:dart_firebase_admin/functions.dart';
+
+final app = FirebaseApp.initializeApp();
+final queue = app.functions().taskQueue('');
+```
-## Available features
+#### enqueue
-| Auth | |
-| ------------------------------------- | --- |
-| auth.tenantManager | ❌ |
-| auth.projectConfigManager | ❌ |
-| auth.generatePasswordResetLink | ✅ |
-| auth.generateEmailVerificationLink | ✅ |
-| auth.generateVerifyAndChangeEmailLink | ✅ |
-| auth.generateSignInWithEmailLink | ✅ |
-| auth.listProviderConfigs | ✅ |
-| auth.createProviderConfig | ✅ |
-| auth.updateProviderConfig | ✅ |
-| auth.getProviderConfig | ✅ |
-| auth.deleteProviderConfig | ✅ |
-| auth.createCustomToken | ✅ |
-| auth.setCustomUserClaims | ✅ |
-| auth.verifyIdToken | ✅ |
-| auth.revokeRefreshTokens | ✅ |
-| auth.createSessionCookie | ✅ |
-| auth.verifySessionCookie | ✅ |
-| auth.importUsers | ✅ |
-| auth.listUsers | ✅ |
-| auth.deleteUser | ✅ |
-| auth.deleteUsers | ✅ |
-| auth.getUser | ✅ |
-| auth.getUserByPhoneNumber | ✅ |
-| auth.getUserByEmail | ✅ |
-| auth.getUserByProviderUid | ✅ |
-| auth.getUsers | ✅ |
-| auth.createUser | ✅ |
-| auth.updateUser | ✅ |
+```dart
+await queue.enqueue({'userId': 'user-123', 'action': 'sendWelcomeEmail'});
+```
-## AppCheck
+#### enqueue with TaskOptions
-### Usage
+```dart
+// Delay delivery by 1 hour
+await queue.enqueue(
+ {'action': 'cleanupTempFiles'},
+ TaskOptions(schedule: DelayDelivery(3600)),
+);
-First, make sure to follow the steps on [how to authenticate](#connecting-to-the-sdk).
-You should now have an instance of a `FirebaseAdminApp` object.
+// Schedule at a specific time
+await queue.enqueue(
+ {'action': 'sendReport'},
+ TaskOptions(schedule: AbsoluteDelivery(DateTime.now().add(Duration(hours: 1)))),
+);
-Then, you can create an instance of `AppCheck` as followed:
+// Use a custom ID for deduplication
+await queue.enqueue(
+ {'orderId': 'order-456', 'action': 'processPayment'},
+ TaskOptions(id: 'payment-order-456'),
+);
+```
+
+#### delete
```dart
-final appCheck = AppCheck();
+await queue.delete('payment-order-456');
```
-You can then use `ApPCheck` to interact with Firebase AppCheck. For example,
-this creates/verifies a token:
+### Messaging
```dart
-final token = await appCheck
- .createToken('');
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+import 'package:dart_firebase_admin/messaging.dart';
-await appCheck.verifyToken(token.token);
+final app = FirebaseApp.initializeApp();
+final messaging = app.messaging();
```
-### Supported features
-
-| AppCheck | |
-| -------------------- | --- |
-| AppCheck.createToken | ✅ |
-| AppCheck.verifyToken | ✅ |
+#### send
-## Security rules
+```dart
+// Send to a specific device
+await messaging.send(
+ TokenMessage(
+ token: '',
+ notification: Notification(title: 'Hello', body: 'World!'),
+ data: {'key': 'value'},
+ ),
+);
-### Usage
+// Send to a topic
+await messaging.send(
+ TopicMessage(
+ topic: '',
+ notification: Notification(title: 'Hello', body: 'World!'),
+ ),
+);
-First, make sure to follow the steps on [how to authenticate](#connecting-to-the-sdk).
-You should now have an instance of a `FirebaseAdminApp` object.
+// Send to a condition
+await messaging.send(
+ ConditionMessage(
+ condition: "'stock-GOOG' in topics || 'industry-tech' in topics",
+ notification: Notification(title: 'Hello', body: 'World!'),
+ ),
+);
+```
-Then, you can create an instance of `SecurityRules` as followed:
+#### sendEach
```dart
-final securityRules = SecurityRules();
+// Send up to 500 messages in a single call
+final response = await messaging.sendEach([
+ TopicMessage(topic: 'topic-1', notification: Notification(title: 'Message 1')),
+ TopicMessage(topic: 'topic-2', notification: Notification(title: 'Message 2')),
+]);
+print('Sent: ${response.successCount}, Failed: ${response.failureCount}');
```
-You can then use `SecurityRules` to interact with Firebase SecurityRules. For example,
-this creates/verifies a token:
+#### sendEachForMulticast
```dart
-final ruleset = await securityRules.createRuleset(
- RulesFile(
- name: 'firestore.rules',
- content: '',
+// Send one message to multiple device tokens
+final response = await messaging.sendEachForMulticast(
+ MulticastMessage(
+ tokens: ['', '', ''],
+ notification: Notification(title: 'Hello', body: 'World!'),
),
);
+print('Sent: ${response.successCount}, Failed: ${response.failureCount}');
+```
+
+#### subscribeToTopic / unsubscribeFromTopic
+
+```dart
+// Subscribe tokens to a topic
+await messaging.subscribeToTopic(['', ''], 'news');
+
+// Unsubscribe tokens from a topic
+await messaging.unsubscribeFromTopic(['', ''], 'news');
+```
+
+### Storage
+
+```dart
+import 'dart:typed_data';
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+import 'package:google_cloud_storage/google_cloud_storage.dart' as gcs;
-await securityRules.releaseFirestoreRuleset(ruleset.name);
+final app = FirebaseApp.initializeApp();
+final storage = app.storage();
+final bucket = storage.bucket('');
```
-### Supported features
+#### insertObject (upload)
-| SecurityRules | |
-| ----------------------------------------------- | --- |
-| SecurityRules.createRuleset | ✅ |
-| SecurityRules.getRuleset | ✅ |
-| SecurityRules.getFirestoreRuleset | ✅ |
-| SecurityRules.getStorageRuleset | ✅ |
-| SecurityRules.releaseFirestoreRuleset | ✅ |
-| SecurityRules.releaseFirestoreRulesetFromSource | ✅ |
-| SecurityRules.releaseStorageRuleset | ✅ |
-| SecurityRules.releaseStorageRulesetFromSource | ✅ |
-| SecurityRules.releaseStorageRuleset | ✅ |
-| SecurityRules.deleteRuleset | ✅ |
-| SecurityRules.listRulesetMetadata | ✅ |
+```dart
+await bucket.storage.insertObject(
+ bucket.name,
+ 'path/to/file.txt',
+ Uint8List.fromList('Hello, world!'.codeUnits),
+ metadata: gcs.ObjectMetadata(contentType: 'text/plain'),
+);
+```
-## Messaging
+#### downloadObject
+
+```dart
+final bytes = await bucket.storage.downloadObject(bucket.name, 'path/to/file.txt');
+print(String.fromCharCodes(bytes));
+```
-### Usage
+#### objectMetadata
-First, make sure to follow the steps on [how to authenticate](#connecting-to-the-sdk).
-You should now have an instance of a `FirebaseAdminApp` object.
+```dart
+final metadata = await bucket.storage.objectMetadata(bucket.name, 'path/to/file.txt');
+print('Size: ${metadata.size} bytes');
+print('Content type: ${metadata.contentType}');
+```
-Then, you can create an instance of `Messaging` as followed:
+#### deleteObject
```dart
-// Obtained in the previous steps
-FirebaseAdminApp admin;
-final messaging = Messaging(messaging);
+await bucket.storage.deleteObject(bucket.name, 'path/to/file.txt');
```
-You can then use that `Messaging` object to interact with Firebase Messaging.
-For example, if you want to send a notification to a specific device, you can do:
+#### getDownloadURL
+
+Returns a long-lived public download URL backed by a Firebase download token, suitable for sharing with end-users.
```dart
-await messaging.send(
- TokenMessage(
- // The token of the targeted device.
- // This token can be obtain by using FlutterFire's firebase_messaging:
- // https://pub.dev/documentation/firebase_messaging/latest/firebase_messaging/FirebaseMessaging/getToken.html
- token: "",
- notification: Notification(
- // The content of the notification
- title: 'Hello',
- body: 'World',
- ),
- ),
-);
+final url = await storage.getDownloadURL(bucket, 'path/to/file.txt');
+print('Download URL: $url');
```
-### Supported features
-
-| Messaging | |
-| ------------------------------ | --- |
-| Messaging.send | ✅ |
-| Messaging.sendEach | ✅ |
-| Messaging.sendEachForMulticast | ✅ |
-| Messaging.subscribeToTopic | ❌ |
-| Messaging.unsubscribeFromTopic | ❌ |
-| TokenMessage | ✅ |
-| TopicMessage | ✅ |
-| ConditionMessage | ✅ |
-
----
-
-
-
-
-
-
- Built and maintained by Invertase .
-
-
+### Security Rules
+
+```dart
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+
+final app = FirebaseApp.initializeApp();
+final securityRules = app.securityRules();
+```
+
+#### getFirestoreRuleset
+
+```dart
+final ruleset = await securityRules.getFirestoreRuleset();
+print(ruleset.name);
+```
+
+#### releaseFirestoreRulesetFromSource
+
+```dart
+final source = '''
+rules_version = '2';
+service cloud.firestore {
+ match /databases/{database}/documents {
+ match /{document=**} {
+ allow read, write: if request.auth != null;
+ }
+ }
+}
+''';
+
+final ruleset = await securityRules.releaseFirestoreRulesetFromSource(source);
+print('Applied ruleset: ${ruleset.name}');
+```
+
+#### getStorageRuleset
+
+```dart
+final ruleset = await securityRules.getStorageRuleset();
+print(ruleset.name);
+```
+
+#### releaseStorageRulesetFromSource
+
+```dart
+final source = '''
+rules_version = '2';
+service firebase.storage {
+ match /b/{bucket}/o {
+ match /{allPaths=**} {
+ allow read, write: if request.auth != null;
+ }
+ }
+}
+''';
+
+final ruleset = await securityRules.releaseStorageRulesetFromSource(source);
+print('Applied ruleset: ${ruleset.name}');
+```
+
+#### createRuleset / deleteRuleset
+
+```dart
+// Create a ruleset without applying it
+const source = "rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if false; } } }";
+final rulesFile = RulesFile(name: 'firestore.rules', content: source);
+final ruleset = await securityRules.createRuleset(rulesFile);
+print('Created ruleset: ${ruleset.name}');
+
+// Delete a ruleset by name
+await securityRules.deleteRuleset(ruleset.name);
+```
+
+## Supported Services
+
+The Firebase Admin Dart SDK currently supports the following Firebase services:
+
+🟢 - Fully supported
+🟡 - Partially supported / Work in progress
+🔴 - Not supported
+
+| Service | Status | Notes |
+|-----------------------|---------|--------------------------------|
+| App | 🟢 | |
+| App Check | 🟢 | |
+| Authentication | 🟢 | |
+| Data Connect | 🔴 | |
+| Realtime Database | 🔴 | |
+| Event Arc | 🔴 | |
+| Extensions | 🔴 | |
+| Firestore | 🟢 | Excludes realtime capabilities |
+| Functions | 🟢 | |
+| Installations | 🔴 | |
+| Machine Learning | 🔴 | |
+| Messaging | 🟢 | |
+| Project Management | 🔴 | |
+| Remote Config | 🔴 | |
+| Security Rules | 🟢 | |
+| Storage | 🟢 | |
+
+## Additional Packages
+
+Alongside the Firebase Admin Dart SDK, this repository contains additional workspace/pub.dev packages to accomodate the SDK:
+
+- [google_cloud_firestore](/packages/google_cloud_firestore/): Standalone Google APIs Firestore SDK, which the Firebase SDK extends.
+- [google_cloud_storage](https://github.com/googleapis/google-cloud-dart/tree/main/packages/google_cloud_storage): Standalone Google Cloud Storage SDK, which the Firebase SDK extends.
+
+# Contributing
+
+TODO
+
+# License
+
+[Apache License Version 2.0](LICENSE)
diff --git a/packages/dart_firebase_admin/example/.gitignore b/packages/dart_firebase_admin/example/.gitignore
index 2ac2004a..8d82c68d 100644
--- a/packages/dart_firebase_admin/example/.gitignore
+++ b/packages/dart_firebase_admin/example/.gitignore
@@ -3,6 +3,7 @@ firebase.json
.firebaserc
firestore.indexes.json
firestore.rules
+service-account-key.json
# Logs
logs
diff --git a/packages/dart_firebase_admin/example/example_functions_ts/.eslintrc.js b/packages/dart_firebase_admin/example/example_functions_ts/.eslintrc.js
new file mode 100644
index 00000000..0f8e2a9b
--- /dev/null
+++ b/packages/dart_firebase_admin/example/example_functions_ts/.eslintrc.js
@@ -0,0 +1,33 @@
+module.exports = {
+ root: true,
+ env: {
+ es6: true,
+ node: true,
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:import/errors",
+ "plugin:import/warnings",
+ "plugin:import/typescript",
+ "google",
+ "plugin:@typescript-eslint/recommended",
+ ],
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ project: ["tsconfig.json", "tsconfig.dev.json"],
+ sourceType: "module",
+ },
+ ignorePatterns: [
+ "/lib/**/*", // Ignore built files.
+ "/generated/**/*", // Ignore generated files.
+ ],
+ plugins: [
+ "@typescript-eslint",
+ "import",
+ ],
+ rules: {
+ "quotes": ["error", "double"],
+ "import/no-unresolved": 0,
+ "indent": ["error", 2],
+ },
+};
diff --git a/packages/dart_firebase_admin/example/example_functions_ts/.gitignore b/packages/dart_firebase_admin/example/example_functions_ts/.gitignore
new file mode 100644
index 00000000..961917a2
--- /dev/null
+++ b/packages/dart_firebase_admin/example/example_functions_ts/.gitignore
@@ -0,0 +1,11 @@
+# Compiled JavaScript files
+lib/**/*.js
+lib/**/*.js.map
+
+# TypeScript v1 declaration files
+typings/
+
+# Node.js dependency directory
+node_modules/
+*.local
+package-lock.json
diff --git a/packages/dart_firebase_admin/example/example_functions_ts/package.json b/packages/dart_firebase_admin/example/example_functions_ts/package.json
new file mode 100644
index 00000000..d7788081
--- /dev/null
+++ b/packages/dart_firebase_admin/example/example_functions_ts/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "functions",
+ "scripts": {
+ "lint": "eslint --ext .js,.ts .",
+ "build": "tsc",
+ "build:watch": "tsc --watch",
+ "serve": "npm run build && firebase emulators:start --only functions",
+ "shell": "npm run build && firebase functions:shell",
+ "start": "npm run shell",
+ "deploy": "firebase deploy --only functions",
+ "logs": "firebase functions:log"
+ },
+ "engines": {
+ "node": "22"
+ },
+ "main": "lib/index.js",
+ "dependencies": {
+ "firebase-admin": "^12.6.0",
+ "firebase-functions": "^6.0.1"
+ },
+ "devDependencies": {
+ "@typescript-eslint/eslint-plugin": "^5.12.0",
+ "@typescript-eslint/parser": "^5.12.0",
+ "eslint": "^8.9.0",
+ "eslint-config-google": "^0.14.0",
+ "eslint-plugin-import": "^2.25.4",
+ "firebase-functions-test": "^3.1.0",
+ "typescript": "^4.9.0"
+ },
+ "private": true
+}
diff --git a/packages/dart_firebase_admin/example/example_functions_ts/src/index.ts b/packages/dart_firebase_admin/example/example_functions_ts/src/index.ts
new file mode 100644
index 00000000..46c66104
--- /dev/null
+++ b/packages/dart_firebase_admin/example/example_functions_ts/src/index.ts
@@ -0,0 +1,28 @@
+/**
+ * Import function triggers from their respective submodules:
+ *
+ * import {onCall} from "firebase-functions/v2/https";
+ * import {onDocumentWritten} from "firebase-functions/v2/firestore";
+ *
+ * See a full list of supported triggers at https://firebase.google.com/docs/functions
+ */
+
+import {onTaskDispatched} from "firebase-functions/v2/tasks";
+
+// Start writing functions
+// https://firebase.google.com/docs/functions/typescript
+
+export const helloWorld = onTaskDispatched(
+ {
+ retryConfig: {
+ maxAttempts: 5,
+ minBackoffSeconds: 60,
+ },
+ rateLimits: {
+ maxConcurrentDispatches: 6,
+ },
+ },
+ async (req) => {
+ console.log("Task received:", req.data);
+ }
+);
diff --git a/packages/dart_firebase_admin/example/example_functions_ts/tsconfig.dev.json b/packages/dart_firebase_admin/example/example_functions_ts/tsconfig.dev.json
new file mode 100644
index 00000000..7560eed4
--- /dev/null
+++ b/packages/dart_firebase_admin/example/example_functions_ts/tsconfig.dev.json
@@ -0,0 +1,5 @@
+{
+ "include": [
+ ".eslintrc.js"
+ ]
+}
diff --git a/packages/dart_firebase_admin/example/example_functions_ts/tsconfig.json b/packages/dart_firebase_admin/example/example_functions_ts/tsconfig.json
new file mode 100644
index 00000000..57b915f3
--- /dev/null
+++ b/packages/dart_firebase_admin/example/example_functions_ts/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "module": "NodeNext",
+ "esModuleInterop": true,
+ "moduleResolution": "nodenext",
+ "noImplicitReturns": true,
+ "noUnusedLocals": true,
+ "outDir": "lib",
+ "sourceMap": true,
+ "strict": true,
+ "target": "es2017"
+ },
+ "compileOnSave": true,
+ "include": [
+ "src"
+ ]
+}
diff --git a/packages/dart_firebase_admin/example/lib/app_check_example.dart b/packages/dart_firebase_admin/example/lib/app_check_example.dart
new file mode 100644
index 00000000..928dd32f
--- /dev/null
+++ b/packages/dart_firebase_admin/example/lib/app_check_example.dart
@@ -0,0 +1,76 @@
+import 'package:dart_firebase_admin/app_check.dart';
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+
+Future appCheckExample(FirebaseApp admin) async {
+ print('\n### App Check Example ###\n');
+
+ final appCheck = admin.appCheck();
+
+ // Example 1: Create an App Check token for a client app
+ try {
+ print('> Creating App Check token...\n');
+ final token = await appCheck.createToken('');
+ print('Token created successfully!');
+ print(' - Token: ${token.token}');
+ print(' - TTL: ${token.ttlMillis}ms');
+ print('');
+ } on FirebaseAppCheckException catch (e) {
+ print('> App Check error: ${e.code} - ${e.message}');
+ } catch (e) {
+ print('> Error creating token: $e');
+ }
+
+ // Example 2: Create a token with a custom TTL (e.g. 1 hour)
+ try {
+ print('> Creating App Check token with custom TTL...\n');
+ final token = await appCheck.createToken(
+ '',
+ AppCheckTokenOptions(ttlMillis: const Duration(hours: 1)),
+ );
+ print('Token with custom TTL created!');
+ print(' - TTL: ${token.ttlMillis}ms');
+ print('');
+ } on FirebaseAppCheckException catch (e) {
+ print('> App Check error: ${e.code} - ${e.message}');
+ } catch (e) {
+ print('> Error creating token with TTL: $e');
+ }
+
+ // Example 3: Verify an App Check token from a client request
+ try {
+ print('> Verifying App Check token...\n');
+ final response = await appCheck.verifyToken('');
+ print('Token verified successfully!');
+ print(' - App ID: ${response.appId}');
+ print(' - Already consumed: ${response.alreadyConsumed}');
+ print('');
+ } on FirebaseAppCheckException catch (e) {
+ if (e.code == AppCheckErrorCode.appCheckTokenExpired.code) {
+ print('> Token has expired');
+ } else {
+ print('> App Check error: ${e.code} - ${e.message}');
+ }
+ } catch (e) {
+ print('> Error verifying token: $e');
+ }
+
+ // Example 4: Verify with replay protection (consume the token)
+ try {
+ print('> Verifying App Check token with replay protection...\n');
+ final response = await appCheck.verifyToken(
+ '',
+ VerifyAppCheckTokenOptions()..consume = true,
+ );
+ if (response.alreadyConsumed ?? false) {
+ print('> Token was already consumed — possible replay attack!');
+ } else {
+ print('Token consumed and verified!');
+ print(' - App ID: ${response.appId}');
+ }
+ print('');
+ } on FirebaseAppCheckException catch (e) {
+ print('> App Check error: ${e.code} - ${e.message}');
+ } catch (e) {
+ print('> Error verifying token with replay protection: $e');
+ }
+}
diff --git a/packages/dart_firebase_admin/example/lib/auth_example.dart b/packages/dart_firebase_admin/example/lib/auth_example.dart
new file mode 100644
index 00000000..b637b3bc
--- /dev/null
+++ b/packages/dart_firebase_admin/example/lib/auth_example.dart
@@ -0,0 +1,193 @@
+import 'package:dart_firebase_admin/auth.dart';
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+
+Future authExample(FirebaseApp admin) async {
+ print('\n### Auth Example ###\n');
+
+ final auth = admin.auth();
+
+ UserRecord? user;
+ try {
+ print('> Check if user with email exists: test@example.com\n');
+ user = await auth.getUserByEmail('test@example.com');
+ print('> User found by email\n');
+ } on FirebaseAuthAdminException catch (e) {
+ if (e.errorCode == AuthClientErrorCode.userNotFound) {
+ print('> User not found, creating new user\n');
+ user = await auth.createUser(
+ CreateRequest(email: 'test@example.com', password: 'Test@12345'),
+ );
+ } else {
+ print('> Auth error: ${e.errorCode} - ${e.message}');
+ }
+ } catch (e, stackTrace) {
+ print('> Unexpected error: $e');
+ print('Stack trace: $stackTrace');
+ }
+
+ if (user != null) {
+ print('Fetched user email: ${user.email}');
+ }
+}
+
+Future projectConfigExample(FirebaseApp admin) async {
+ print('\n### Project Config Example ###\n');
+
+ final projectConfigManager = admin.auth().projectConfigManager;
+
+ try {
+ // Get current project configuration
+ print('> Fetching current project configuration...\n');
+ final config = await projectConfigManager.getProjectConfig();
+
+ // Display current configuration
+ print('Current project configuration:');
+ if (config.emailPrivacyConfig != null) {
+ print(
+ ' - Email Privacy: ${config.emailPrivacyConfig!.enableImprovedEmailPrivacy}',
+ );
+ }
+ if (config.passwordPolicyConfig != null) {
+ print(
+ ' - Password Policy: ${config.passwordPolicyConfig!.enforcementState}',
+ );
+ }
+ if (config.smsRegionConfig != null) {
+ print(' - SMS Region Config: enabled');
+ }
+ if (config.mobileLinksConfig != null) {
+ print(' - Mobile Links: ${config.mobileLinksConfig!.domain?.value}');
+ }
+ print('');
+
+ // Example: Update email privacy configuration
+ print('> Updating email privacy configuration...\n');
+ final updatedConfig = await projectConfigManager.updateProjectConfig(
+ UpdateProjectConfigRequest(
+ emailPrivacyConfig: EmailPrivacyConfig(
+ enableImprovedEmailPrivacy: true,
+ ),
+ ),
+ );
+
+ print('Configuration updated successfully!');
+ if (updatedConfig.emailPrivacyConfig != null) {
+ print(
+ ' - Improved Email Privacy: ${updatedConfig.emailPrivacyConfig!.enableImprovedEmailPrivacy}',
+ );
+ }
+ } on FirebaseAuthAdminException catch (e) {
+ print('> Auth error: ${e.code} - ${e.message}');
+ } catch (e) {
+ print('> Error managing project config: $e');
+ }
+}
+
+/// Tenant management example.
+///
+/// Steps to enable Identity Platform:
+///
+/// 1. Go to Google Cloud Console (not Firebase Console):
+/// - Visit: https://console.cloud.google.com/
+/// - Select your project
+///
+/// 2. Enable Identity Platform API:
+/// - In the search bar, search for "Identity Platform"
+/// - Click on "Identity Platform"
+/// - Click "Enable API" if not already enabled
+///
+/// 3. Upgrade to Identity Platform:
+/// - Once in Identity Platform, look for an "Upgrade" or "Get Started" button
+/// - Follow the prompts to upgrade from Firebase Auth to Identity Platform
+///
+/// 4. Enable Multi-tenancy:
+/// - After upgrading, go to Settings
+/// - Look for "Multi-tenancy" option
+/// - Enable it
+Future tenantExample(FirebaseApp admin) async {
+ print('\n### Tenant Example ###\n');
+
+ final tenantManager = admin.auth().tenantManager;
+
+ String? createdTenantId;
+
+ try {
+ print('> Creating a new tenant...\n');
+ final newTenant = await tenantManager.createTenant(
+ UpdateTenantRequest(
+ displayName: 'example-tenant',
+ emailSignInConfig: EmailSignInProviderConfig(
+ enabled: true,
+ passwordRequired: true,
+ ),
+ ),
+ );
+ createdTenantId = newTenant.tenantId;
+
+ print('Tenant created successfully!');
+ print(' - Tenant ID: ${newTenant.tenantId}');
+ print(' - Display Name: ${newTenant.displayName}');
+ print('');
+
+ // Get the tenant
+ print('> Fetching tenant details...\n');
+ final tenant = await tenantManager.getTenant(createdTenantId);
+ print('Tenant details:');
+ print(' - ID: ${tenant.tenantId}');
+ print(' - Display Name: ${tenant.displayName}');
+ print('');
+
+ // Update the tenant
+ print('> Updating tenant...\n');
+ final updatedTenant = await tenantManager.updateTenant(
+ createdTenantId,
+ UpdateTenantRequest(displayName: 'updated-tenant'),
+ );
+ print('Tenant updated successfully!');
+ print(' - New Display Name: ${updatedTenant.displayName}');
+ print('');
+
+ // List tenants
+ print('> Listing all tenants...\n');
+ final listResult = await tenantManager.listTenants();
+ print('Found ${listResult.tenants.length} tenant(s)');
+ for (final t in listResult.tenants) {
+ print(' - ${t.tenantId}: ${t.displayName}');
+ }
+ print('');
+
+ // Delete the tenant
+ print('> Deleting tenant...\n');
+ await tenantManager.deleteTenant(createdTenantId);
+ print('Tenant deleted successfully!');
+ } on FirebaseAuthAdminException catch (e) {
+ if (e.code == 'auth/invalid-project-id') {
+ print('> Multi-tenancy is not enabled for this project.');
+ print(
+ ' Enable it in Firebase Console under Identity Platform settings.',
+ );
+ } else {
+ print('> Auth error: ${e.code} - ${e.message}');
+ }
+
+ // Clean up if tenant was created
+ if (createdTenantId != null) {
+ try {
+ await tenantManager.deleteTenant(createdTenantId);
+ } catch (_) {
+ // Ignore cleanup errors
+ }
+ }
+ } catch (e) {
+ print('> Error managing tenants: $e');
+
+ // Clean up if tenant was created
+ if (createdTenantId != null) {
+ try {
+ await tenantManager.deleteTenant(createdTenantId);
+ } catch (_) {
+ // Ignore cleanup errors
+ }
+ }
+ }
+}
diff --git a/packages/dart_firebase_admin/example/lib/firestore_example.dart b/packages/dart_firebase_admin/example/lib/firestore_example.dart
new file mode 100644
index 00000000..3354b3aa
--- /dev/null
+++ b/packages/dart_firebase_admin/example/lib/firestore_example.dart
@@ -0,0 +1,262 @@
+import 'dart:async';
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+import 'package:google_cloud_firestore/google_cloud_firestore.dart';
+
+/// Main entry point for all Firestore examples
+Future firestoreExample(FirebaseApp admin) async {
+ print('\n### Firestore Examples ###\n');
+
+ await basicFirestoreExample(admin);
+ await multiDatabaseExample(admin);
+ await bulkWriterExamples(admin);
+ await bundleBuilderExample(admin);
+}
+
+/// Example 1: Basic Firestore operations with default database
+Future basicFirestoreExample(FirebaseApp admin) async {
+ print('> Basic Firestore operations (default database)...\n');
+
+ final firestore = admin.firestore();
+
+ try {
+ final collection = firestore.collection('users');
+ await collection.doc('123').set({'name': 'John Doe', 'age': 27});
+ final snapshot = await collection.get();
+ for (final doc in snapshot.docs) {
+ print('> Document data: ${doc.data()}');
+ }
+ } catch (e) {
+ print('> Error: $e');
+ }
+ print('');
+}
+
+/// Example 2: Multi-database support
+Future multiDatabaseExample(FirebaseApp admin) async {
+ print('### Multi-Database Examples ###\n');
+
+ // Named database
+ print('> Using named database "my-database"...\n');
+ final namedFirestore = admin.firestore(databaseId: 'my-database');
+
+ try {
+ final collection = namedFirestore.collection('products');
+ await collection.doc('product-1').set({
+ 'name': 'Widget',
+ 'price': 19.99,
+ 'inStock': true,
+ });
+ print('> Document written to named database\n');
+
+ final doc = await collection.doc('product-1').get();
+ if (doc.exists) {
+ print('> Retrieved from named database: ${doc.data()}');
+ }
+ } catch (e) {
+ print('> Error with named database: $e');
+ }
+
+ // Multiple databases simultaneously
+ print('\n> Demonstrating multiple database access...\n');
+ try {
+ final defaultDb = admin.firestore();
+ final analyticsDb = admin.firestore(databaseId: 'analytics-db');
+
+ await defaultDb.collection('users').doc('user-1').set({
+ 'name': 'Alice',
+ 'email': 'alice@example.com',
+ });
+
+ await analyticsDb.collection('events').doc('event-1').set({
+ 'type': 'page_view',
+ 'timestamp': DateTime.now().toIso8601String(),
+ 'userId': 'user-1',
+ });
+
+ print('> Successfully wrote to multiple databases');
+ } catch (e) {
+ print('> Error with multiple databases: $e');
+ }
+ print('');
+}
+
+/// BulkWriter examples demonstrating common patterns
+Future bulkWriterExamples(FirebaseApp admin) async {
+ print('### BulkWriter Examples ###\n');
+
+ final firestore = admin.firestore();
+
+ await bulkWriterBasicExample(firestore);
+ await bulkWriterErrorHandlingExample(firestore);
+}
+
+/// Basic BulkWriter usage
+Future bulkWriterBasicExample(Firestore firestore) async {
+ print('> Basic BulkWriter usage...\n');
+
+ try {
+ final bulkWriter = firestore.bulkWriter();
+
+ // Queue multiple write operations (don't await individual operations)
+ for (var i = 0; i < 10; i++) {
+ unawaited(
+ bulkWriter.set(firestore.collection('bulk-demo').doc('item-$i'), {
+ 'name': 'Item $i',
+ 'index': i,
+ 'createdAt': DateTime.now().toIso8601String(),
+ }),
+ );
+ }
+
+ await bulkWriter.close();
+ print('> Successfully wrote 10 documents in bulk\n');
+ } catch (e) {
+ print('> Error: $e');
+ }
+}
+
+/// BulkWriter with error handling and retry logic
+Future bulkWriterErrorHandlingExample(Firestore firestore) async {
+ print('> BulkWriter with error handling and retry logic...\n');
+
+ try {
+ final bulkWriter = firestore.bulkWriter();
+
+ var successCount = 0;
+ var errorCount = 0;
+
+ bulkWriter.onWriteResult((ref, result) {
+ successCount++;
+ print(' ✓ Success: ${ref.path} at ${result.writeTime}');
+ });
+
+ bulkWriter.onWriteError((error) {
+ errorCount++;
+ print(' ✗ Error: ${error.documentRef.path} - ${error.message}');
+
+ // Retry on transient errors, but not more than 3 times
+ if (error.failedAttempts < 3 &&
+ (error.code.name == 'unavailable' || error.code.name == 'aborted')) {
+ print(' → Retrying (attempt ${error.failedAttempts + 1})...');
+ return true;
+ }
+ return false;
+ });
+
+ // Mix of operations (queue them, don't await)
+ // Use set() instead of create() to make example idempotent
+ unawaited(
+ bulkWriter.set(firestore.collection('orders').doc('order-1'), {
+ 'status': 'pending',
+ 'total': 99.99,
+ }),
+ );
+
+ unawaited(
+ bulkWriter.set(firestore.collection('orders').doc('order-2'), {
+ 'status': 'completed',
+ 'total': 149.99,
+ }),
+ );
+
+ final orderRef = firestore.collection('orders').doc('order-3');
+ await orderRef.set({'status': 'processing'});
+
+ unawaited(
+ bulkWriter.update(orderRef, {
+ FieldPath(const ['status']): 'shipped',
+ FieldPath(const ['shippedAt']): DateTime.now().toIso8601String(),
+ }),
+ );
+
+ unawaited(
+ bulkWriter.delete(firestore.collection('orders').doc('order-to-delete')),
+ );
+
+ await bulkWriter.close();
+
+ print('\n> BulkWriter completed:');
+ print(' - Successful writes: $successCount');
+ print(' - Failed writes: $errorCount\n');
+ } catch (e) {
+ print('> Error: $e');
+ }
+}
+
+/// BundleBuilder example demonstrating data bundle creation
+Future bundleBuilderExample(FirebaseApp admin) async {
+ print('### BundleBuilder Example ###\n');
+
+ final firestore = admin.firestore();
+
+ try {
+ print('> Creating a data bundle...\n');
+
+ // Create a bundle
+ final bundle = firestore.bundle('example-bundle');
+
+ // Create and add some sample documents
+ final collection = firestore.collection('bundle-demo');
+
+ // Add individual documents
+ await collection.doc('user-1').set({
+ 'name': 'Alice Smith',
+ 'role': 'admin',
+ 'lastLogin': DateTime.now().toIso8601String(),
+ });
+
+ await collection.doc('user-2').set({
+ 'name': 'Bob Johnson',
+ 'role': 'user',
+ 'lastLogin': DateTime.now().toIso8601String(),
+ });
+
+ await collection.doc('user-3').set({
+ 'name': 'Charlie Brown',
+ 'role': 'user',
+ 'lastLogin': DateTime.now().toIso8601String(),
+ });
+
+ // Get snapshots and add to bundle
+ final doc1 = await collection.doc('user-1').get();
+ final doc2 = await collection.doc('user-2').get();
+ final doc3 = await collection.doc('user-3').get();
+
+ bundle.addDocument(doc1);
+ bundle.addDocument(doc2);
+ bundle.addDocument(doc3);
+
+ print(' ✓ Added 3 documents to bundle');
+
+ // Add a query to the bundle
+ final query = collection.where('role', WhereFilter.equal, 'user');
+ final querySnapshot = await query.get();
+
+ bundle.addQuery('regular-users', querySnapshot);
+
+ print(' ✓ Added query "regular-users" to bundle');
+
+ // Build the bundle
+ final bundleData = bundle.build();
+
+ print('\n> Bundle created successfully!');
+ print(' - Bundle size: ${bundleData.length} bytes');
+ print(' - Contains: 3 documents + 1 named query');
+ print('\n You can now:');
+ print(' - Serve this bundle via CDN');
+ print(' - Save to a file for static hosting');
+ print(' - Send to clients for offline-first apps');
+ print(' - Cache and reuse across multiple client sessions\n');
+
+ // Example: Save to file (commented out)
+ // import 'dart:io';
+ // await File('bundle.txt').writeAsBytes(bundleData);
+
+ // Clean up
+ await collection.doc('user-1').delete();
+ await collection.doc('user-2').delete();
+ await collection.doc('user-3').delete();
+ } catch (e) {
+ print('> Error creating bundle: $e');
+ }
+}
diff --git a/packages/dart_firebase_admin/example/lib/functions_example.dart b/packages/dart_firebase_admin/example/lib/functions_example.dart
new file mode 100644
index 00000000..d118b93d
--- /dev/null
+++ b/packages/dart_firebase_admin/example/lib/functions_example.dart
@@ -0,0 +1,81 @@
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+import 'package:dart_firebase_admin/functions.dart';
+
+/// Functions example prerequisites:
+/// 1) Run `npm run build` in `example_functions_ts` to generate `index.js`.
+/// 2) From the example directory root (with `firebase.json` and `.firebaserc`),
+/// start emulators with `firebase emulators:start`.
+/// 3) Run `dart_firebase_admin/packages/dart_firebase_admin/example/run_with_emulator.sh`.
+Future functionsExample(FirebaseApp admin) async {
+ print('\n### Functions Example ###\n');
+
+ final functions = admin.functions();
+
+ // Get a task queue reference
+ // The function name should match an existing Cloud Function or queue name
+ final taskQueue = functions.taskQueue('helloWorld');
+
+ // Example 1: Enqueue a simple task
+ try {
+ print('> Enqueuing a simple task...\n');
+ await taskQueue.enqueue({
+ 'userId': 'user-123',
+ 'action': 'sendWelcomeEmail',
+ 'timestamp': DateTime.now().toIso8601String(),
+ });
+ print('Task enqueued successfully!\n');
+ } on FirebaseFunctionsAdminException catch (e) {
+ print('> Functions error: ${e.code} - ${e.message}\n');
+ } catch (e) {
+ print('> Error enqueuing task: $e\n');
+ }
+
+ // Example 2: Enqueue with delay (1 hour from now)
+ try {
+ print('> Enqueuing a delayed task...\n');
+ await taskQueue.enqueue(
+ {'action': 'cleanupTempFiles'},
+ TaskOptions(schedule: DelayDelivery(3600)), // 1 hour delay
+ );
+ print('Delayed task enqueued successfully!\n');
+ } on FirebaseFunctionsAdminException catch (e) {
+ print('> Functions error: ${e.code} - ${e.message}\n');
+ }
+
+ // Example 3: Enqueue at specific time
+ try {
+ print('> Enqueuing a scheduled task...\n');
+ final scheduledTime = DateTime.now().add(const Duration(minutes: 30));
+ await taskQueue.enqueue({
+ 'action': 'sendReport',
+ }, TaskOptions(schedule: AbsoluteDelivery(scheduledTime)));
+ print('Scheduled task enqueued for: $scheduledTime\n');
+ } on FirebaseFunctionsAdminException catch (e) {
+ print('> Functions error: ${e.code} - ${e.message}\n');
+ }
+
+ // Example 4: Enqueue with custom task ID (for deduplication)
+ try {
+ print('> Enqueuing a task with custom ID...\n');
+ await taskQueue.enqueue({
+ 'orderId': 'order-456',
+ 'action': 'processPayment',
+ }, TaskOptions(id: 'payment-order-456'));
+ print('Task with custom ID enqueued!\n');
+ } on FirebaseFunctionsAdminException catch (e) {
+ if (e.errorCode == FunctionsClientErrorCode.taskAlreadyExists) {
+ print('> Task with this ID already exists (deduplication)\n');
+ } else {
+ print('> Functions error: ${e.code} - ${e.message}\n');
+ }
+ }
+
+ // Example 5: Delete a task
+ try {
+ print('> Deleting task...\n');
+ await taskQueue.delete('payment-order-456');
+ print('Task deleted successfully!\n');
+ } on FirebaseFunctionsAdminException catch (e) {
+ print('> Functions error: ${e.code} - ${e.message}\n');
+ }
+}
diff --git a/packages/dart_firebase_admin/example/lib/main.dart b/packages/dart_firebase_admin/example/lib/main.dart
index 69c148e5..b92c2419 100644
--- a/packages/dart_firebase_admin/example/lib/main.dart
+++ b/packages/dart_firebase_admin/example/lib/main.dart
@@ -1,44 +1,32 @@
import 'package:dart_firebase_admin/dart_firebase_admin.dart';
-import 'package:dart_firebase_admin/firestore.dart';
-import 'package:dart_firebase_admin/messaging.dart';
Future main() async {
- final admin = FirebaseAdminApp.initializeApp(
- 'dart-firebase-admin',
- Credential.fromApplicationDefaultCredentials(),
- );
+ final admin = FirebaseApp.initializeApp();
- // // admin.useEmulator();
+ // Uncomment to run auth example
+ // await authExample(admin);
- final messaging = Messaging(admin);
+ // Uncomment to run project config example
+ // await projectConfigExample(admin);
- final result = await messaging.send(
- TokenMessage(
- token:
- 'e8Ap1n9UTQenyB-UEjNQt9:APA91bHhgc9RZYDcCKb7U1scQo1K0ZTSMItop8IqctrOcgvmN__oBo4vgbFX-ji4atr1PVw3Loug-eOCBmj4HVZjUE0aQBA0mGry7uL-7JuMaojhtl13MpvQtbZptvX_8f6vDcqei88O',
- notification: Notification(
- title: 'Hello',
- body: 'World',
- ),
- ),
- );
+ // Uncomment to run tenant example (requires Identity Platform upgrade)
+ // await tenantExample(admin);
- print(result);
+ // await firestoreExample(admin);
- final firestore = Firestore(admin);
+ // await functionsExample(admin);
- final collection = firestore.collection('users');
+ // Uncomment to run messaging example (requires valid fcm token)
+ // await messagingExample(admin);
- await collection.doc('123').set({
- 'name': 'John Doe',
- 'age': 30,
- });
+ // Uncomment to run storage example
+ // await storageExample(admin);
- final snapshot = await collection.get();
+ // Uncomment to run app check example
+ // await appCheckExample(admin);
- for (final doc in snapshot.docs) {
- print(doc.data());
- }
+ // Uncomment to run security rules example
+ // await securityRulesExample(admin);
await admin.close();
}
diff --git a/packages/dart_firebase_admin/example/lib/messaging_example.dart b/packages/dart_firebase_admin/example/lib/messaging_example.dart
new file mode 100644
index 00000000..ce8b73fe
--- /dev/null
+++ b/packages/dart_firebase_admin/example/lib/messaging_example.dart
@@ -0,0 +1,152 @@
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+import 'package:dart_firebase_admin/messaging.dart';
+
+Future messagingExample(FirebaseApp admin) async {
+ print('\n### Messaging Example ###\n');
+
+ final messaging = admin.messaging();
+
+ // Example 1: Send a message to a topic
+ try {
+ print('> Sending message to topic: fcm_test_topic\n');
+ final messageId = await messaging.send(
+ TopicMessage(
+ topic: 'fcm_test_topic',
+ notification: Notification(
+ title: 'Hello World',
+ body: 'Dart Firebase Admin SDK works!',
+ ),
+ data: {'timestamp': DateTime.now().toIso8601String()},
+ ),
+ );
+ print('Message sent successfully!');
+ print(' - Message ID: $messageId');
+ print('');
+ } on FirebaseMessagingAdminException catch (e) {
+ print('> Messaging error: ${e.code} - ${e.message}');
+ } catch (e) {
+ print('> Error sending message: $e');
+ }
+
+ // Example 2: Send multiple messages
+ try {
+ print('> Sending multiple messages...\n');
+ final response = await messaging.sendEach([
+ TopicMessage(
+ topic: 'topic1',
+ notification: Notification(title: 'Message 1'),
+ ),
+ TopicMessage(
+ topic: 'topic2',
+ notification: Notification(title: 'Message 2'),
+ ),
+ ]);
+
+ print('Batch send completed!');
+ print(' - Success: ${response.successCount}');
+ print(' - Failures: ${response.failureCount}');
+ for (var i = 0; i < response.responses.length; i++) {
+ final resp = response.responses[i];
+ if (resp.success) {
+ print(' - Message $i: ${resp.messageId}');
+ } else {
+ print(' - Message $i failed: ${resp.error?.message}');
+ }
+ }
+ print('');
+ } on FirebaseMessagingAdminException catch (e) {
+ print('> Messaging error: ${e.code} - ${e.message}');
+ } catch (e) {
+ print('> Error sending batch: $e');
+ }
+
+ // Example 3: Send multicast message to multiple tokens
+ try {
+ print('> Sending multicast message...\n');
+ // Note: Using fake tokens for demonstration
+ final response = await messaging.sendEachForMulticast(
+ MulticastMessage(
+ tokens: ['fake-token-1', 'fake-token-2'],
+ notification: Notification(
+ title: 'Multicast Message',
+ body: 'This goes to multiple devices',
+ ),
+ ),
+ dryRun: true, // Use dry run to validate without actually sending
+ );
+
+ print('Multicast send completed!');
+ print(' - Success: ${response.successCount}');
+ print(' - Failures: ${response.failureCount}');
+ print('');
+ } on FirebaseMessagingAdminException catch (e) {
+ print('> Messaging error: ${e.code} - ${e.message}');
+ } catch (e) {
+ print('> Error sending multicast: $e');
+ }
+
+ // Example 4: Subscribe tokens to a topic
+ try {
+ print('> Subscribing tokens to topic: test-topic\n');
+ // Note: Using fake token for demonstration
+ final response = await messaging.subscribeToTopic([
+ 'fake-registration-token',
+ ], 'test-topic');
+
+ print('Subscription completed!');
+ print(' - Success: ${response.successCount}');
+ print(' - Failures: ${response.failureCount}');
+ if (response.errors.isNotEmpty) {
+ for (final error in response.errors) {
+ print(' - Token ${error.index} error: ${error.error.message}');
+ }
+ }
+ print('');
+ } on FirebaseMessagingAdminException catch (e) {
+ if (e.errorCode == MessagingClientErrorCode.invalidArgument) {
+ print('> Invalid topic format or empty tokens list');
+ } else {
+ print('> Messaging error: ${e.code} - ${e.message}');
+ }
+ } catch (e) {
+ print('> Error subscribing to topic: $e');
+ }
+
+ // Example 5: Send with platform-specific options
+ try {
+ print('> Sending message with platform-specific options...\n');
+ final messageId = await messaging.send(
+ TokenMessage(
+ token: 'fake-device-token',
+ notification: Notification(
+ title: 'Platform-specific message',
+ body: 'With Android and iOS options',
+ ),
+ android: AndroidConfig(
+ priority: AndroidConfigPriority.high,
+ notification: AndroidNotification(color: '#FF0000', sound: 'default'),
+ ),
+ apns: ApnsConfig(
+ payload: ApnsPayload(
+ aps: Aps(
+ contentAvailable: true,
+ sound: CriticalSound(critical: true, name: 'default'),
+ ),
+ ),
+ ),
+ ),
+ dryRun: true, // Use dry run to validate
+ );
+
+ print('Platform-specific message validated!');
+ print(' - Message ID: $messageId');
+ } on FirebaseMessagingAdminException catch (e) {
+ if (e.errorCode == MessagingClientErrorCode.invalidRegistrationToken) {
+ print('> Invalid registration token format');
+ } else {
+ print('> Messaging error: ${e.code} - ${e.message}');
+ }
+ } catch (e) {
+ print('> Error sending platform-specific message: $e');
+ }
+}
diff --git a/packages/dart_firebase_admin/example/lib/security_rules_example.dart b/packages/dart_firebase_admin/example/lib/security_rules_example.dart
new file mode 100644
index 00000000..6cb5fa3e
--- /dev/null
+++ b/packages/dart_firebase_admin/example/lib/security_rules_example.dart
@@ -0,0 +1,127 @@
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+import 'package:dart_firebase_admin/security_rules.dart';
+
+Future securityRulesExample(FirebaseApp admin) async {
+ print('\n### Security Rules Example ###\n');
+
+ final securityRules = admin.securityRules();
+
+ // Example 1: Get the currently applied Firestore ruleset
+ try {
+ print('> Fetching current Firestore ruleset...\n');
+ final ruleset = await securityRules.getFirestoreRuleset();
+ print('Current Firestore ruleset:');
+ print(' - Name: ${ruleset.name}');
+ print(' - Created: ${ruleset.createTime}');
+ print('');
+ } on FirebaseSecurityRulesException catch (e) {
+ print('> Security Rules error: ${e.code} - ${e.message}');
+ } catch (e) {
+ print('> Error fetching Firestore ruleset: $e');
+ }
+
+ // Example 2: Deploy new Firestore rules from source
+ try {
+ print('> Deploying new Firestore rules...\n');
+ const source = """
+rules_version = '2';
+service cloud.firestore {
+ match /databases/{database}/documents {
+ match /{document=**} {
+ allow read, write: if request.auth != null;
+ }
+ }
+}
+""";
+ final ruleset = await securityRules.releaseFirestoreRulesetFromSource(
+ source,
+ );
+ print('Firestore rules deployed successfully!');
+ print(' - Ruleset name: ${ruleset.name}');
+ print('');
+ } on FirebaseSecurityRulesException catch (e) {
+ print('> Security Rules error: ${e.code} - ${e.message}');
+ } catch (e) {
+ print('> Error deploying Firestore rules: $e');
+ }
+
+ // Example 3: Get the currently applied Storage ruleset
+ try {
+ print('> Fetching current Storage ruleset...\n');
+ final ruleset = await securityRules.getStorageRuleset();
+ print('Current Storage ruleset:');
+ print(' - Name: ${ruleset.name}');
+ print(' - Created: ${ruleset.createTime}');
+ print('');
+ } on FirebaseSecurityRulesException catch (e) {
+ print('> Security Rules error: ${e.code} - ${e.message}');
+ } catch (e) {
+ print('> Error fetching Storage ruleset: $e');
+ }
+
+ // Example 4: Deploy new Storage rules from source
+ try {
+ print('> Deploying new Storage rules...\n');
+ const source = """
+rules_version = '2';
+service firebase.storage {
+ match /b/{bucket}/o {
+ match /{allPaths=**} {
+ allow read, write: if request.auth != null;
+ }
+ }
+}
+""";
+ final ruleset = await securityRules.releaseStorageRulesetFromSource(source);
+ print('Storage rules deployed successfully!');
+ print(' - Ruleset name: ${ruleset.name}');
+ print('');
+ } on FirebaseSecurityRulesException catch (e) {
+ print('> Security Rules error: ${e.code} - ${e.message}');
+ } catch (e) {
+ print('> Error deploying Storage rules: $e');
+ }
+
+ // Example 5: Create a ruleset and delete it
+ try {
+ print('> Creating a standalone ruleset...\n');
+ const source = """
+rules_version = '2';
+service cloud.firestore {
+ match /databases/{database}/documents {
+ match /{document=**} {
+ allow read, write: if false;
+ }
+ }
+}
+""";
+ final rulesFile = RulesFile(name: 'firestore.rules', content: source);
+ final ruleset = await securityRules.createRuleset(rulesFile);
+ print('Ruleset created: ${ruleset.name}');
+
+ await securityRules.deleteRuleset(ruleset.name);
+ print('Ruleset deleted successfully!\n');
+ } on FirebaseSecurityRulesException catch (e) {
+ print('> Security Rules error: ${e.code} - ${e.message}');
+ } catch (e) {
+ print('> Error creating/deleting ruleset: $e');
+ }
+
+ // Example 6: List existing rulesets
+ try {
+ print('> Listing rulesets...\n');
+ final result = await securityRules.listRulesetMetadata(pageSize: 10);
+ print('Found ${result.rulesets.length} ruleset(s):');
+ for (final meta in result.rulesets) {
+ print(' - ${meta.name} (created: ${meta.createTime})');
+ }
+ if (result.nextPageToken != null) {
+ print(' (more rulesets available — use nextPageToken to paginate)');
+ }
+ print('');
+ } on FirebaseSecurityRulesException catch (e) {
+ print('> Security Rules error: ${e.code} - ${e.message}');
+ } catch (e) {
+ print('> Error listing rulesets: $e');
+ }
+}
diff --git a/packages/dart_firebase_admin/example/lib/storage_example.dart b/packages/dart_firebase_admin/example/lib/storage_example.dart
new file mode 100644
index 00000000..3eaccc4e
--- /dev/null
+++ b/packages/dart_firebase_admin/example/lib/storage_example.dart
@@ -0,0 +1,83 @@
+import 'dart:convert';
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+import 'package:dart_firebase_admin/storage.dart';
+
+Future storageExample(FirebaseApp admin) async {
+ print('\n### Storage Example ###\n');
+
+ await basicExample(admin);
+ await getDownloadURLExample(admin);
+}
+
+Future basicExample(FirebaseApp admin) async {
+ print('> Basic Storage usage...\n');
+
+ try {
+ final storage = admin.storage();
+
+ final bucket = storage.bucket('dart-firebase-admin.firebasestorage.app');
+ print('> Using bucket: ${bucket.name}\n');
+
+ const objectName = 'foo.txt';
+ const fileContent = 'Hello from basicExample() in storage_example.dart';
+
+ print('> Uploading "$objectName" to Storage...\n');
+ await bucket.storage.insertObject(
+ bucket.name,
+ objectName,
+ utf8.encode(fileContent),
+ );
+ print('> ✓ File uploaded successfully!\n');
+
+ final metadata = await bucket.storage.objectMetadata(
+ bucket.name,
+ objectName,
+ );
+ print('> File size: ${metadata.size} bytes\n');
+ print('> Content type: ${metadata.contentType}\n');
+
+ final downloaded = await bucket.storage.downloadObject(
+ bucket.name,
+ objectName,
+ );
+ print('> Downloaded content: ${utf8.decode(downloaded)}\n');
+
+ print('> Deleting "$objectName"...\n');
+ await bucket.storage.deleteObject(bucket.name, objectName);
+ print('> ✓ File deleted successfully!\n');
+ } catch (e, stackTrace) {
+ print('> ✗ Error: $e\n');
+ print('> Stack trace: $stackTrace\n');
+ }
+}
+
+Future getDownloadURLExample(FirebaseApp admin) async {
+ print('> getDownloadURL usage...\n');
+
+ final storage = admin.storage();
+ final bucket = storage.bucket('dart-firebase-admin.firebasestorage.app');
+ const objectName = 'download-url-example.txt';
+
+ try {
+ await bucket.storage.insertObject(
+ bucket.name,
+ objectName,
+ utf8.encode('Hello from getDownloadURLExample()!'),
+ );
+ print('> ✓ File uploaded\n');
+
+ final url = await storage.getDownloadURL(bucket, objectName);
+ print('> Download URL: $url\n');
+ } on FirebaseStorageAdminException catch (e) {
+ if (e.errorCode == StorageClientErrorCode.noDownloadToken) {
+ print(
+ '> No download token available. Create one in the Firebase Console.\n',
+ );
+ } else {
+ print('> ✗ Error: $e\n');
+ }
+ } finally {
+ await bucket.storage.deleteObject(bucket.name, objectName);
+ print('> ✓ File cleaned up\n');
+ }
+}
diff --git a/packages/dart_firebase_admin/example/pubspec.yaml b/packages/dart_firebase_admin/example/pubspec.yaml
index 04c20971..ea56aadc 100644
--- a/packages/dart_firebase_admin/example/pubspec.yaml
+++ b/packages/dart_firebase_admin/example/pubspec.yaml
@@ -2,8 +2,15 @@ name: dart_firebase_admin_example
publish_to: none
environment:
- sdk: ">=3.0.0 <4.0.0"
+ sdk: '^3.9.0'
dependencies:
+ dart_firebase_admin: ^0.1.0
+ google_cloud_firestore: ^0.1.0
+ google_cloud_storage: ^0.5.1
+
+dependency_overrides:
dart_firebase_admin:
- path: ../
+ path: ../../dart_firebase_admin
+ google_cloud_firestore:
+ path: ../../google_cloud_firestore
diff --git a/packages/dart_firebase_admin/example/run_with_emulator.sh b/packages/dart_firebase_admin/example/run_with_emulator.sh
new file mode 100755
index 00000000..dbfe3e09
--- /dev/null
+++ b/packages/dart_firebase_admin/example/run_with_emulator.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+# Set environment variables for emulator
+export FIRESTORE_EMULATOR_HOST=localhost:8080
+export FIREBASE_AUTH_EMULATOR_HOST=localhost:9099
+export CLOUD_TASKS_EMULATOR_HOST=localhost:9499
+export FIREBASE_STORAGE_EMULATOR_HOST=localhost:9199
+export GOOGLE_CLOUD_PROJECT=dart-firebase-admin
+
+# Run the example
+firebase emulators:exec --only auth,firestore,functions,tasks,storage "dart run lib/main.dart"
diff --git a/packages/dart_firebase_admin/example/run_with_prod.sh b/packages/dart_firebase_admin/example/run_with_prod.sh
new file mode 100755
index 00000000..196cd868
--- /dev/null
+++ b/packages/dart_firebase_admin/example/run_with_prod.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+# Run example against Firebase production
+#
+# Authentication Options:
+#
+# Option 1: Service Account Key (used by this script)
+# 1. Download your service account key from Firebase Console:
+# - Go to Project Settings > Service Accounts
+# - Click "Generate New Private Key"
+# - Save as serviceAccountKey.json in this directory
+# 2. Set GOOGLE_APPLICATION_CREDENTIALS below (already configured)
+#
+# Option 2: Application Default Credentials (alternative)
+# 1. Run: gcloud auth application-default login
+# 2. Set GOOGLE_CLOUD_PROJECT or GCLOUD_PROJECT (uncomment below)
+# 3. Comment out GOOGLE_APPLICATION_CREDENTIALS
+#
+# For available environment variables, see:
+# ../lib/src/app/environment.dart
+
+# Service account credentials file path
+# See: Environment.googleApplicationCredentials
+export GOOGLE_APPLICATION_CREDENTIALS=service-account-key.json
+
+# (Optional) Explicit project ID - uncomment if needed
+# See: Environment.googleCloudProject
+# export GOOGLE_CLOUD_PROJECT=your-project-id
+
+# (Optional) Legacy gcloud project ID - uncomment if needed
+# See: Environment.gcloudProject
+# export GCLOUD_PROJECT=your-project-id
+
+# Run the example
+dart run lib/main.dart
diff --git a/packages/dart_firebase_admin/example_server_app/.dockerignore b/packages/dart_firebase_admin/example_server_app/.dockerignore
new file mode 100644
index 00000000..21504f8f
--- /dev/null
+++ b/packages/dart_firebase_admin/example_server_app/.dockerignore
@@ -0,0 +1,9 @@
+.dockerignore
+Dockerfile
+build/
+.dart_tool/
+.git/
+.github/
+.gitignore
+.idea/
+.packages
diff --git a/packages/dart_firebase_admin/example_server_app/.gitignore b/packages/dart_firebase_admin/example_server_app/.gitignore
new file mode 100644
index 00000000..8d82c68d
--- /dev/null
+++ b/packages/dart_firebase_admin/example_server_app/.gitignore
@@ -0,0 +1,73 @@
+android
+firebase.json
+.firebaserc
+firestore.indexes.json
+firestore.rules
+service-account-key.json
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+firebase-debug.log*
+firebase-debug.*.log*
+
+# Firebase cache
+.firebase/
+
+# Firebase config
+
+# Uncomment this if you'd like others to create their own Firebase project.
+# For a team working on the same Firebase project(s), it is recommended to leave
+# it commented so all members can deploy to the same project(s) in .firebaserc.
+# .firebaserc
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
diff --git a/packages/dart_firebase_admin/example_server_app/CHANGELOG.md b/packages/dart_firebase_admin/example_server_app/CHANGELOG.md
new file mode 100644
index 00000000..effe43c8
--- /dev/null
+++ b/packages/dart_firebase_admin/example_server_app/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 1.0.0
+
+- Initial version.
diff --git a/packages/dart_firebase_admin/example_server_app/Dockerfile b/packages/dart_firebase_admin/example_server_app/Dockerfile
new file mode 100644
index 00000000..c333dee7
--- /dev/null
+++ b/packages/dart_firebase_admin/example_server_app/Dockerfile
@@ -0,0 +1,21 @@
+# Use latest stable channel SDK.
+FROM dart:stable AS build
+
+# Resolve app dependencies.
+WORKDIR /app
+COPY pubspec.* ./
+RUN dart pub get
+
+# Copy app source code (except anything in .dockerignore) and AOT compile app.
+COPY . .
+RUN dart compile exe bin/server.dart -o bin/server
+
+# Build minimal serving image from AOT-compiled `/server`
+# and the pre-built AOT-runtime in the `/runtime/` directory of the base image.
+FROM scratch
+COPY --from=build /runtime/ /
+COPY --from=build /app/bin/server /app/bin/
+
+# Start server.
+EXPOSE 8080
+CMD ["/app/bin/server"]
diff --git a/packages/dart_firebase_admin/example_server_app/README.md b/packages/dart_firebase_admin/example_server_app/README.md
new file mode 100644
index 00000000..92df4c1e
--- /dev/null
+++ b/packages/dart_firebase_admin/example_server_app/README.md
@@ -0,0 +1,111 @@
+# # Firebase Admin SDK Cloud Run Example
+
+A simple Dart server demonstrating Firebase Admin SDK features deployed to Cloud Run.
+
+## Prerequisites
+
+- Google Cloud Project with billing enabled
+- Firebase project linked to your GCP project
+- gcloud CLI installed
+- Docker installed (for local testing)
+
+## Local Development
+
+1. Set up Application Default Credentials:
+
+ ```bash
+ gcloud auth application-default login
+ ```
+
+2. Install dependencies:
+
+ ```bash
+ dart pub get
+ ```
+
+3. Run locally:
+ ```bash
+ dart run bin/server.dart
+ ```
+
+4. Test endpoints:
+
+# Health check
+
+curl http://localhost:8080/health
+
+# Send message
+
+curl -X POST http://localhost:8080/send-message \
+-H "Content-Type: application/json" \
+-d '{"token":"DEVICE_TOKEN","title":"Hello","body":"World"}'
+
+# Subscribe to topic
+
+curl -X POST http://localhost:8080/subscribe-topic \
+-H "Content-Type: application/json" \
+-d '{"tokens":["TOKEN1","TOKEN2"],"topic":"news"}'
+
+**Deploy to Cloud Run**
+
+1. Set your project ID:
+ export PROJECT_ID="your-project-id"
+ gcloud config set project $PROJECT_ID
+
+2. Build and push container:
+ gcloud builds submit --tag gcr.io/$PROJECT_ID/firebase-admin-server
+
+3. Deploy to Cloud Run:
+ gcloud run deploy firebase-admin-server \
+ --image gcr.io/$PROJECT_ID/firebase-admin-server \
+ --platform managed \
+ --region us-central1 \
+ --allow-unauthenticated
+
+4. Get the service URL:
+ gcloud run services describe firebase-admin-server \
+ --platform managed \
+ --region us-central1 \
+ --format 'value(status.url)'
+
+**API Endpoints**
+
+**GET /health**
+
+Health check endpoint.
+
+**POST /send-message**
+
+Send an FCM message to a device token.
+
+**Body:**
+{
+"token": "DEVICE_TOKEN",
+"title": "Notification Title",
+"body": "Notification body"
+}
+
+**POST /subscribe-topic**
+
+Subscribe device tokens to a topic.
+
+**Body:**
+{
+"tokens": ["TOKEN1", "TOKEN2"],
+"topic": "news"
+}
+
+**POST /verify-token**
+
+Verify a Firebase ID token.
+
+**Body:**
+{
+"idToken": "FIREBASE_ID_TOKEN"
+}
+
+**Notes**
+
+- Cloud Run automatically injects Application Default Credentials
+- The service will scale to zero when not in use
+- Each request gets a fresh instance if needed
diff --git a/packages/dart_firebase_admin/example_server_app/analysis_options.yaml b/packages/dart_firebase_admin/example_server_app/analysis_options.yaml
new file mode 100644
index 00000000..dee8927a
--- /dev/null
+++ b/packages/dart_firebase_admin/example_server_app/analysis_options.yaml
@@ -0,0 +1,30 @@
+# This file configures the static analysis results for your project (errors,
+# warnings, and lints).
+#
+# This enables the 'recommended' set of lints from `package:lints`.
+# This set helps identify many issues that may lead to problems when running
+# or consuming Dart code, and enforces writing Dart using a single, idiomatic
+# style and format.
+#
+# If you want a smaller set of lints you can change this to specify
+# 'package:lints/core.yaml'. These are just the most critical lints
+# (the recommended set includes the core lints).
+# The core lints are also what is used by pub.dev for scoring packages.
+
+include: package:lints/recommended.yaml
+
+# Uncomment the following section to specify additional rules.
+
+# linter:
+# rules:
+# - camel_case_types
+
+# analyzer:
+# exclude:
+# - path/to/excluded/files/**
+
+# For more information about the core and recommended set of lints, see
+# https://dart.dev/go/core-lints
+
+# For additional information about configuring this file, see
+# https://dart.dev/guides/language/analysis-options
diff --git a/packages/dart_firebase_admin/example_server_app/bin/server.dart b/packages/dart_firebase_admin/example_server_app/bin/server.dart
new file mode 100644
index 00000000..739a3e29
--- /dev/null
+++ b/packages/dart_firebase_admin/example_server_app/bin/server.dart
@@ -0,0 +1,151 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+import 'package:dart_firebase_admin/messaging.dart';
+import 'package:shelf/shelf.dart';
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:shelf_router/shelf_router.dart';
+
+void main() async {
+ // Get port from environment (Cloud Run sets PORT)
+ final port = int.parse(Platform.environment['PORT'] ?? '8080');
+
+ // Initialize Firebase Admin SDK
+ final app = FirebaseApp.initializeApp();
+
+ print('Firebase Admin SDK initialized');
+
+ final router = Router()
+ ..get('/health', healthHandler)
+ ..post('/send-message', (Request req) => sendMessageHandler(req, app))
+ ..post(
+ '/subscribe-topic',
+ (Request req) => subscribeToTopicHandler(req, app),
+ )
+ ..post('/verify-token', (Request req) => verifyTokenHandler(req, app));
+
+ final handler = Pipeline()
+ .addMiddleware(logRequests())
+ .addHandler((request) => router.call(request));
+
+ final server = await shelf_io.serve(handler, '0.0.0.0', port);
+ print('Server running on port ${server.port}');
+}
+
+Response healthHandler(Request req) => Response.ok(
+ jsonEncode({
+ 'status': 'healthy',
+ 'timestamp': DateTime.now().toIso8601String(),
+ }),
+);
+
+/// Send FCM message
+Future sendMessageHandler(Request request, FirebaseApp app) async {
+ try {
+ final body =
+ jsonDecode(await request.readAsString()) as Map;
+ final token = body['token'] as String?;
+ final title = body['title'] as String?;
+ final bodyText = body['body'] as String?;
+
+ if (token == null || title == null || bodyText == null) {
+ return Response.badRequest(
+ body: jsonEncode({
+ 'error': 'Missing required fields: token, title, body',
+ }),
+ headers: {'Content-Type': 'application/json'},
+ );
+ }
+
+ final messageId = await app.messaging.send(
+ TokenMessage(
+ token: token,
+ notification: Notification(title: title, body: bodyText),
+ ),
+ );
+
+ return Response.ok(
+ jsonEncode({'success': true, 'messageId': messageId}),
+ headers: {'Content-Type': 'application/json'},
+ );
+ } catch (e) {
+ return Response.internalServerError(
+ body: jsonEncode({'error': e.toString()}),
+ headers: {'Content-Type': 'application/json'},
+ );
+ }
+}
+
+/// Subscribe tokens to topic
+Future subscribeToTopicHandler(
+ Request request,
+ FirebaseApp app,
+) async {
+ try {
+ final body =
+ jsonDecode(await request.readAsString()) as Map;
+ final tokens = (body['tokens'] as List?)?.cast();
+ final topic = body['topic'] as String?;
+
+ if (tokens == null || tokens.isEmpty || topic == null) {
+ return Response.badRequest(
+ body: jsonEncode({
+ 'error': 'Missing required fields: tokens (array), topic',
+ }),
+ headers: {'Content-Type': 'application/json'},
+ );
+ }
+
+ final response = await app.messaging.subscribeToTopic(tokens, topic);
+
+ return Response.ok(
+ jsonEncode({
+ 'success': true,
+ 'successCount': response.successCount,
+ 'failureCount': response.failureCount,
+ 'errors': response.errors
+ .map((e) => {'index': e.index, 'error': e.error.message})
+ .toList(),
+ }),
+ headers: {'Content-Type': 'application/json'},
+ );
+ } catch (e) {
+ return Response.internalServerError(
+ body: jsonEncode({'error': e.toString()}),
+ headers: {'Content-Type': 'application/json'},
+ );
+ }
+}
+
+/// Verify Firebase ID token
+Future verifyTokenHandler(Request request, FirebaseApp app) async {
+ try {
+ final body =
+ jsonDecode(await request.readAsString()) as Map;
+ final idToken = body['idToken'] as String?;
+
+ if (idToken == null) {
+ return Response.badRequest(
+ body: jsonEncode({'error': 'Missing required field: idToken'}),
+ headers: {'Content-Type': 'application/json'},
+ );
+ }
+
+ final decodedToken = await app.auth.verifyIdToken(idToken);
+
+ return Response.ok(
+ jsonEncode({
+ 'success': true,
+ 'uid': decodedToken.uid,
+ 'email': decodedToken.email,
+ }),
+ headers: {'Content-Type': 'application/json'},
+ );
+ } catch (e) {
+ return Response.unauthorized(
+ jsonEncode({'error': e.toString()}),
+ headers: {'Content-Type': 'application/json'},
+ );
+ }
+}
diff --git a/packages/dart_firebase_admin/example_server_app/pubspec.yaml b/packages/dart_firebase_admin/example_server_app/pubspec.yaml
new file mode 100644
index 00000000..77bb9815
--- /dev/null
+++ b/packages/dart_firebase_admin/example_server_app/pubspec.yaml
@@ -0,0 +1,19 @@
+name: dart_firebase_admin_example_server_app
+publish_to: none
+
+environment:
+ sdk: '>=3.9.0 <4.0.0'
+
+dependencies:
+ dart_firebase_admin:
+ git:
+ url: https://github.com/invertase/dart_firebase_admin.git
+ path: packages/dart_firebase_admin
+ ref: next
+ shelf: ^1.4.2
+ shelf_router: ^1.1.2
+
+dev_dependencies:
+ http: ^1.2.2
+ lints: ^6.0.0
+ test: ^1.25.6
diff --git a/packages/dart_firebase_admin/example_server_app/test/server_test.dart b/packages/dart_firebase_admin/example_server_app/test/server_test.dart
new file mode 100644
index 00000000..3081d874
--- /dev/null
+++ b/packages/dart_firebase_admin/example_server_app/test/server_test.dart
@@ -0,0 +1,39 @@
+import 'dart:io';
+
+import 'package:http/http.dart';
+import 'package:test/test.dart';
+
+void main() {
+ final port = '8080';
+ final host = 'http://0.0.0.0:$port';
+ late Process p;
+
+ setUp(() async {
+ p = await Process.start(
+ 'dart',
+ ['run', 'bin/server.dart'],
+ environment: {'PORT': port},
+ );
+ // Wait for server to start and print to stdout.
+ await p.stdout.first;
+ });
+
+ tearDown(() => p.kill());
+
+ test('Root', () async {
+ final response = await get(Uri.parse('$host/'));
+ expect(response.statusCode, 200);
+ expect(response.body, 'Hello, World!\n');
+ });
+
+ test('Echo', () async {
+ final response = await get(Uri.parse('$host/echo/hello'));
+ expect(response.statusCode, 200);
+ expect(response.body, 'hello\n');
+ });
+
+ test('404', () async {
+ final response = await get(Uri.parse('$host/foobar'));
+ expect(response.statusCode, 404);
+ });
+}
diff --git a/packages/dart_firebase_admin/firebase.json b/packages/dart_firebase_admin/firebase.json
new file mode 100644
index 00000000..a1effde8
--- /dev/null
+++ b/packages/dart_firebase_admin/firebase.json
@@ -0,0 +1,38 @@
+{
+ "emulators": {
+ "auth": {
+ "port": 9099
+ },
+ "firestore": {
+ "port": 8080
+ },
+ "functions": {
+ "port": 5001
+ },
+ "tasks": {
+ "port": 9499
+ },
+ "ui": {
+ "enabled": true
+ },
+ "singleProjectMode": true,
+ "storage": {
+ "port": 9199
+ }
+ },
+ "functions": [
+ {
+ "source": "test/functions",
+ "codebase": "default",
+ "ignore": [
+ "node_modules",
+ ".git",
+ "firebase-debug.log",
+ "*.local"
+ ]
+ }
+ ],
+ "storage": {
+ "rules": "storage.rules"
+ }
+}
diff --git a/packages/dart_firebase_admin/lib/app_check.dart b/packages/dart_firebase_admin/lib/app_check.dart
index a22e092c..8c671d5b 100644
--- a/packages/dart_firebase_admin/lib/app_check.dart
+++ b/packages/dart_firebase_admin/lib/app_check.dart
@@ -1,2 +1,3 @@
-export 'src/app_check/app_check.dart';
+export 'src/app_check/app_check.dart'
+ hide AppCheckRequestHandler, AppCheckHttpClient;
export 'src/app_check/app_check_api.dart';
diff --git a/packages/dart_firebase_admin/lib/auth.dart b/packages/dart_firebase_admin/lib/auth.dart
index 2ba9ad83..cde0930e 100644
--- a/packages/dart_firebase_admin/lib/auth.dart
+++ b/packages/dart_firebase_admin/lib/auth.dart
@@ -1 +1 @@
-export 'src/auth.dart' hide UserMetadataToJson;
+export 'src/auth.dart' hide AuthRequestHandler, AuthHttpClient;
diff --git a/packages/dart_firebase_admin/lib/dart_firebase_admin.dart b/packages/dart_firebase_admin/lib/dart_firebase_admin.dart
index db463153..fe7bfa47 100644
--- a/packages/dart_firebase_admin/lib/dart_firebase_admin.dart
+++ b/packages/dart_firebase_admin/lib/dart_firebase_admin.dart
@@ -1 +1,12 @@
-export 'src/app.dart' hide envSymbol;
+export 'src/app.dart'
+ hide
+ envSymbol,
+ ApplicationDefaultCredential,
+ ServiceAccountCredential,
+ AppRegistry,
+ EmulatorClient,
+ Environment,
+ FirebaseUserAgentClient,
+ FirebaseServiceType,
+ FirebaseService,
+ CloudTasksEmulatorClient;
diff --git a/packages/dart_firebase_admin/lib/firestore.dart b/packages/dart_firebase_admin/lib/firestore.dart
index 7be1b6f3..fb1455ae 100644
--- a/packages/dart_firebase_admin/lib/firestore.dart
+++ b/packages/dart_firebase_admin/lib/firestore.dart
@@ -1,2 +1 @@
-export 'src/google_cloud_firestore/firestore.dart'
- hide $SettingsCopyWith, ApiMapValue;
+export 'src/firestore/firestore.dart';
diff --git a/packages/dart_firebase_admin/lib/functions.dart b/packages/dart_firebase_admin/lib/functions.dart
new file mode 100644
index 00000000..ff072ec6
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/functions.dart
@@ -0,0 +1 @@
+export 'src/functions/functions.dart';
diff --git a/packages/dart_firebase_admin/lib/messaging.dart b/packages/dart_firebase_admin/lib/messaging.dart
index e2d7947d..b4284baa 100644
--- a/packages/dart_firebase_admin/lib/messaging.dart
+++ b/packages/dart_firebase_admin/lib/messaging.dart
@@ -1 +1,2 @@
-export 'src/messaging.dart' hide FirebaseMessagingRequestHandler;
+export 'src/messaging/messaging.dart'
+ hide FirebaseMessagingRequestHandler, FirebaseMessagingHttpClient;
diff --git a/packages/dart_firebase_admin/lib/security_rules.dart b/packages/dart_firebase_admin/lib/security_rules.dart
index c6318dd4..3ba780b8 100644
--- a/packages/dart_firebase_admin/lib/security_rules.dart
+++ b/packages/dart_firebase_admin/lib/security_rules.dart
@@ -1,4 +1,2 @@
-export 'src/security_rules/security_rules.dart';
-export 'src/security_rules/security_rules_api_internals.dart'
- hide SecurityRulesApiClient;
-export 'src/security_rules/security_rules_internals.dart';
+export 'src/security_rules/security_rules.dart'
+ hide SecurityRulesRequestHandler, SecurityRulesHttpClient;
diff --git a/packages/dart_firebase_admin/lib/src/app.dart b/packages/dart_firebase_admin/lib/src/app.dart
index 87c4d58a..83e63fc5 100644
--- a/packages/dart_firebase_admin/lib/src/app.dart
+++ b/packages/dart_firebase_admin/lib/src/app.dart
@@ -3,13 +3,35 @@ library app;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
+import 'dart:typed_data';
+import 'package:equatable/equatable.dart';
+import 'package:google_cloud/constants.dart' as google_cloud;
+import 'package:google_cloud/google_cloud.dart' as google_cloud;
+import 'package:google_cloud_firestore/google_cloud_firestore.dart'
+ as google_cloud_firestore;
import 'package:googleapis/identitytoolkit/v3.dart' as auth3;
-import 'package:googleapis_auth/auth_io.dart' as auth;
-import 'package:googleapis_auth/googleapis_auth.dart';
+import 'package:googleapis_auth/auth_io.dart' as googleapis_auth;
import 'package:http/http.dart';
import 'package:meta/meta.dart';
+import '../app_check.dart';
+import '../auth.dart';
+import '../firestore.dart';
+import '../functions.dart';
+import '../messaging.dart';
+import '../security_rules.dart';
+import '../storage.dart';
+import '../version.g.dart';
+import 'utils/utils.dart';
+
+part 'app/app_exception.dart';
+part 'app/app_options.dart';
+part 'app/app_registry.dart';
part 'app/credential.dart';
+part 'app/emulator_client.dart';
+part 'app/environment.dart';
part 'app/exception.dart';
-part 'app/firebase_admin.dart';
+part 'app/firebase_app.dart';
+part 'app/firebase_service.dart';
+part 'app/firebase_user_agent_client.dart';
diff --git a/packages/dart_firebase_admin/lib/src/app/app_exception.dart b/packages/dart_firebase_admin/lib/src/app/app_exception.dart
new file mode 100644
index 00000000..57634aa2
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app/app_exception.dart
@@ -0,0 +1,90 @@
+part of '../app.dart';
+
+/// Exception thrown for Firebase app initialization and lifecycle errors.
+class FirebaseAppException extends FirebaseAdminException {
+ FirebaseAppException(this.errorCode, [String? message])
+ : super('app', errorCode.code, message ?? errorCode.message);
+
+ /// The error code object containing code and default message.
+ final AppErrorCode errorCode;
+
+ @override
+ String toString() => 'FirebaseAppException($code): $message';
+}
+
+/// Firebase App error codes with their default messages.
+enum AppErrorCode {
+ /// Firebase app with the given name has already been deleted.
+ appDeleted(
+ code: 'app-deleted',
+ message: 'The specified Firebase app has already been deleted.',
+ ),
+
+ /// Firebase app with the same name already exists.
+ duplicateApp(
+ code: 'duplicate-app',
+ message: 'A Firebase app with the given name already exists.',
+ ),
+
+ /// Invalid argument provided to a Firebase App method.
+ invalidArgument(
+ code: 'invalid-argument',
+ message: 'Invalid argument provided.',
+ ),
+
+ /// An internal error occurred within the Firebase SDK.
+ internalError(
+ code: 'internal-error',
+ message: 'An internal error has occurred.',
+ ),
+
+ /// Invalid Firebase app name provided.
+ invalidAppName(
+ code: 'invalid-app-name',
+ message: 'Invalid Firebase app name provided.',
+ ),
+
+ /// Invalid app options provided to initializeApp().
+ invalidAppOptions(
+ code: 'invalid-app-options',
+ message: 'Invalid app options provided to initializeApp().',
+ ),
+
+ /// Invalid credential configuration.
+ invalidCredential(
+ code: 'invalid-credential',
+ message: 'The credential configuration is invalid.',
+ ),
+
+ /// Network error occurred during the operation.
+ networkError(code: 'network-error', message: 'A network error has occurred.'),
+
+ /// Network timeout occurred during the operation.
+ networkTimeout(
+ code: 'network-timeout',
+ message: 'The network request timed out.',
+ ),
+
+ /// No Firebase app exists with the given name.
+ noApp(code: 'no-app', message: 'No Firebase app exists with the given name.'),
+
+ /// Operation failed because a precondition was not met.
+ failedPrecondition(
+ code: 'failed-precondition',
+ message: 'The operation failed because a precondition was not met.',
+ ),
+
+ /// Unable to parse the server response.
+ unableToParseResponse(
+ code: 'unable-to-parse-response',
+ message: 'Unable to parse the response from the server.',
+ );
+
+ const AppErrorCode({required this.code, required this.message});
+
+ /// The error code string identifier.
+ final String code;
+
+ /// The default error message for this error code.
+ final String message;
+}
diff --git a/packages/dart_firebase_admin/lib/src/app/app_options.dart b/packages/dart_firebase_admin/lib/src/app/app_options.dart
new file mode 100644
index 00000000..c3e19bd7
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app/app_options.dart
@@ -0,0 +1,133 @@
+part of '../app.dart';
+
+/// Configuration options for initializing a Firebase app.
+///
+/// Only [credential] is required. All other fields are optional and will be
+/// auto-discovered or use defaults when not provided.
+class AppOptions extends Equatable {
+ const AppOptions({
+ this.credential,
+ this.projectId,
+ this.databaseURL,
+ this.storageBucket,
+ this.serviceAccountId,
+ this.httpClient,
+ this.databaseAuthVariableOverride,
+ });
+
+ /// A credential used to authenticate the Admin SDK.
+ ///
+ /// Use one of:
+ /// - [Credential.fromServiceAccount] - For service account JSON files
+ /// - [Credential.fromServiceAccountParams] - For individual service account parameters
+ /// - [Credential.fromApplicationDefaultCredentials] - For Application Default Credentials (ADC)
+ final Credential? credential;
+
+ /// The Firebase project ID.
+ ///
+ /// If not provided, will be auto-discovered from:
+ /// 1. The credential (service account JSON contains project_id)
+ /// 2. GOOGLE_CLOUD_PROJECT environment variable
+ /// 3. GCLOUD_PROJECT environment variable
+ /// 4. GCE metadata server (when running on Google Cloud)
+ final String? projectId;
+
+ /// The Realtime Database URL.
+ ///
+ /// Format: https://project-id.firebaseio.com
+ ///
+ /// Required only if using Realtime Database and the URL cannot be inferred
+ /// from the project ID.
+ final String? databaseURL;
+
+ /// The Cloud Storage bucket name.
+ ///
+ /// Format: project-id.appspot.com (without gs:// prefix)
+ ///
+ /// Required only if using Cloud Storage and the bucket name differs from
+ /// the default project bucket.
+ final String? storageBucket;
+
+ /// The service account email to use for operations requiring it.
+ ///
+ /// Format: firebase-adminsdk@project-id.iam.gserviceaccount.com
+ ///
+ /// If not provided, will be auto-discovered from the credential.
+ final String? serviceAccountId;
+
+ /// Custom HTTP client to use for REST API calls.
+ ///
+ /// This client is used by all services that make REST calls (Auth, Messaging,
+ /// App Check, Security Rules, etc.).
+ ///
+ /// Firestore uses gRPC and does not use this HTTP client.
+ ///
+ /// If not provided, a default client will be created automatically.
+ ///
+ /// Useful for:
+ /// - Testing: Inject mock HTTP clients
+ /// - Proxies: Configure proxy settings
+ /// - Custom timeouts: Set per-request timeouts
+ /// - Connection pooling: Control connection behavior
+ /// - Request/response logging
+ ///
+ /// Example:
+ /// ```dart
+ /// import 'package:googleapis_auth/auth_io.dart' as auth;
+ ///
+ /// final customClient = await auth.clientViaApplicationDefaultCredentials();
+ /// final app = FirebaseAdminApp.initializeApp(
+ /// AppOptions(
+ /// credential: credential,
+ /// httpClient: customClient,
+ /// ),
+ /// );
+ /// ```
+ final googleapis_auth.AuthClient? httpClient;
+
+ /// The object to use as the auth variable in Realtime Database Rules.
+ ///
+ /// This allows you to downscope the Admin SDK from its default full read
+ /// and write privileges.
+ ///
+ /// - Pass a Map to act as a specific user: `{'uid': 'user123', 'role': 'admin'}`
+ /// - Pass `null` to act as an unauthenticated client
+ /// - Omit this field to use default admin privileges
+ ///
+ /// See: https://firebase.google.com/docs/database/admin/start#authenticate-with-limited-privileges
+ ///
+ /// Example:
+ /// ```dart
+ /// // Act as a specific user
+ /// final app = FirebaseAdminApp.initializeApp(
+ /// AppOptions(
+ /// credential: credential,
+ /// databaseAuthVariableOverride: {
+ /// 'uid': 'user123',
+ /// 'email': 'user@example.com',
+ /// 'customClaims': {'role': 'admin'},
+ /// },
+ /// ),
+ /// );
+ ///
+ /// // Act as unauthenticated
+ /// final unauthApp = FirebaseAdminApp.initializeApp(
+ /// AppOptions(
+ /// credential: credential,
+ /// databaseAuthVariableOverride: null,
+ /// ),
+ /// );
+ /// ```
+ final Map? databaseAuthVariableOverride;
+
+ @override
+ List get props => [
+ // Exclude credential and httpClient from comparison
+ // (they're instances that can't be meaningfully compared)
+ projectId,
+ databaseURL,
+ storageBucket,
+ serviceAccountId,
+ databaseAuthVariableOverride,
+ ];
+}
diff --git a/packages/dart_firebase_admin/lib/src/app/app_registry.dart b/packages/dart_firebase_admin/lib/src/app/app_registry.dart
new file mode 100644
index 00000000..3db9cd43
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app/app_registry.dart
@@ -0,0 +1,200 @@
+part of '../app.dart';
+
+@internal
+class AppRegistry {
+ AppRegistry._();
+
+ /// Returns the shared default instance, creating it on first access.
+ factory AppRegistry.getDefault() {
+ return _instance ??= AppRegistry._();
+ }
+
+ static AppRegistry? _instance;
+
+ /// The current instance, if one exists.
+ static AppRegistry? get instance => _instance;
+
+ /// Replaces the singleton instance. Use for testing.
+ @visibleForTesting
+ static set instance(AppRegistry? registry) {
+ _instance = registry;
+ }
+
+ static const _defaultAppName = '[DEFAULT]';
+
+ final Map _apps = {};
+
+ /// Initializes a new Firebase app or returns an existing one.
+ ///
+ /// Creates a new app with the given [options] and [name], or returns an
+ /// existing app if one with the same name already exists with matching
+ /// configuration.
+ ///
+ /// If [options] is null, the app will be initialized from the
+ /// FIREBASE_CONFIG environment variable.
+ ///
+ /// [name] defaults to `[DEFAULT]` if not provided.
+ ///
+ /// Throws `FirebaseAppException` if:
+ /// - An app with the same name exists but with different configuration
+ /// - An app with the same name exists but was initialized differently
+ /// (one from env, one explicitly)
+ FirebaseApp initializeApp({AppOptions? options, String? name}) {
+ name ??= _defaultAppName;
+ _validateAppName(name);
+
+ var wasInitializedFromEnv = false;
+ final effectiveOptions = options ?? fetchOptionsFromEnvironment();
+
+ if (options == null) {
+ wasInitializedFromEnv = true;
+ }
+
+ // App doesn't exist - create it
+ if (!_apps.containsKey(name)) {
+ final app = FirebaseApp(
+ options: effectiveOptions,
+ name: name,
+ wasInitializedFromEnv: wasInitializedFromEnv,
+ );
+ _apps[name] = app;
+ return app;
+ }
+
+ // App exists
+ final existingApp = _apps[name]!;
+
+ // Check initialization mode matches
+ if (existingApp.wasInitializedFromEnv != wasInitializedFromEnv) {
+ throw FirebaseAppException(
+ AppErrorCode.invalidAppOptions,
+ 'Firebase app named "$name" already exists with different configuration.',
+ );
+ }
+
+ // Both from env: return existing app (skip comparison)
+ if (wasInitializedFromEnv) {
+ return existingApp;
+ }
+
+ // Check if options match existing app (using Equatable)
+ if (options != existingApp.options) {
+ throw FirebaseAppException(
+ AppErrorCode.duplicateApp,
+ 'Firebase app named "$name" already exists with different configuration.',
+ );
+ }
+
+ return existingApp;
+ }
+
+ /// Loads app options from the FIREBASE_CONFIG environment variable.
+ ///
+ /// If the variable contains a string starting with '{', it's parsed as JSON.
+ /// Otherwise, it's treated as a file path to read.
+ ///
+ /// Returns empty AppOptions if FIREBASE_CONFIG is not set.
+ AppOptions fetchOptionsFromEnvironment() {
+ final env =
+ Zone.current[envSymbol] as Map? ?? Platform.environment;
+
+ final config = env['FIREBASE_CONFIG'];
+ if (config == null || config.isEmpty) {
+ return AppOptions(credential: _credentialFromEnv(env));
+ }
+
+ try {
+ final String contents;
+ if (config.startsWith('{')) {
+ // Parse as JSON directly
+ contents = config;
+ } else {
+ // Treat as file path
+ contents = File(config).readAsStringSync();
+ }
+
+ final json = jsonDecode(contents) as Map;
+
+ return AppOptions(
+ credential: _credentialFromEnv(env),
+ projectId: json['projectId'] as String?,
+ databaseURL: json['databaseURL'] as String?,
+ storageBucket: json['storageBucket'] as String?,
+ serviceAccountId: json['serviceAccountId'] as String?,
+ );
+ } catch (error) {
+ throw FirebaseAppException(
+ AppErrorCode.invalidArgument,
+ 'Failed to parse FIREBASE_CONFIG: $error',
+ );
+ }
+ }
+
+ /// Gets an existing app by name.
+ ///
+ /// Returns the app with the given [name], or the default app if [name]
+ /// is null or not provided.
+ ///
+ /// Throws [FirebaseAppException] if no app exists with the given name.
+ FirebaseApp getApp([String? name]) {
+ name ??= _defaultAppName;
+ _validateAppName(name);
+
+ if (!_apps.containsKey(name)) {
+ final errorMessage = name == _defaultAppName
+ ? 'The default Firebase app does not exist. '
+ : 'Firebase app named "$name" does not exist. ';
+ throw FirebaseAppException(
+ AppErrorCode.noApp,
+ '${errorMessage}Make sure you call initializeApp() before using any of the Firebase services.',
+ );
+ }
+
+ return _apps[name]!;
+ }
+
+ /// Returns a list of all initialized apps.
+ List get apps {
+ return List.unmodifiable(_apps.values);
+ }
+
+ /// Deletes the specified app and cleans up its resources.
+ ///
+ /// This calls [FirebaseApp.close] on the app, which will also remove it
+ /// from the registry.
+ Future deleteApp(FirebaseApp app) async {
+ final existingApp = getApp(app.name);
+ await existingApp.close();
+ }
+
+ /// Removes an app from the registry.
+ ///
+ /// This is called internally by [FirebaseApp.close] to remove the app
+ /// from the registry after cleanup.
+ void removeApp(String name) {
+ _apps.remove(name);
+ }
+
+ /// Validates that an app name is a non-empty string.
+ void _validateAppName(String name) {
+ if (name.isEmpty) {
+ throw FirebaseAppException(
+ AppErrorCode.invalidAppName,
+ 'Invalid Firebase app name "$name" provided. App name must be a non-empty string.',
+ );
+ }
+ }
+
+ Credential _credentialFromEnv(Map env) {
+ final googleCredPath = env['GOOGLE_APPLICATION_CREDENTIALS'];
+ if (googleCredPath != null) {
+ try {
+ return Credential.fromServiceAccount(File(googleCredPath));
+ } catch (_) {
+ // File missing, not a service account JSON, or missing project_id –
+ // fall through to ADC.
+ }
+ }
+ return Credential.fromApplicationDefaultCredentials();
+ }
+}
diff --git a/packages/dart_firebase_admin/lib/src/app/credential.dart b/packages/dart_firebase_admin/lib/src/app/credential.dart
index 29e25530..202db812 100644
--- a/packages/dart_firebase_admin/lib/src/app/credential.dart
+++ b/packages/dart_firebase_admin/lib/src/app/credential.dart
@@ -3,114 +3,186 @@ part of '../app.dart';
@internal
const envSymbol = #_envSymbol;
-class _RequestImpl extends BaseRequest {
- _RequestImpl(super.method, super.url, [Stream>? stream])
- : _stream = stream ?? const Stream.empty();
+/// Base class for Firebase Admin SDK credentials.
+///
+/// Create credentials using one of the factory methods:
+/// - [Credential.fromServiceAccount] - For service account JSON files
+/// - [Credential.fromServiceAccountParams] - For individual service account parameters
+/// - [Credential.fromApplicationDefaultCredentials] - For Application Default Credentials (ADC)
+///
+/// The credential is used to authenticate all API calls made by the Admin SDK.
+sealed class Credential {
+ /// Creates a credential using Application Default Credentials (ADC).
+ ///
+ /// ADC attempts to find credentials in the following order:
+ /// 1. [Environment.googleApplicationCredentials] environment variable (path to service account JSON)
+ /// 2. Compute Engine default service account (when running on GCE)
+ /// 3. Other ADC sources
+ ///
+ /// [serviceAccountId] can optionally be provided to override the service
+ /// account email if needed for specific operations.
+ factory Credential.fromApplicationDefaultCredentials({
+ String? serviceAccountId,
+ }) {
+ return ApplicationDefaultCredential._(serviceAccountId: serviceAccountId);
+ }
- final Stream> _stream;
+ /// Creates a credential from a service account JSON file.
+ ///
+ /// The service account file must contain:
+ /// - `project_id`: The Google Cloud project ID
+ /// - `private_key`: The service account private key
+ /// - `client_email`: The service account email
+ ///
+ /// You can download service account JSON files from the Firebase Console
+ /// under Project Settings > Service Accounts.
+ ///
+ /// Example:
+ /// ```dart
+ /// final credential = Credential.fromServiceAccount(
+ /// File('path/to/service-account.json'),
+ /// );
+ /// ```
+ factory Credential.fromServiceAccount(File serviceAccountFile) {
+ try {
+ final json = serviceAccountFile.readAsStringSync();
+ final credentials = googleapis_auth.ServiceAccountCredentials.fromJson(
+ json,
+ );
+ return ServiceAccountCredential._(credentials);
+ } catch (e) {
+ throw FirebaseAppException(
+ AppErrorCode.invalidCredential,
+ 'Failed to parse service account JSON: $e',
+ );
+ }
+ }
- @override
- ByteStream finalize() {
- super.finalize();
- return ByteStream(_stream);
+ /// Creates a credential from individual service account parameters.
+ ///
+ /// Parameters:
+ /// - [clientId]: The OAuth2 client ID (optional)
+ /// - [privateKey]: The private key in PEM format
+ /// - [email]: The service account email address
+ /// - [projectId]: The Google Cloud project ID
+ ///
+ /// Example:
+ /// ```dart
+ /// final credential = Credential.fromServiceAccountParams(
+ /// clientId: 'client-id',
+ /// privateKey: '-----BEGIN PRIVATE KEY-----\n...',
+ /// email: 'client@example.iam.gserviceaccount.com',
+ /// projectId: 'my-project',
+ /// );
+ /// ```
+ factory Credential.fromServiceAccountParams({
+ String? clientId,
+ required String privateKey,
+ required String email,
+ required String projectId,
+ }) {
+ try {
+ final json = {
+ 'type': 'service_account',
+ 'project_id': projectId,
+ 'private_key': privateKey,
+ 'client_email': email,
+ 'client_id': clientId ?? '',
+ };
+ final credentials = googleapis_auth.ServiceAccountCredentials.fromJson(
+ json,
+ );
+ return ServiceAccountCredential._(credentials);
+ } catch (e) {
+ throw FirebaseAppException(
+ AppErrorCode.invalidCredential,
+ 'Failed to create service account credentials: $e',
+ );
+ }
}
-}
-/// Will close the underlying `http.Client` depending on a constructor argument.
-class _EmulatorClient extends BaseClient {
- _EmulatorClient(this.client);
+ /// Private constructor for sealed class.
+ Credential._();
- final Client client;
+ /// Returns the underlying [googleapis_auth.ServiceAccountCredentials] if this is a
+ /// [ServiceAccountCredential], null otherwise.
+ @internal
+ googleapis_auth.ServiceAccountCredentials? get serviceAccountCredentials;
- @override
- Future send(BaseRequest request) async {
- // Make new request object and perform the authenticated request.
- final modifiedRequest = _RequestImpl(
- request.method,
- request.url,
- request.finalize(),
- );
- modifiedRequest.headers.addAll(request.headers);
- modifiedRequest.headers['Authorization'] = 'Bearer owner';
-
- return client.send(modifiedRequest);
- }
+ /// Returns the service account ID (email) if available.
+ @internal
+ String? get serviceAccountId;
+}
- @override
- void close() {
- client.close();
- super.close();
+/// Service account credentials for Firebase Admin SDK.
+///
+/// Holds [googleapis_auth.ServiceAccountCredentials] and ensures
+/// the [projectId] field is present, which is required for Firebase Admin SDK operations.
+@internal
+final class ServiceAccountCredential extends Credential {
+ ServiceAccountCredential._(this._serviceAccountCredentials) : super._() {
+ // Firebase requires projectId
+ if (_serviceAccountCredentials.projectId == null) {
+ throw FirebaseAppException(
+ AppErrorCode.invalidCredential,
+ 'Service account JSON must contain a "project_id" property',
+ );
+ }
}
-}
-/// Authentication information for Firebase Admin SDK.
-class Credential {
- Credential._(
- this.serviceAccountCredentials, {
- this.serviceAccountId,
- }) : assert(
- serviceAccountId == null || serviceAccountCredentials == null,
- 'Cannot specify both serviceAccountId and serviceAccountCredentials',
- );
-
- /// Log in to firebase from a service account file.
- factory Credential.fromServiceAccount(File serviceAccountFile) {
- final content = serviceAccountFile.readAsStringSync();
+ final googleapis_auth.ServiceAccountCredentials _serviceAccountCredentials;
- final json = jsonDecode(content);
- if (json is! Map) {
- throw const FormatException('Invalid service account file');
- }
+ /// The Google Cloud project ID associated with this service account.
+ ///
+ /// This is extracted from the `project_id` field in the service account JSON.
+ String get projectId => _serviceAccountCredentials.projectId!;
- final serviceAccountCredentials =
- auth.ServiceAccountCredentials.fromJson(json);
+ /// The service account email address.
+ ///
+ /// This is the `client_email` field from the service account JSON.
+ /// Format: `firebase-adminsdk-xxxxx@project-id.iam.gserviceaccount.com`
+ String get clientEmail => _serviceAccountCredentials.email;
- return Credential._(serviceAccountCredentials);
- }
+ /// The service account private key in PEM format.
+ ///
+ /// This is used to sign authentication tokens for API calls.
+ String get privateKey => _serviceAccountCredentials.privateKey;
- /// Log in to firebase from a service account file parameters.
- factory Credential.fromServiceAccountParams({
- required String clientId,
- required String privateKey,
- required String email,
- }) {
- final serviceAccountCredentials = auth.ServiceAccountCredentials(
- email,
- ClientId(clientId),
- privateKey,
- );
+ @override
+ googleapis_auth.ServiceAccountCredentials? get serviceAccountCredentials =>
+ _serviceAccountCredentials;
- return Credential._(serviceAccountCredentials);
- }
+ @override
+ String? get serviceAccountId => _serviceAccountCredentials.email;
+}
- /// Log in to firebase using the environment variable.
- factory Credential.fromApplicationDefaultCredentials({
- String? serviceAccountId,
- }) {
- ServiceAccountCredentials? creds;
-
- final env =
- Zone.current[envSymbol] as Map? ?? Platform.environment;
- final maybeConfig = env['GOOGLE_APPLICATION_CREDENTIALS'];
- if (maybeConfig != null && File(maybeConfig).existsSync()) {
- try {
- final text = File(maybeConfig).readAsStringSync();
- final decodedValue = jsonDecode(text);
- if (decodedValue is Map) {
- creds = ServiceAccountCredentials.fromJson(decodedValue);
- }
- } on FormatException catch (_) {}
- }
+/// Application Default Credentials for Firebase Admin SDK.
+///
+/// Uses Google Application Default Credentials (ADC) to automatically discover
+/// credentials from the environment. ADC checks the following sources in order:
+///
+/// 1. [Environment.googleApplicationCredentials] environment variable pointing to a
+/// service account JSON file
+/// 2. **Compute Engine** default service account (when running on GCE, Cloud Run, etc.)
+/// 3. Other ADC sources (gcloud CLI credentials, etc.)
+///
+/// This credential type is recommended for production environments as it allows
+/// the same code to work across different deployment environments without
+/// hardcoding credential paths.
+///
+/// The project ID is discovered from [google_cloud.computeProjectId].
+@internal
+final class ApplicationDefaultCredential extends Credential {
+ ApplicationDefaultCredential._({String? serviceAccountId})
+ : _serviceAccountId = serviceAccountId,
+ super._();
- return Credential._(
- creds,
- serviceAccountId: serviceAccountId,
- );
- }
+ final String? _serviceAccountId;
- @internal
- final String? serviceAccountId;
+ @override
+ googleapis_auth.ServiceAccountCredentials? get serviceAccountCredentials =>
+ null;
- @internal
- final auth.ServiceAccountCredentials? serviceAccountCredentials;
+ @override
+ String? get serviceAccountId => _serviceAccountId;
}
diff --git a/packages/dart_firebase_admin/lib/src/app/emulator_client.dart b/packages/dart_firebase_admin/lib/src/app/emulator_client.dart
new file mode 100644
index 00000000..ddf9ff21
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app/emulator_client.dart
@@ -0,0 +1,176 @@
+part of '../app.dart';
+
+/// Internal HTTP request implementation that wraps a stream.
+///
+/// This is used by [EmulatorClient] to create modified requests with
+/// updated headers while preserving the request body stream.
+class _RequestImpl extends BaseRequest {
+ _RequestImpl(super.method, super.url, [Stream>? stream])
+ : _stream = stream ?? const Stream.empty();
+
+ final Stream> _stream;
+
+ @override
+ ByteStream finalize() {
+ super.finalize();
+ return ByteStream(_stream);
+ }
+}
+
+/// HTTP client wrapper that adds Firebase emulator authentication.
+///
+/// This client wraps another HTTP client and automatically adds the
+/// `Authorization: Bearer owner` header to all requests, which is required
+/// when communicating with Firebase emulators (Auth, Firestore, etc.).
+///
+/// Firebase emulators expect this specific bearer token to grant full
+/// admin privileges for local development and testing.
+@internal
+class EmulatorClient extends BaseClient implements googleapis_auth.AuthClient {
+ EmulatorClient(this.client);
+
+ final Client client;
+
+ @override
+ googleapis_auth.AccessCredentials get credentials =>
+ throw UnimplementedError();
+
+ @override
+ Future send(BaseRequest request) async {
+ final modifiedRequest = _RequestImpl(
+ request.method,
+ request.url,
+ request.finalize(),
+ );
+ modifiedRequest.headers.addAll(request.headers);
+ modifiedRequest.headers['Authorization'] = 'Bearer owner';
+
+ return client.send(modifiedRequest);
+ }
+
+ @override
+ void close() => client.close();
+}
+
+/// HTTP client for Cloud Tasks emulator that rewrites URLs.
+///
+/// The googleapis CloudTasksApi uses `/v2/` prefix in its API paths, but the
+/// Firebase Cloud Tasks emulator expects paths without this prefix:
+/// - googleapis sends: `http://host:port/v2/projects/{projectId}/...`
+/// - emulator expects: `http://host:port/projects/{projectId}/...`
+///
+/// This client intercepts requests and removes the `/v2/` prefix from the path.
+@internal
+class CloudTasksEmulatorClient implements googleapis_auth.AuthClient {
+ CloudTasksEmulatorClient(this._emulatorHost)
+ : _innerClient = EmulatorClient(Client());
+
+ final String _emulatorHost;
+ final EmulatorClient _innerClient;
+
+ @override
+ googleapis_auth.AccessCredentials get credentials =>
+ throw UnimplementedError();
+
+ /// Rewrites the URL to remove `/v2/` prefix and route to emulator host.
+ Uri _rewriteUrl(Uri url) {
+ // Replace the path: remove /v2/ prefix if present
+ var path = url.path;
+ if (path.startsWith('/v2/')) {
+ path = path.substring(3); // Remove '/v2' (keep the trailing /)
+ }
+
+ // Route to emulator host
+ return Uri.parse(
+ 'http://$_emulatorHost$path${url.hasQuery ? '?${url.query}' : ''}',
+ );
+ }
+
+ @override
+ Future send(BaseRequest request) async {
+ final rewrittenUrl = _rewriteUrl(request.url);
+
+ final modifiedRequest = _RequestImpl(
+ request.method,
+ rewrittenUrl,
+ request.finalize(),
+ );
+ modifiedRequest.headers.addAll(request.headers);
+ modifiedRequest.headers['Authorization'] = 'Bearer owner';
+
+ return _innerClient.client.send(modifiedRequest);
+ }
+
+ @override
+ void close() {
+ _innerClient.close();
+ }
+
+ @override
+ Future head(Uri url, {Map? headers}) =>
+ _innerClient.head(_rewriteUrl(url), headers: headers);
+
+ @override
+ Future get(Uri url, {Map? headers}) =>
+ _innerClient.get(_rewriteUrl(url), headers: headers);
+
+ @override
+ Future post(
+ Uri url, {
+ Map? headers,
+ Object? body,
+ Encoding? encoding,
+ }) => _innerClient.post(
+ _rewriteUrl(url),
+ headers: headers,
+ body: body,
+ encoding: encoding,
+ );
+
+ @override
+ Future put(
+ Uri url, {
+ Map? headers,
+ Object? body,
+ Encoding? encoding,
+ }) => _innerClient.put(
+ _rewriteUrl(url),
+ headers: headers,
+ body: body,
+ encoding: encoding,
+ );
+
+ @override
+ Future patch(
+ Uri url, {
+ Map? headers,
+ Object? body,
+ Encoding? encoding,
+ }) => _innerClient.patch(
+ _rewriteUrl(url),
+ headers: headers,
+ body: body,
+ encoding: encoding,
+ );
+
+ @override
+ Future delete(
+ Uri url, {
+ Map? headers,
+ Object? body,
+ Encoding? encoding,
+ }) => _innerClient.delete(
+ _rewriteUrl(url),
+ headers: headers,
+ body: body,
+ encoding: encoding,
+ );
+
+ @override
+ Future read(Uri url, {Map? headers}) =>
+ _innerClient.read(_rewriteUrl(url), headers: headers);
+
+ @override
+ Future readBytes(Uri url, {Map? headers}) =>
+ _innerClient.readBytes(_rewriteUrl(url), headers: headers);
+}
diff --git a/packages/dart_firebase_admin/lib/src/app/environment.dart b/packages/dart_firebase_admin/lib/src/app/environment.dart
new file mode 100644
index 00000000..3af900ab
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app/environment.dart
@@ -0,0 +1,163 @@
+part of '../app.dart';
+
+/// Environment variable names used by the Firebase Admin SDK.
+///
+/// These constants provide type-safe access to environment variables
+/// that configure SDK behavior, credentials, and emulator connections.
+@internal
+abstract class Environment {
+ /// Path to Google Application Credentials JSON file.
+ ///
+ /// Used by Application Default Credentials to load service account credentials.
+ /// Example: `/path/to/serviceAccountKey.json`
+ static const googleApplicationCredentials = 'GOOGLE_APPLICATION_CREDENTIALS';
+
+ /// Google Cloud project ID.
+ ///
+ /// Used to explicitly specify the project ID when not available from credentials.
+ static const googleCloudProject = 'GOOGLE_CLOUD_PROJECT';
+
+ /// Legacy Google Cloud project ID (gcloud CLI).
+ ///
+ /// Alternative to [googleCloudProject], used by gcloud CLI.
+ static const gcloudProject = 'GCLOUD_PROJECT';
+
+ /// Firebase Auth Emulator host address.
+ ///
+ /// When set, Auth service automatically connects to the emulator instead of production.
+ /// Format: `host:port` (e.g., `localhost:9099`)
+ static const firebaseAuthEmulatorHost = 'FIREBASE_AUTH_EMULATOR_HOST';
+
+ /// Firestore Emulator host address.
+ ///
+ /// When set, Firestore service automatically connects to the emulator instead of production.
+ /// Format: `host:port` (e.g., `localhost:8080`)
+ static const firestoreEmulatorHost = 'FIRESTORE_EMULATOR_HOST';
+
+ /// Cloud Tasks Emulator host address.
+ ///
+ /// When set, Functions (Cloud Tasks) service automatically connects to the emulator instead of production.
+ /// Format: `host:port` (e.g., `127.0.0.1:9499`)
+ static const cloudTasksEmulatorHost = 'CLOUD_TASKS_EMULATOR_HOST';
+
+ /// Firebase Storage Emulator host address.
+ ///
+ /// When set, Storage service automatically connects to the emulator instead of production.
+ /// Format: `host:port` (e.g., `localhost:9199`)
+ static const firebaseStorageEmulatorHost = 'FIREBASE_STORAGE_EMULATOR_HOST';
+
+ /// Checks if the Firestore emulator is enabled via environment variable.
+ ///
+ /// Returns `true` if [firestoreEmulatorHost] is set in the environment.
+ ///
+ /// Example:
+ /// ```dart
+ /// if (Environment.isFirestoreEmulatorEnabled()) {
+ /// print('Using Firestore emulator');
+ /// }
+ /// ```
+ static bool isFirestoreEmulatorEnabled() {
+ final env =
+ Zone.current[envSymbol] as Map? ?? Platform.environment;
+ return env[firestoreEmulatorHost] != null;
+ }
+
+ /// Checks if the Auth emulator is enabled via environment variable.
+ ///
+ /// Returns `true` if [firebaseAuthEmulatorHost] is set in the environment.
+ ///
+ /// Example:
+ /// ```dart
+ /// if (Environment.isAuthEmulatorEnabled()) {
+ /// print('Using Auth emulator');
+ /// }
+ /// ```
+ static bool isAuthEmulatorEnabled() {
+ final env =
+ Zone.current[envSymbol] as Map? ?? Platform.environment;
+ return env[firebaseAuthEmulatorHost] != null;
+ }
+
+ /// Checks if the Cloud Tasks emulator is enabled via environment variable.
+ ///
+ /// Returns `true` if [cloudTasksEmulatorHost] is set in the environment.
+ ///
+ /// Example:
+ /// ```dart
+ /// if (Environment.isCloudTasksEmulatorEnabled()) {
+ /// print('Using Cloud Tasks emulator');
+ /// }
+ /// ```
+ static bool isCloudTasksEmulatorEnabled() {
+ final env =
+ Zone.current[envSymbol] as Map? ?? Platform.environment;
+ return env[cloudTasksEmulatorHost] != null;
+ }
+
+ /// Checks if the Storage emulator is enabled via environment variable.
+ ///
+ /// Returns `true` if [firebaseStorageEmulatorHost] is set in the environment.
+ ///
+ /// Example:
+ /// ```dart
+ /// if (Environment.isStorageEmulatorEnabled()) {
+ /// print('Using Storage emulator');
+ /// }
+ /// ```
+ static bool isStorageEmulatorEnabled() {
+ final env =
+ Zone.current[envSymbol] as Map? ?? Platform.environment;
+ return env[firebaseStorageEmulatorHost] != null;
+ }
+
+ /// Gets the Storage emulator host from environment variables.
+ ///
+ /// Returns the host:port string if set, otherwise null.
+ ///
+ /// Example:
+ /// ```dart
+ /// final host = Environment.getStorageEmulatorHost();
+ /// if (host != null) {
+ /// print('Storage emulator at $host');
+ /// }
+ /// ```
+ static String? getStorageEmulatorHost() {
+ final env =
+ Zone.current[envSymbol] as Map? ?? Platform.environment;
+ return env[firebaseStorageEmulatorHost];
+ }
+
+ /// Gets the Auth emulator host from environment variables.
+ ///
+ /// Returns the host:port string if set, otherwise null.
+ ///
+ /// Example:
+ /// ```dart
+ /// final host = Environment.getAuthEmulatorHost();
+ /// if (host != null) {
+ /// print('Auth emulator at $host');
+ /// }
+ /// ```
+ static String? getAuthEmulatorHost() {
+ final env =
+ Zone.current[envSymbol] as Map? ?? Platform.environment;
+ return env[firebaseAuthEmulatorHost];
+ }
+
+ /// Gets the Cloud Tasks emulator host from environment variables.
+ ///
+ /// Returns the host:port string if set, otherwise null.
+ ///
+ /// Example:
+ /// ```dart
+ /// final host = Environment.getCloudTasksEmulatorHost();
+ /// if (host != null) {
+ /// print('Tasks emulator at $host');
+ /// }
+ /// ```
+ static String? getCloudTasksEmulatorHost() {
+ final env =
+ Zone.current[envSymbol] as Map? ?? Platform.environment;
+ return env[cloudTasksEmulatorHost];
+ }
+}
diff --git a/packages/dart_firebase_admin/lib/src/app/exception.dart b/packages/dart_firebase_admin/lib/src/app/exception.dart
index 907ab9d2..282bbf75 100644
--- a/packages/dart_firebase_admin/lib/src/app/exception.dart
+++ b/packages/dart_firebase_admin/lib/src/app/exception.dart
@@ -11,6 +11,16 @@ class FirebaseArrayIndexError {
/// The error object.
final FirebaseAdminException error;
+
+ /// Converts this error to a JSON-serializable map.
+ ///
+ /// This is useful for structured logging and error reporting.
+ /// The returned map contains:
+ /// - `index`: The index of the errored item
+ /// - `error`: The serialized error object (with code and message)
+ Map toJson() {
+ return {'index': index, 'error': error.toJson()};
+ }
}
/// A set of platform level error codes.
@@ -30,7 +40,7 @@ String _platformErrorCodeMessage(String code) {
case 'PERMISSION_DENIED':
return 'Client does not have sufficient permission. This can happen because the OAuth token does not have the right scopes, the client does not have permission, or the API has not been enabled for the client project.';
case 'NOT_FOUND':
- return 'Specified resource not found, or the request is rejected due to undisclosed reasons such as whitelisting.';
+ return 'Specified resource not found, or the request is rejected due to undisclosed reasons such as allow list restrictions.';
case 'CONFLICT':
return 'Concurrency conflict, such as read-modify-write conflict. Only used by a few legacy services. Most services use ABORTED or ALREADY_EXISTS instead of this. Refer to the service-specific documentation to see which one to handle in your code.';
case 'ABORTED':
@@ -78,6 +88,27 @@ abstract class FirebaseAdminException implements Exception {
/// this message should not be displayed in your application.
String get message => _message ?? _platformErrorCodeMessage(_code);
+ /// Converts this exception to a JSON-serializable map.
+ ///
+ /// This is useful for structured logging and error reporting in GCP Cloud Logging.
+ /// The returned map contains:
+ /// - `code`: The error code string (e.g., "auth/invalid-uid")
+ /// - `message`: The error message
+ ///
+ /// Example:
+ /// ```dart
+ /// try {
+ /// // ...
+ /// } catch (e) {
+ /// if (e is FirebaseAdminException) {
+ /// print(jsonEncode(e.toJson())); // Logs structured JSON
+ /// }
+ /// }
+ /// ```
+ Map toJson() {
+ return {'code': code, 'message': message};
+ }
+
@override
String toString() {
return '$runtimeType($code, $message)';
diff --git a/packages/dart_firebase_admin/lib/src/app/firebase_admin.dart b/packages/dart_firebase_admin/lib/src/app/firebase_admin.dart
deleted file mode 100644
index afc206b7..00000000
--- a/packages/dart_firebase_admin/lib/src/app/firebase_admin.dart
+++ /dev/null
@@ -1,77 +0,0 @@
-part of '../app.dart';
-
-class FirebaseAdminApp {
- FirebaseAdminApp.initializeApp(
- this.projectId,
- this.credential, {
- Client? client,
- }) : _clientOverride = client;
-
- /// The ID of the Google Cloud project associated with the app.
- final String projectId;
-
- /// The [Credential] used to authenticate the Admin SDK.
- final Credential credential;
-
- bool get isUsingEmulator => _isUsingEmulator;
- var _isUsingEmulator = false;
-
- @internal
- Uri authApiHost = Uri.https('identitytoolkit.googleapis.com', '/');
- @internal
- Uri firestoreApiHost = Uri.https('firestore.googleapis.com', '/');
- @internal
- String tasksEmulatorHost = 'https://cloudfunctions.googleapis.com/';
-
- /// Use the Firebase Emulator Suite to run the app locally.
- void useEmulator() {
- _isUsingEmulator = true;
- final env =
- Zone.current[envSymbol] as Map? ?? Platform.environment;
-
- authApiHost = Uri.http(
- env['FIREBASE_AUTH_EMULATOR_HOST'] ?? '127.0.0.1:9099',
- 'identitytoolkit.googleapis.com/',
- );
- firestoreApiHost = Uri.http(
- env['FIRESTORE_EMULATOR_HOST'] ?? '127.0.0.1:8080',
- '/',
- );
- tasksEmulatorHost = Uri.http(
- env['CLOUD_TASKS_EMULATOR_HOST'] ?? '127.0.0.1:5001',
- '/',
- ).toString();
- }
-
- @internal
- late final client = _getClient(
- [
- auth3.IdentityToolkitApi.cloudPlatformScope,
- auth3.IdentityToolkitApi.firebaseScope,
- ],
- );
- final Client? _clientOverride;
-
- Future _getClient(List scopes) async {
- if (_clientOverride != null) {
- return _clientOverride;
- }
-
- if (isUsingEmulator) {
- return _EmulatorClient(Client());
- }
-
- final serviceAccountCredentials = credential.serviceAccountCredentials;
- final client = serviceAccountCredentials == null
- ? await auth.clientViaApplicationDefaultCredentials(scopes: scopes)
- : await auth.clientViaServiceAccount(serviceAccountCredentials, scopes);
-
- return client;
- }
-
- /// Stops the app and releases any resources associated with it.
- Future close() async {
- final client = await this.client;
- client.close();
- }
-}
diff --git a/packages/dart_firebase_admin/lib/src/app/firebase_app.dart b/packages/dart_firebase_admin/lib/src/app/firebase_app.dart
new file mode 100644
index 00000000..6338c428
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app/firebase_app.dart
@@ -0,0 +1,282 @@
+part of '../app.dart';
+
+/// Represents a Firebase app instance.
+///
+/// Each app is associated with a Firebase project and has its own
+/// configuration options and services.
+class FirebaseApp {
+ FirebaseApp({
+ required this.options,
+ required this.name,
+ required this.wasInitializedFromEnv,
+ });
+
+ static final _defaultAppRegistry = AppRegistry.getDefault();
+
+ /// Initializes a Firebase app.
+ ///
+ /// Creates a new app instance or returns an existing one if already
+ /// initialized with the same configuration.
+ ///
+ /// If [options] is not provided, the app will be auto-initialized from
+ /// the FIREBASE_CONFIG environment variable.
+ ///
+ /// [name] defaults to an internal string if not specified.
+ static FirebaseApp initializeApp({AppOptions? options, String? name}) {
+ return _defaultAppRegistry.initializeApp(options: options, name: name);
+ }
+
+ /// Returns the default Firebase app instance.
+ ///
+ /// This is a convenience getter equivalent to `getApp()`.
+ ///
+ /// Throws `FirebaseAppException` if the default app has not been initialized.
+ static FirebaseApp get instance => getApp();
+
+ /// Gets an existing Firebase app by name.
+ ///
+ /// Returns the app with the given [name], or the default app if [name]
+ /// is not provided.
+ ///
+ /// Throws `FirebaseAppException` if no app exists with the given name.
+ static FirebaseApp getApp([String? name]) {
+ return _defaultAppRegistry.getApp(name);
+ }
+
+ /// Returns a list of all initialized Firebase apps.
+ static List get apps {
+ return _defaultAppRegistry.apps;
+ }
+
+ /// Deletes the specified Firebase app and cleans up its resources.
+ ///
+ /// Throws `FirebaseAppException` if the app does not exist.
+ static Future deleteApp(FirebaseApp app) {
+ return _defaultAppRegistry.deleteApp(app);
+ }
+
+ /// The name of this app.
+ ///
+ /// The default app's name is `[DEFAULT]`.
+ final String name;
+
+ /// The configuration options for this app.
+ final AppOptions options;
+
+ /// Whether this app was initialized from environment variables.
+ ///
+ /// When true, indicates the app was created via `initializeApp()` without
+ /// explicit options, loading config from environment instead.
+ final bool wasInitializedFromEnv;
+
+ /// Whether this app has been deleted.
+ bool _isDeleted = false;
+
+ /// Returns true if this app has been deleted.
+ bool get isDeleted => _isDeleted;
+
+ @override
+ String toString() =>
+ 'FirebaseApp('
+ 'name: $name, '
+ 'projectId: $projectId, '
+ 'wasInitializedFromEnv: $wasInitializedFromEnv, '
+ 'isDeleted: $_isDeleted)';
+
+ /// Map of service name to service instance for caching.
+ final Map _services = {};
+
+ /// The HTTP client for this app.
+ ///
+ /// Uses the client from options if provided, otherwise creates a default one.
+ /// Nullable to avoid triggering lazy initialization during cleanup.
+ Future? _httpClient;
+
+ Future _createDefaultClient() async {
+ // Always create an authenticated client for production services.
+ // Services with emulators (Firestore, Auth) create their own
+ // unauthenticated clients when in emulator mode to avoid ADC warnings.
+
+ // Use proper OAuth scope constants
+ final scopes = [
+ auth3.IdentityToolkitApi.cloudPlatformScope,
+ auth3.IdentityToolkitApi.firebaseScope,
+ ];
+
+ // Get credential
+ final credential = options.credential;
+
+ // Create authenticated client based on credential type
+ final client = switch (credential) {
+ Credential(:final serviceAccountCredentials?) =>
+ googleapis_auth.clientViaServiceAccount(
+ serviceAccountCredentials,
+ scopes,
+ ),
+ _ => googleapis_auth.clientViaApplicationDefaultCredentials(
+ scopes: scopes,
+ ),
+ };
+
+ return FirebaseUserAgentClient(await client);
+ }
+
+ /// Returns the HTTP client for this app.
+ /// Lazily initializes on first access.
+ @internal
+ Future get client {
+ return _httpClient ??= options.httpClient != null
+ ? Future.value(options.httpClient!)
+ : _createDefaultClient();
+ }
+
+ @internal
+ Future getProjectId({
+ String? projectIdOverride,
+ Map? environment,
+ }) async {
+ final env = environment ?? Zone.current[envSymbol] as Map?;
+ if (env != null) {
+ for (final envKey in google_cloud.projectIdEnvironmentVariableOptions) {
+ final value = env[envKey];
+ if (value != null) return value;
+ }
+ }
+
+ final explicitProjectId = projectIdOverride ?? options.projectId;
+ if (explicitProjectId != null) return explicitProjectId;
+
+ return google_cloud.computeProjectId();
+ }
+
+ /// Returns the explicitly configured project ID, if available.
+ ///
+ /// This is a simple synchronous getter that returns the project ID from
+ /// [AppOptions.projectId] if it was explicitly set. Returns null if not set.
+ ///
+ /// Services that need project ID should use their own discovery mechanism
+ /// via `ProjectIdProvider.discoverProjectId()` which handles async metadata
+ /// service lookup when explicit projectId is not available.
+ String? get projectId => options.projectId;
+
+ /// Gets or initializes a service for this app.
+ ///
+ /// Services are cached per app instance. The first call with a given [name]
+ /// will invoke [init] to create the service. Subsequent calls return the
+ /// cached instance.
+ @internal
+ T getOrInitService(
+ String name,
+ T Function(FirebaseApp) init,
+ ) {
+ _checkDestroyed();
+ if (!_services.containsKey(name)) {
+ _services[name] = init(this);
+ }
+ return _services[name]! as T;
+ }
+
+ /// Gets the App Check service instance for this app.
+ ///
+ /// Returns a cached instance if one exists, otherwise creates a new one.
+ AppCheck appCheck() => AppCheck.internal(this);
+
+ /// Gets the Auth service instance for this app.
+ ///
+ /// Returns a cached instance if one exists, otherwise creates a new one.
+ Auth auth() => Auth.internal(this);
+
+ /// Gets the Firestore service instance for this app.
+ ///
+ /// Returns a cached instance if one exists, otherwise creates a new one.
+ /// Optional [settings] are only applied when creating a new instance.
+ ///
+ /// For multi-database support, use [databaseId] to specify a named database.
+ /// Default is '(default)'.
+ google_cloud_firestore.Firestore firestore({
+ google_cloud_firestore.Settings? settings,
+ String databaseId = kDefaultDatabaseId,
+ }) {
+ final service = Firestore.internal(this);
+
+ if (settings != null) {
+ return service.initializeDatabase(databaseId, settings);
+ }
+ return service.getDatabase(databaseId);
+ }
+
+ /// Gets the Messaging service instance for this app.
+ ///
+ /// Returns a cached instance if one exists, otherwise creates a new one.
+ Messaging messaging() => Messaging.internal(this);
+
+ /// Gets the Security Rules service instance for this app.
+ ///
+ /// Returns a cached instance if one exists, otherwise creates a new one.
+ SecurityRules securityRules() => SecurityRules.internal(this);
+
+ /// Gets the Functions service instance for this app.
+ ///
+ /// Returns a cached instance if one exists, otherwise creates a new one.
+ Functions functions() => Functions.internal(this);
+
+ /// Gets the Storage service instance for this app.
+ ///
+ /// Returns a cached instance if one exists, otherwise creates a new one.
+ Storage storage() => Storage.internal(this);
+
+ /// Closes this app and cleans up all associated resources.
+ ///
+ /// This method:
+ /// 1. Removes the app from the global registry
+ /// 2. Calls [FirebaseService.delete] on all registered services
+ /// 3. Closes the HTTP client (if it was created by the SDK)
+ /// 4. Marks the app as deleted
+ ///
+ /// After calling this method, the app instance can no longer be used.
+ /// Any subsequent calls to the app or its services will throw a
+ /// `FirebaseAppException` with code 'app-deleted'.
+ ///
+ /// Note: If you provided a custom [AppOptions.httpClient], it will NOT
+ /// be closed automatically. You are responsible for closing it.
+ ///
+ /// Example:
+ /// ```dart
+ /// final app = FirebaseApp.initializeApp(options: options);
+ /// // Use app...
+ /// await app.close();
+ /// // App can no longer be used
+ /// ```
+ Future close() async {
+ _checkDestroyed();
+
+ // Remove from registry
+ _defaultAppRegistry.removeApp(name);
+
+ // Delete all services
+ await Future.wait(
+ _services.values.map((service) {
+ return service.delete();
+ }),
+ );
+
+ _services.clear();
+
+ // Only close client if it was initialized AND we created it (not user-provided)
+ if (_httpClient != null && options.httpClient == null) {
+ (await _httpClient!).close();
+ }
+
+ _isDeleted = true;
+ }
+
+ /// Checks if this app has been deleted and throws if so.
+ void _checkDestroyed() {
+ if (_isDeleted) {
+ throw FirebaseAppException(
+ AppErrorCode.appDeleted,
+ 'Firebase app "$name" has already been deleted.',
+ );
+ }
+ }
+}
diff --git a/packages/dart_firebase_admin/lib/src/app/firebase_service.dart b/packages/dart_firebase_admin/lib/src/app/firebase_service.dart
new file mode 100644
index 00000000..3b13d924
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app/firebase_service.dart
@@ -0,0 +1,66 @@
+part of '../app.dart';
+
+@internal
+enum FirebaseServiceType {
+ appCheck(name: 'app-check'),
+ auth(name: 'auth'),
+ firestore(name: 'firestore'),
+ messaging(name: 'messaging'),
+ securityRules(name: 'security-rules'),
+ functions(name: 'functions'),
+ storage(name: 'storage');
+
+ const FirebaseServiceType({required this.name});
+
+ final String name;
+}
+
+/// Base class for all Firebase services.
+///
+/// All Firebase services (Auth, Messaging, Firestore, etc.) implement this
+/// interface to enable proper lifecycle management.
+///
+/// Services are automatically registered with the [FirebaseApp] when first
+/// accessed via factory constructors. When the app is closed via
+/// [FirebaseApp.close], all registered services have their [delete] method
+/// called to clean up resources.
+///
+/// Example implementation:
+/// ```dart
+/// class MyService implements FirebaseService {
+/// factory MyService(FirebaseApp app) {
+/// return app.getOrInitService(
+/// 'my-service',
+/// (app) => MyService._(app),
+/// ) as MyService;
+/// }
+///
+/// MyService._(this.app);
+///
+/// @override
+/// final FirebaseApp app;
+///
+/// @override
+/// Future delete() async {
+/// // Cleanup logic here
+/// }
+/// }
+/// ```
+@internal
+abstract class FirebaseService {
+ FirebaseService(this.app);
+
+ /// The Firebase app this service is associated with.
+ final FirebaseApp app;
+
+ /// Cleans up resources used by this service.
+ ///
+ /// This method is called automatically when [FirebaseApp.close] is called
+ /// on the parent app. Services should override this to release any held
+ /// resources such as:
+ /// - Network connections
+ /// - File handles
+ /// - Cached data
+ /// - Subscriptions or listeners
+ Future delete();
+}
diff --git a/packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart b/packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart
new file mode 100644
index 00000000..45c9e47a
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart
@@ -0,0 +1,29 @@
+part of '../app.dart';
+
+/// HTTP client wrapper that adds Firebase and Google API client headers for usage tracking.
+///
+/// Wraps another HTTP client and injects:
+/// - `X-Firebase-Client: fire-admin-dart/{version}`
+/// - `X-Goog-Api-Client: gl-dart/{dartVersion} fire-admin-dart/{version}`
+/// into every outgoing request so Firebase backend services can identify the SDK.
+@internal
+class FirebaseUserAgentClient extends BaseClient
+ implements googleapis_auth.AuthClient {
+ FirebaseUserAgentClient(this._client);
+
+ final googleapis_auth.AuthClient _client;
+
+ @override
+ googleapis_auth.AccessCredentials get credentials => _client.credentials;
+
+ @override
+ Future send(BaseRequest request) {
+ request.headers['X-Firebase-Client'] = 'fire-admin-dart/$packageVersion';
+ request.headers['X-Goog-Api-Client'] =
+ 'gl-dart/$dartVersion fire-admin-dart/$packageVersion';
+ return _client.send(request);
+ }
+
+ @override
+ void close() => _client.close();
+}
diff --git a/packages/dart_firebase_admin/lib/src/app_check/app_check.dart b/packages/dart_firebase_admin/lib/src/app_check/app_check.dart
index 6ed8aad3..904c2cbd 100644
--- a/packages/dart_firebase_admin/lib/src/app_check/app_check.dart
+++ b/packages/dart_firebase_admin/lib/src/app_check/app_check.dart
@@ -1,18 +1,54 @@
+import 'dart:async';
+
+import 'package:googleapis/firebaseappcheck/v1.dart' as appcheck1;
+import 'package:googleapis_auth/auth_io.dart' as googleapis_auth;
+import 'package:googleapis_beta/firebaseappcheck/v1beta.dart' as appcheck1_beta;
+import 'package:meta/meta.dart';
+
import '../app.dart';
-import '../utils/crypto_signer.dart';
+import '../utils/jwt.dart';
import 'app_check_api.dart';
-import 'app_check_api_internal.dart';
import 'token_generator.dart';
import 'token_verifier.dart';
-class AppCheck {
- AppCheck(this.app);
+part 'app_check_exception.dart';
+part 'app_check_http_client.dart';
+part 'app_check_request_handler.dart';
+
+class AppCheck implements FirebaseService {
+ /// Creates or returns the cached AppCheck instance for the given app.
+ @internal
+ factory AppCheck.internal(
+ FirebaseApp app, {
+ AppCheckRequestHandler? requestHandler,
+ AppCheckTokenGenerator? tokenGenerator,
+ AppCheckTokenVerifier? tokenVerifier,
+ }) {
+ return app.getOrInitService(
+ FirebaseServiceType.appCheck.name,
+ (app) => AppCheck._(
+ app,
+ requestHandler: requestHandler,
+ tokenGenerator: tokenGenerator,
+ tokenVerifier: tokenVerifier,
+ ),
+ );
+ }
+
+ AppCheck._(
+ this.app, {
+ AppCheckRequestHandler? requestHandler,
+ AppCheckTokenGenerator? tokenGenerator,
+ AppCheckTokenVerifier? tokenVerifier,
+ }) : _requestHandler = requestHandler ?? AppCheckRequestHandler(app),
+ _tokenGenerator = tokenGenerator ?? AppCheckTokenGenerator(app),
+ _appCheckTokenVerifier = tokenVerifier ?? AppCheckTokenVerifier(app);
- final FirebaseAdminApp app;
- late final _tokenGenerator =
- AppCheckTokenGenerator(CryptoSigner.fromApp(app));
- late final _client = AppCheckApiClient(app);
- late final _appCheckTokenVerifier = AppCheckTokenVerifier(app);
+ @override
+ final FirebaseApp app;
+ final AppCheckRequestHandler _requestHandler;
+ final AppCheckTokenGenerator _tokenGenerator;
+ final AppCheckTokenVerifier _appCheckTokenVerifier;
/// Creates a new [AppCheckToken] that can be sent
/// back to a client.
@@ -25,9 +61,16 @@ class AppCheck {
String appId, [
AppCheckTokenOptions? options,
]) async {
+ if (appId.isEmpty) {
+ throw FirebaseAppCheckException(
+ AppCheckErrorCode.invalidArgument,
+ '`appId` must be a non-empty string.',
+ );
+ }
+
final customToken = await _tokenGenerator.createCustomToken(appId, options);
- return _client.exchangeToken(customToken, appId);
+ return _requestHandler.exchangeToken(customToken, appId);
}
/// Verifies a Firebase App Check token (JWT). If the token is valid, the promise is
@@ -43,12 +86,21 @@ class AppCheck {
String appCheckToken, [
VerifyAppCheckTokenOptions? options,
]) async {
- final decodedToken =
- await _appCheckTokenVerifier.verifyToken(appCheckToken);
+ if (appCheckToken.isEmpty) {
+ throw FirebaseAppCheckException(
+ AppCheckErrorCode.invalidArgument,
+ '`appCheckToken` must be a non-empty string.',
+ );
+ }
+
+ final decodedToken = await _appCheckTokenVerifier.verifyToken(
+ appCheckToken,
+ );
if (options?.consume ?? false) {
- final alreadyConsumed =
- await _client.verifyReplayProtection(appCheckToken);
+ final alreadyConsumed = await _requestHandler.verifyReplayProtection(
+ appCheckToken,
+ );
return VerifyAppCheckTokenResponse(
alreadyConsumed: alreadyConsumed,
appId: decodedToken.appId,
@@ -62,4 +114,9 @@ class AppCheck {
token: decodedToken,
);
}
+
+ @override
+ Future delete() async {
+ // AppCheck service cleanup if needed
+ }
}
diff --git a/packages/dart_firebase_admin/lib/src/app_check/app_check_api.dart b/packages/dart_firebase_admin/lib/src/app_check/app_check_api.dart
index 62a3ec1e..9aa5ece0 100644
--- a/packages/dart_firebase_admin/lib/src/app_check/app_check_api.dart
+++ b/packages/dart_firebase_admin/lib/src/app_check/app_check_api.dart
@@ -1,7 +1,6 @@
-import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:meta/meta.dart';
import 'app_check.dart';
-import 'app_check_api_internal.dart';
class AppCheckToken {
@internal
@@ -15,9 +14,7 @@ class AppCheckToken {
}
class AppCheckTokenOptions {
- AppCheckTokenOptions({
- this.ttlMillis,
- }) {
+ AppCheckTokenOptions({this.ttlMillis}) {
if (ttlMillis case final ttlMillis?) {
if (ttlMillis.inMinutes < 30 || ttlMillis.inDays > 7) {
throw FirebaseAppCheckException(
@@ -88,14 +85,14 @@ class DecodedAppCheckToken {
});
DecodedAppCheckToken.fromMap(Map map)
- : this._(
- iss: map['iss'] as String,
- sub: map['sub'] as String,
- aud: (map['aud'] as List).cast(),
- exp: map['exp'] as int,
- iat: map['iat'] as int,
- appId: map['sub'] as String,
- );
+ : this._(
+ iss: map['iss'] as String,
+ sub: map['sub'] as String,
+ aud: (map['aud'] as List).cast(),
+ exp: map['exp'] as int,
+ iat: map['iat'] as int,
+ appId: map['sub'] as String,
+ );
/// The issuer identifier for the issuer of the response.
/// This value is a URL with the format
diff --git a/packages/dart_firebase_admin/lib/src/app_check/app_check_api_internal.dart b/packages/dart_firebase_admin/lib/src/app_check/app_check_api_internal.dart
deleted file mode 100644
index 8644db92..00000000
--- a/packages/dart_firebase_admin/lib/src/app_check/app_check_api_internal.dart
+++ /dev/null
@@ -1,162 +0,0 @@
-import 'package:googleapis/firebaseappcheck/v1.dart' as appcheck1;
-import 'package:googleapis_beta/firebaseappcheck/v1beta.dart' as appcheck1_beta;
-import 'package:meta/meta.dart';
-
-import '../app.dart';
-import '../utils/crypto_signer.dart';
-import '../utils/jwt.dart';
-import 'app_check_api.dart';
-
-/// Class that facilitates sending requests to the Firebase App Check backend API.
-@internal
-class AppCheckApiClient {
- AppCheckApiClient(this.app);
-
- final FirebaseAdminApp app;
-
- Future _v1(
- Future Function(appcheck1.FirebaseappcheckApi client) fn,
- ) async {
- return fn(appcheck1.FirebaseappcheckApi(await app.client));
- }
-
- Future _v1Beta(
- Future Function(appcheck1_beta.FirebaseappcheckApi client) fn,
- ) async {
- return fn(appcheck1_beta.FirebaseappcheckApi(await app.client));
- }
-
- /// Exchange a signed custom token to App Check token
- ///
- /// [customToken] - The custom token to be exchanged.
- /// [appId] - The mobile App ID.
- ///
- /// Returns a future that fulfills with a [AppCheckToken].
- Future exchangeToken(String customToken, String appId) {
- return _v1((client) async {
- final response = await client.projects.apps.exchangeCustomToken(
- appcheck1.GoogleFirebaseAppcheckV1ExchangeCustomTokenRequest(
- customToken: customToken,
- ),
- 'projects/${app.projectId}/apps/$appId',
- );
-
- return AppCheckToken(
- token: response.token!,
- ttlMillis: _stringToMilliseconds(response.ttl!),
- );
- });
- }
-
- Future verifyReplayProtection(String token) {
- return _v1Beta((client) async {
- final response = await client.projects.verifyAppCheckToken(
- appcheck1_beta.GoogleFirebaseAppcheckV1betaVerifyAppCheckTokenRequest(
- appCheckToken: token,
- ),
- 'projects/${app.projectId}',
- );
-
- return response.alreadyConsumed ?? false;
- });
- }
-
- /// Converts a duration string with the suffix `s` to milliseconds.
- ///
- /// [duration] - The duration as a string with the suffix "s" preceded by the
- /// number of seconds, with fractional seconds. For example, 3 seconds with 0 nanoseconds
- /// is expressed as "3s", while 3 seconds and 1 nanosecond is expressed as "3.000000001s",
- /// and 3 seconds and 1 microsecond is expressed as "3.000001s".
- ///
- /// Returns the duration in milliseconds.
- int _stringToMilliseconds(String duration) {
- if (duration.isEmpty || !duration.endsWith('s')) {
- throw FirebaseAppCheckException(
- AppCheckErrorCode.invalidArgument,
- '`ttl` must be a valid duration string with the suffix `s`.',
- );
- }
-
- final seconds = duration.substring(0, duration.length - 1);
- return (double.parse(seconds) * 1000).floor();
- }
-}
-
-final appCheckErrorCodeMapping = {
- 'ABORTED': AppCheckErrorCode.aborted,
- 'INVALID_ARGUMENT': AppCheckErrorCode.invalidArgument,
- 'INVALID_CREDENTIAL': AppCheckErrorCode.invalidCredential,
- 'INTERNAL': AppCheckErrorCode.internalError,
- 'PERMISSION_DENIED': AppCheckErrorCode.permissionDenied,
- 'UNAUTHENTICATED': AppCheckErrorCode.unauthenticated,
- 'NOT_FOUND': AppCheckErrorCode.notFound,
- 'UNKNOWN': AppCheckErrorCode.unknownError,
-};
-
-enum AppCheckErrorCode {
- aborted('aborted'),
- invalidArgument('invalid-argument'),
- invalidCredential('invalid-credential'),
- internalError('internal-error'),
- permissionDenied('permission-denied'),
- unauthenticated('unauthenticated'),
- notFound('not-found'),
- appCheckTokenExpired('app-check-token-expired'),
- unknownError('unknown-error');
-
- const AppCheckErrorCode(this.code);
-
- static AppCheckErrorCode from(String code) {
- switch (code) {
- case CryptoSignerErrorCode.invalidCredential:
- return AppCheckErrorCode.invalidCredential;
- case CryptoSignerErrorCode.invalidArgument:
- return AppCheckErrorCode.invalidArgument;
- default:
- return AppCheckErrorCode.internalError;
- }
- }
-
- final String code;
-}
-
-/// Firebase App Check error code structure. This extends PrefixedFirebaseError.
-///
-/// [code] - The error code.
-/// [message] - The error message.
-class FirebaseAppCheckException extends FirebaseAdminException {
- FirebaseAppCheckException(AppCheckErrorCode code, [String? _message])
- : super('app-check', code.code, _message);
-
- factory FirebaseAppCheckException.fromJwtException(JwtException error) {
- if (error.code == JwtErrorCode.tokenExpired) {
- const errorMessage =
- 'The provided App Check token has expired. Get a fresh App Check token'
- ' from your client app and try again.';
- return FirebaseAppCheckException(
- AppCheckErrorCode.appCheckTokenExpired,
- errorMessage,
- );
- } else if (error.code == JwtErrorCode.invalidSignature) {
- const errorMessage =
- 'The provided App Check token has invalid signature.';
- return FirebaseAppCheckException(
- AppCheckErrorCode.invalidArgument,
- errorMessage,
- );
- } else if (error.code == JwtErrorCode.noMatchingKid) {
- const errorMessage =
- 'The provided App Check token has "kid" claim which does not '
- 'correspond to a known public key. Most likely the provided App Check token '
- 'is expired, so get a fresh token from your client app and try again.';
- return FirebaseAppCheckException(
- AppCheckErrorCode.invalidArgument,
- errorMessage,
- );
- }
- return FirebaseAppCheckException(
- AppCheckErrorCode.invalidArgument,
- error.message,
- );
- }
-}
diff --git a/packages/dart_firebase_admin/lib/src/app_check/app_check_exception.dart b/packages/dart_firebase_admin/lib/src/app_check/app_check_exception.dart
new file mode 100644
index 00000000..04cfb268
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app_check/app_check_exception.dart
@@ -0,0 +1,69 @@
+part of 'app_check.dart';
+
+final appCheckErrorCodeMapping = {
+ 'ABORTED': AppCheckErrorCode.aborted,
+ 'INVALID_ARGUMENT': AppCheckErrorCode.invalidArgument,
+ 'INVALID_CREDENTIAL': AppCheckErrorCode.invalidCredential,
+ 'INTERNAL': AppCheckErrorCode.internalError,
+ 'PERMISSION_DENIED': AppCheckErrorCode.permissionDenied,
+ 'UNAUTHENTICATED': AppCheckErrorCode.unauthenticated,
+ 'NOT_FOUND': AppCheckErrorCode.notFound,
+ 'UNKNOWN': AppCheckErrorCode.unknownError,
+};
+
+enum AppCheckErrorCode {
+ aborted('aborted'),
+ invalidArgument('invalid-argument'),
+ invalidCredential('invalid-credential'),
+ internalError('internal-error'),
+ permissionDenied('permission-denied'),
+ unauthenticated('unauthenticated'),
+ notFound('not-found'),
+ appCheckTokenExpired('app-check-token-expired'),
+ unknownError('unknown-error');
+
+ const AppCheckErrorCode(this.code);
+
+ final String code;
+}
+
+/// Firebase App Check error code structure. This extends PrefixedFirebaseError.
+///
+/// [code] - The error code.
+/// [message] - The error message.
+class FirebaseAppCheckException extends FirebaseAdminException {
+ FirebaseAppCheckException(AppCheckErrorCode code, [String? _message])
+ : super(FirebaseServiceType.appCheck.name, code.code, _message);
+
+ factory FirebaseAppCheckException.fromJwtException(JwtException error) {
+ if (error.code == JwtErrorCode.tokenExpired) {
+ const errorMessage =
+ 'The provided App Check token has expired. Get a fresh App Check token'
+ ' from your client app and try again.';
+ return FirebaseAppCheckException(
+ AppCheckErrorCode.appCheckTokenExpired,
+ errorMessage,
+ );
+ } else if (error.code == JwtErrorCode.invalidSignature) {
+ const errorMessage =
+ 'The provided App Check token has invalid signature.';
+ return FirebaseAppCheckException(
+ AppCheckErrorCode.invalidArgument,
+ errorMessage,
+ );
+ } else if (error.code == JwtErrorCode.noMatchingKid) {
+ const errorMessage =
+ 'The provided App Check token has "kid" claim which does not '
+ 'correspond to a known public key. Most likely the provided App Check token '
+ 'is expired, so get a fresh token from your client app and try again.';
+ return FirebaseAppCheckException(
+ AppCheckErrorCode.invalidArgument,
+ errorMessage,
+ );
+ }
+ return FirebaseAppCheckException(
+ AppCheckErrorCode.invalidArgument,
+ error.message,
+ );
+ }
+}
diff --git a/packages/dart_firebase_admin/lib/src/app_check/app_check_http_client.dart b/packages/dart_firebase_admin/lib/src/app_check/app_check_http_client.dart
new file mode 100644
index 00000000..cfe4af1a
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app_check/app_check_http_client.dart
@@ -0,0 +1,101 @@
+part of 'app_check.dart';
+
+/// HTTP client for Firebase App Check API operations.
+///
+/// Handles HTTP client management, googleapis API client creation,
+/// path builders, and simple API operations.
+/// Does not handle emulator routing as App Check has no emulator support.
+@internal
+class AppCheckHttpClient {
+ AppCheckHttpClient(this.app);
+
+ final FirebaseApp app;
+
+ /// Builds the app resource path for App Check operations.
+ String buildAppPath(String projectId, String appId) {
+ return 'projects/$projectId/apps/$appId';
+ }
+
+ /// Builds the project resource path for App Check operations.
+ String buildProjectPath(String projectId) {
+ return 'projects/$projectId';
+ }
+
+ Future _run(
+ Future Function(googleapis_auth.AuthClient client, String projectId) fn,
+ ) async {
+ final client = await app.client;
+ final projectId = await app.getProjectId();
+ try {
+ return await fn(client, projectId);
+ } on FirebaseAppCheckException {
+ rethrow;
+ } on appcheck1.DetailedApiRequestError catch (e, stack) {
+ switch (e.jsonResponse) {
+ case {'error': {'status': final String status}}:
+ final code = appCheckErrorCodeMapping[status];
+ if (code != null) {
+ Error.throwWithStackTrace(
+ FirebaseAppCheckException(code, e.message),
+ stack,
+ );
+ }
+ }
+ Error.throwWithStackTrace(
+ FirebaseAppCheckException(
+ AppCheckErrorCode.unknownError,
+ 'Unexpected error: $e',
+ ),
+ stack,
+ );
+ }
+ }
+
+ /// Executes an App Check v1 API operation with automatic projectId injection.
+ Future v1(
+ Future Function(appcheck1.FirebaseappcheckApi api, String projectId) fn,
+ ) => _run(
+ (client, projectId) => fn(appcheck1.FirebaseappcheckApi(client), projectId),
+ );
+
+ /// Executes an App Check v1Beta API operation with automatic projectId injection.
+ Future v1Beta(
+ Future Function(appcheck1_beta.FirebaseappcheckApi api, String projectId)
+ fn,
+ ) => _run(
+ (client, projectId) =>
+ fn(appcheck1_beta.FirebaseappcheckApi(client), projectId),
+ );
+
+ /// Exchange a custom token for an App Check token (low-level API call).
+ ///
+ /// Returns the raw googleapis response without transformation.
+ Future exchangeCustomToken(
+ String customToken,
+ String appId,
+ ) {
+ return v1((api, projectId) async {
+ return api.projects.apps.exchangeCustomToken(
+ appcheck1.GoogleFirebaseAppcheckV1ExchangeCustomTokenRequest(
+ customToken: customToken,
+ ),
+ buildAppPath(projectId, appId),
+ );
+ });
+ }
+
+ /// Verify an App Check token with replay protection (low-level API call).
+ ///
+ /// Returns the raw googleapis response without transformation.
+ Future
+ verifyAppCheckToken(String token) {
+ return v1Beta((api, projectId) async {
+ return api.projects.verifyAppCheckToken(
+ appcheck1_beta.GoogleFirebaseAppcheckV1betaVerifyAppCheckTokenRequest(
+ appCheckToken: token,
+ ),
+ buildProjectPath(projectId),
+ );
+ });
+ }
+}
diff --git a/packages/dart_firebase_admin/lib/src/app_check/app_check_request_handler.dart b/packages/dart_firebase_admin/lib/src/app_check/app_check_request_handler.dart
new file mode 100644
index 00000000..695b816c
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app_check/app_check_request_handler.dart
@@ -0,0 +1,65 @@
+part of 'app_check.dart';
+
+/// Request handler for Firebase App Check API operations.
+///
+/// Handles complex business logic, request/response transformations,
+/// and validation. Delegates simple API calls to [AppCheckHttpClient].
+@internal
+class AppCheckRequestHandler {
+ AppCheckRequestHandler(FirebaseApp app)
+ : _httpClient = AppCheckHttpClient(app);
+
+ final AppCheckHttpClient _httpClient;
+
+ /// Exchange a signed custom token to App Check token.
+ ///
+ /// Delegates to HTTP client for the API call, then transforms
+ /// the response by converting TTL from duration string to milliseconds.
+ ///
+ /// [customToken] - The custom token to be exchanged.
+ /// [appId] - The mobile App ID.
+ ///
+ /// Returns a future that fulfills with a [AppCheckToken].
+ Future exchangeToken(String customToken, String appId) async {
+ final response = await _httpClient.exchangeCustomToken(customToken, appId);
+
+ return AppCheckToken(
+ token: response.token!,
+ ttlMillis: _stringToMilliseconds(response.ttl!),
+ );
+ }
+
+ /// Verify an App Check token with replay protection.
+ ///
+ /// Delegates to HTTP client for the API call, then transforms
+ /// the response by extracting the alreadyConsumed field.
+ ///
+ /// [token] - The App Check token to verify.
+ ///
+ /// Returns true if token was already consumed, false otherwise.
+ Future verifyReplayProtection(String token) async {
+ final response = await _httpClient.verifyAppCheckToken(token);
+
+ return response.alreadyConsumed ?? false;
+ }
+
+ /// Converts a duration string with the suffix `s` to milliseconds.
+ ///
+ /// [duration] - The duration as a string with the suffix "s" preceded by the
+ /// number of seconds, with fractional seconds. For example, 3 seconds with 0 nanoseconds
+ /// is expressed as "3s", while 3 seconds and 1 nanosecond is expressed as "3.000000001s",
+ /// and 3 seconds and 1 microsecond is expressed as "3.000001s".
+ ///
+ /// Returns the duration in milliseconds.
+ int _stringToMilliseconds(String duration) {
+ if (duration.isEmpty || !duration.endsWith('s')) {
+ throw FirebaseAppCheckException(
+ AppCheckErrorCode.invalidArgument,
+ '`ttl` must be a valid duration string with the suffix `s`.',
+ );
+ }
+
+ final seconds = duration.substring(0, duration.length - 1);
+ return (double.parse(seconds) * 1000).floor();
+ }
+}
diff --git a/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart b/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart
index 406017c6..dcb2906d 100644
--- a/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart
+++ b/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart
@@ -1,10 +1,12 @@
import 'dart:convert';
+import 'package:googleapis_auth/auth_io.dart' as googleapis_auth;
import 'package:meta/meta.dart';
-import '../utils/crypto_signer.dart';
+import '../../dart_firebase_admin.dart';
+import '../utils/app_extension.dart';
+import 'app_check.dart';
import 'app_check_api.dart';
-import 'app_check_api_internal.dart';
// Audience to use for Firebase App Check Custom tokens
const firebaseAppCheckAudience =
@@ -15,9 +17,9 @@ const oneMinuteInSeconds = 60;
/// Class for generating Firebase App Check tokens.
@internal
class AppCheckTokenGenerator {
- AppCheckTokenGenerator(this.signer);
+ AppCheckTokenGenerator(this.app);
- final CryptoSigner signer;
+ final FirebaseApp app;
/// Creates a new custom token that can be exchanged to an App Check token.
///
@@ -30,12 +32,9 @@ class AppCheckTokenGenerator {
AppCheckTokenOptions? options,
]) async {
try {
- final account = await signer.getAccountId();
+ final account = await app.serviceAccountEmail;
- final header = {
- 'alg': signer.algorithm,
- 'typ': 'JWT',
- };
+ final header = {'alg': 'RS256', 'typ': 'JWT'};
final iat = (DateTime.now().millisecondsSinceEpoch / 1000).floor();
final body = {
'iss': account,
@@ -48,11 +47,14 @@ class AppCheckTokenGenerator {
final token = '${_encodeSegment(header)}.${_encodeSegment(body)}';
- final signature = await signer.sign(utf8.encode(token));
+ final signature = await app.sign(utf8.encode(token));
- return '$token.${_encodeSegmentBuffer(signature)}';
- } on CryptoSignerException catch (err) {
- throw _appCheckErrorFromCryptoSignerError(err);
+ return '$token.$signature';
+ } on googleapis_auth.ServerRequestFailedException catch (err) {
+ throw FirebaseAppCheckException(
+ AppCheckErrorCode.invalidCredential,
+ err.message,
+ );
}
}
@@ -70,19 +72,3 @@ class AppCheckTokenGenerator {
return base64Encode(data).replaceAll('/', '_').replaceAll('+', '-');
}
}
-
-/// Creates a new `FirebaseAppCheckError` by extracting the error code, message and other relevant
-/// details from a `CryptoSignerError`.
-///
-/// [err] - The Error to convert into a [FirebaseAppCheckException] error
-/// Returns a Firebase App Check error that can be returned to the user.
-FirebaseAppCheckException _appCheckErrorFromCryptoSignerError(
- CryptoSignerException err,
-) {
- // TODO handle CryptoSignerException.cause
-
- return FirebaseAppCheckException(
- AppCheckErrorCode.from(err.code),
- err.message,
- );
-}
diff --git a/packages/dart_firebase_admin/lib/src/app_check/token_verifier.dart b/packages/dart_firebase_admin/lib/src/app_check/token_verifier.dart
index 768362ed..934510ce 100644
--- a/packages/dart_firebase_admin/lib/src/app_check/token_verifier.dart
+++ b/packages/dart_firebase_admin/lib/src/app_check/token_verifier.dart
@@ -1,9 +1,11 @@
+import 'dart:async';
+
import 'package:meta/meta.dart';
import '../app.dart';
import '../utils/jwt.dart';
+import 'app_check.dart';
import 'app_check_api.dart';
-import 'app_check_api_internal.dart';
const appCheckIssuer = 'https://firebaseappcheck.googleapis.com/';
const jwksUrl = 'https://firebaseappcheck.googleapis.com/v1/jwks';
@@ -14,12 +16,15 @@ const jwksUrl = 'https://firebaseappcheck.googleapis.com/v1/jwks';
class AppCheckTokenVerifier {
AppCheckTokenVerifier(this.app);
- final FirebaseAdminApp app;
- final _signatureVerifier =
- PublicKeySignatureVerifier.withJwksUrl(Uri.parse(jwksUrl));
+ final FirebaseApp app;
+
+ final _signatureVerifier = PublicKeySignatureVerifier.withJwksUrl(
+ Uri.parse(jwksUrl),
+ );
Future verifyToken(String token) async {
- final decoded = await _decodeAndVerify(token, app.projectId);
+ final projectId = await app.getProjectId();
+ final decoded = await _decodeAndVerify(token, projectId);
return DecodedAppCheckToken.fromMap(decoded.payload);
}
diff --git a/packages/dart_firebase_admin/lib/src/auth.dart b/packages/dart_firebase_admin/lib/src/auth.dart
index 806f95a0..5b25abcb 100644
--- a/packages/dart_firebase_admin/lib/src/auth.dart
+++ b/packages/dart_firebase_admin/lib/src/auth.dart
@@ -1,6 +1,6 @@
+import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
-
import 'package:collection/collection.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart' as dart_jsonwebtoken;
import 'package:googleapis/identitytoolkit/v1.dart' as auth1;
@@ -8,23 +8,30 @@ import 'package:googleapis/identitytoolkit/v1.dart' as v1;
import 'package:googleapis/identitytoolkit/v2.dart' as auth2;
import 'package:googleapis/identitytoolkit/v2.dart' as v2;
import 'package:googleapis/identitytoolkit/v3.dart' as auth3;
-import 'package:http/http.dart';
+import 'package:googleapis_auth/auth_io.dart' as googleapis_auth;
+import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'app.dart';
import 'object_utils.dart';
-import 'utils/crypto_signer.dart';
+import 'utils/app_extension.dart';
import 'utils/jwt.dart';
import 'utils/utils.dart';
import 'utils/validator.dart';
part 'auth/action_code_settings_builder.dart';
part 'auth/auth.dart';
-part 'auth/auth_api_request.dart';
part 'auth/auth_config.dart';
+part 'auth/auth_config_tenant.dart';
part 'auth/auth_exception.dart';
+part 'auth/auth_http_client.dart';
+part 'auth/auth_request_handler.dart';
part 'auth/base_auth.dart';
part 'auth/identifier.dart';
+part 'auth/project_config.dart';
+part 'auth/project_config_manager.dart';
+part 'auth/tenant.dart';
+part 'auth/tenant_manager.dart';
part 'auth/token_generator.dart';
part 'auth/token_verifier.dart';
part 'auth/user.dart';
diff --git a/packages/dart_firebase_admin/lib/src/auth/action_code_settings_builder.dart b/packages/dart_firebase_admin/lib/src/auth/action_code_settings_builder.dart
index 685d5157..93c798bb 100644
--- a/packages/dart_firebase_admin/lib/src/auth/action_code_settings_builder.dart
+++ b/packages/dart_firebase_admin/lib/src/auth/action_code_settings_builder.dart
@@ -36,7 +36,7 @@ class ActionCodeSettings {
this.handleCodeInApp,
this.iOS,
this.android,
- this.dynamicLinkDomain,
+ this.linkDomain,
});
/// Defines the link continue/state URL, which has different meanings in
@@ -70,31 +70,30 @@ class ActionCodeSettings {
/// upgrade the app.
final ActionCodeSettingsAndroid? android;
- /// Defines the dynamic link domain to use for the current link if it is to be
- /// opened using Firebase Dynamic Links, as multiple dynamic link domains can be
- /// configured per project. This field provides the ability to explicitly choose
- /// configured per project. This fields provides the ability explicitly choose
- /// one. If none is provided, the oldest domain is used by default.
- final String? dynamicLinkDomain;
+ /// Defines the link domain to use for the current link. This can be a custom
+ /// domain configured in your Firebase project or a Firebase Dynamic Link domain.
+ /// If none is provided, the oldest configured domain is used by default.
+ final String? linkDomain;
}
class _ActionCodeSettingsBuilder {
_ActionCodeSettingsBuilder(ActionCodeSettings actionCodeSettings)
- : _continueUrl = actionCodeSettings.url,
- _canHandleCodeInApp = actionCodeSettings.handleCodeInApp ?? false,
- _dynamicLinkDomain = actionCodeSettings.dynamicLinkDomain,
- _ibi = actionCodeSettings.iOS?.bundleId,
- _apn = actionCodeSettings.android?.packageName,
- _amv = actionCodeSettings.android?.minimumVersion,
- _installApp = actionCodeSettings.android?.installApp ?? false {
+ : _continueUrl = actionCodeSettings.url,
+ _canHandleCodeInApp = actionCodeSettings.handleCodeInApp ?? false,
+ _linkDomain = actionCodeSettings.linkDomain,
+ _ibi = actionCodeSettings.iOS?.bundleId,
+ _apn = actionCodeSettings.android?.packageName,
+ _amv = actionCodeSettings.android?.minimumVersion,
+ _installApp = actionCodeSettings.android?.installApp ?? false {
if (Uri.tryParse(actionCodeSettings.url) == null) {
throw FirebaseAuthAdminException(AuthClientErrorCode.invalidContinueUri);
}
- final dynamicLinkDomain = actionCodeSettings.dynamicLinkDomain;
- if (dynamicLinkDomain != null && dynamicLinkDomain.isEmpty) {
+ // Validate linkDomain if provided
+ final linkDomain = actionCodeSettings.linkDomain;
+ if (linkDomain != null && linkDomain.isEmpty) {
throw FirebaseAuthAdminException(
- AuthClientErrorCode.invalidDynamicLinkDomain,
+ AuthClientErrorCode.invalidHostingLinkDomain,
);
}
@@ -132,14 +131,14 @@ class _ActionCodeSettingsBuilder {
final bool _installApp;
final String? _ibi;
final bool _canHandleCodeInApp;
- final String? _dynamicLinkDomain;
+ final String? _linkDomain;
void buildRequest(
auth1.GoogleCloudIdentitytoolkitV1GetOobCodeRequest request,
) {
request.continueUrl = _continueUrl;
request.canHandleCodeInApp = _canHandleCodeInApp;
- request.dynamicLinkDomain = _dynamicLinkDomain;
+ request.linkDomain = _linkDomain;
request.androidPackageName = _apn;
request.androidMinimumVersion = _amv;
request.androidInstallApp = _installApp;
diff --git a/packages/dart_firebase_admin/lib/src/auth/auth.dart b/packages/dart_firebase_admin/lib/src/auth/auth.dart
index 03d3c951..5bf04800 100644
--- a/packages/dart_firebase_admin/lib/src/auth/auth.dart
+++ b/packages/dart_firebase_admin/lib/src/auth/auth.dart
@@ -2,13 +2,70 @@ part of '../auth.dart';
/// Auth service bound to the provided app.
/// An Auth instance can have multiple tenants.
-class Auth extends _BaseAuth {
- Auth(FirebaseAdminApp app)
- : super(
- app: app,
- authRequestHandler: _AuthRequestHandler(app),
- );
-
- // TODO tenantManager
- // TODO projectConfigManager
+class Auth extends _BaseAuth implements FirebaseService {
+ /// Creates or returns the cached Auth instance for the given app.
+ @internal
+ factory Auth.internal(
+ FirebaseApp app, {
+ AuthRequestHandler? requestHandler,
+ FirebaseTokenVerifier? idTokenVerifier,
+ FirebaseTokenVerifier? sessionCookieVerifier,
+ }) {
+ return app.getOrInitService(
+ FirebaseServiceType.auth.name,
+ (app) => Auth._(
+ app,
+ requestHandler: requestHandler,
+ idTokenVerifier: idTokenVerifier,
+ sessionCookieVerifier: sessionCookieVerifier,
+ ),
+ );
+ }
+
+ Auth._(
+ FirebaseApp app, {
+ AuthRequestHandler? requestHandler,
+ super.idTokenVerifier,
+ super.sessionCookieVerifier,
+ }) : super(
+ app: app,
+ authRequestHandler: requestHandler ?? AuthRequestHandler(app),
+ );
+
+ @override
+ Future delete() async {
+ // Close HTTP client if we created it (emulator mode)
+ // In production mode, we use app.client which is closed by the app
+ if (Environment.isAuthEmulatorEnabled()) {
+ try {
+ final client = await _authRequestHandler.httpClient.client;
+ client.close();
+ } catch (_) {
+ // Ignore errors if client wasn't initialized
+ }
+ }
+ }
+
+ TenantManager? _tenantManager;
+
+ /// The [TenantManager] instance associated with the current project.
+ ///
+ /// This provides tenant management capabilities for multi-tenant applications.
+ /// Multi-tenancy support requires Google Cloud's Identity Platform (GCIP).
+ /// To learn more about GCIP, including pricing and features, see the
+ /// [GCIP documentation](https://cloud.google.com/identity-platform).
+ TenantManager get tenantManager {
+ return _tenantManager ??= TenantManager._(app);
+ }
+
+ ProjectConfigManager? _projectConfigManager;
+
+ /// The [ProjectConfigManager] instance associated with the current project.
+ ///
+ /// This provides methods to get and update the project configuration,
+ /// including SMS regions, multi-factor authentication, reCAPTCHA, password policy,
+ /// email privacy, and mobile links settings.
+ ProjectConfigManager get projectConfigManager {
+ return _projectConfigManager ??= ProjectConfigManager._(app);
+ }
}
diff --git a/packages/dart_firebase_admin/lib/src/auth/auth_config.dart b/packages/dart_firebase_admin/lib/src/auth/auth_config.dart
index 96310f1b..adcf37d0 100644
--- a/packages/dart_firebase_admin/lib/src/auth/auth_config.dart
+++ b/packages/dart_firebase_admin/lib/src/auth/auth_config.dart
@@ -1,24 +1,17 @@
part of '../auth.dart';
/// The possible types for [AuthProviderConfigFilter._type].
-enum _AuthProviderConfigFilterType {
- saml,
- oidc,
-}
+enum _AuthProviderConfigFilterType { saml, oidc }
/// The filter interface used for listing provider configurations. This is used
/// when specifying how to list configured identity providers via
/// [_BaseAuth.listProviderConfigs].
class AuthProviderConfigFilter {
- AuthProviderConfigFilter.oidc({
- this.maxResults,
- this.pageToken,
- }) : _type = _AuthProviderConfigFilterType.oidc;
+ AuthProviderConfigFilter.oidc({this.maxResults, this.pageToken})
+ : _type = _AuthProviderConfigFilterType.oidc;
- AuthProviderConfigFilter.saml({
- this.maxResults,
- this.pageToken,
- }) : _type = _AuthProviderConfigFilterType.saml;
+ AuthProviderConfigFilter.saml({this.maxResults, this.pageToken})
+ : _type = _AuthProviderConfigFilterType.saml;
/// The Auth provider configuration filter. This can be either `saml` or `oidc`.
/// The former is used to look up SAML providers only, while the latter is used
@@ -222,6 +215,44 @@ class SAMLAuthProviderConfig extends AuthProviderConfig
this.enableRequestSigning,
}) : super._();
+ factory SAMLAuthProviderConfig.fromResponse(
+ v2.GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig response,
+ ) {
+ final idpConfig = response.idpConfig;
+ final idpEntityId = idpConfig?.idpEntityId;
+ final ssoURL = idpConfig?.ssoUrl;
+ final spConfig = response.spConfig;
+ final spEntityId = spConfig?.spEntityId;
+ final providerId = response.name.let(
+ SAMLAuthProviderConfig.getProviderIdFromResourceName,
+ );
+
+ if (idpConfig == null ||
+ idpEntityId == null ||
+ ssoURL == null ||
+ spConfig == null ||
+ spEntityId == null ||
+ providerId == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Invalid SAML configuration response',
+ );
+ }
+
+ return SAMLAuthProviderConfig(
+ idpEntityId: idpEntityId,
+ ssoURL: ssoURL,
+ x509Certificates: [
+ ...?idpConfig.idpCertificates?.map((c) => c.x509Certificate).nonNulls,
+ ],
+ rpEntityId: spEntityId,
+ callbackURL: spConfig.callbackUri,
+ providerId: providerId,
+ displayName: response.displayName,
+ enabled: response.enabled ?? false,
+ );
+ }
+
/// The SAML IdP entity identifier.
@override
final String idpEntityId;
@@ -259,6 +290,134 @@ class SAMLAuthProviderConfig extends AuthProviderConfig
@override
final String? issuer;
+
+ static v2.GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig?
+ _buildServerRequest(
+ _SAMLAuthProviderRequestBase options, {
+ bool ignoreMissingFields = false,
+ }) {
+ final makeRequest = options.providerId != null || ignoreMissingFields;
+ if (!makeRequest) return null;
+
+ SAMLAuthProviderConfig._validate(
+ options,
+ ignoreMissingFields: ignoreMissingFields,
+ );
+
+ return v2.GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig(
+ enabled: options.enabled,
+ displayName: options.displayName,
+ spConfig: options.callbackURL == null && options.rpEntityId == null
+ ? null
+ : v2.GoogleCloudIdentitytoolkitAdminV2SpConfig(
+ callbackUri: options.callbackURL,
+ spEntityId: options.rpEntityId,
+ ),
+ idpConfig:
+ options.idpEntityId == null &&
+ options.ssoURL == null &&
+ options.x509Certificates == null
+ ? null
+ : v2.GoogleCloudIdentitytoolkitAdminV2IdpConfig(
+ idpEntityId: options.idpEntityId,
+ ssoUrl: options.ssoURL,
+ signRequest: options.enableRequestSigning,
+ idpCertificates: options.x509Certificates
+ ?.map(
+ (c) => v2.GoogleCloudIdentitytoolkitAdminV2IdpCertificate(
+ x509Certificate: c,
+ ),
+ )
+ .toList(),
+ ),
+ );
+ }
+
+ static String? getProviderIdFromResourceName(String resourceName) {
+ // name is of form projects/project1/inboundSamlConfigs/providerId1
+ final matchProviderRes = RegExp(
+ r'\/inboundSamlConfigs\/(saml\..*)$',
+ ).firstMatch(resourceName);
+ if (matchProviderRes == null || matchProviderRes.groupCount < 1) {
+ return null;
+ }
+ return matchProviderRes[1];
+ }
+
+ static bool isProviderId(String providerId) {
+ return providerId.isNotEmpty && providerId.startsWith('saml.');
+ }
+
+ static void _validate(
+ _SAMLAuthProviderRequestBase options, {
+ required bool ignoreMissingFields,
+ }) {
+ // Required fields.
+ final providerId = options.providerId;
+ if (providerId != null && providerId.isNotEmpty) {
+ if (!providerId.startsWith('saml.')) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidProviderId,
+ '"SAMLAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "saml.".',
+ );
+ }
+ } else if (!ignoreMissingFields) {
+ // providerId is required and not provided correctly.
+ throw FirebaseAuthAdminException(
+ providerId == null
+ ? AuthClientErrorCode.missingProviderId
+ : AuthClientErrorCode.invalidProviderId,
+ '"SAMLAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "saml.".',
+ );
+ }
+
+ final idpEntityId = options.idpEntityId;
+ if (!(ignoreMissingFields && idpEntityId == null) &&
+ !(idpEntityId != null && idpEntityId.isNotEmpty)) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidConfig,
+ '"SAMLAuthProviderConfig.idpEntityId" must be a valid non-empty string.',
+ );
+ }
+
+ final ssoURL = options.ssoURL;
+ if (!(ignoreMissingFields && ssoURL == null) &&
+ Uri.tryParse(ssoURL ?? '') == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidConfig,
+ '"SAMLAuthProviderConfig.ssoURL" must be a valid URL string.',
+ );
+ }
+
+ final rpEntityId = options.rpEntityId;
+ if (!(ignoreMissingFields && rpEntityId == null) &&
+ !(rpEntityId != null && rpEntityId.isNotEmpty)) {
+ throw FirebaseAuthAdminException(
+ rpEntityId == null
+ ? AuthClientErrorCode.missingSamlRelyingPartyConfig
+ : AuthClientErrorCode.invalidConfig,
+ '"SAMLAuthProviderConfig.rpEntityId" must be a valid non-empty string.',
+ );
+ }
+
+ final callbackURL = options.callbackURL;
+ if (!(ignoreMissingFields && callbackURL == null) &&
+ (callbackURL != null && Uri.tryParse(callbackURL) == null)) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidConfig,
+ '"SAMLAuthProviderConfig.callbackURL" must be a valid URL string.',
+ );
+ }
+
+ final x509Certificates = options.x509Certificates;
+ if (!(ignoreMissingFields && x509Certificates == null) &&
+ x509Certificates == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidConfig,
+ '"SAMLAuthProviderConfig.x509Certificates" must be a valid array of X509 certificate strings.',
+ );
+ }
+ }
}
/// The [OIDC](https://openid.net/specs/openid-connect-core-1_0-final.html) Auth
@@ -276,68 +435,7 @@ class OIDCAuthProviderConfig extends AuthProviderConfig
this.responseType,
}) : super._();
- /// This is the required client ID used to confirm the audience of an OIDC
- /// provider's
- /// [ID token](https://openid.net/specs/openid-connect-core-1_0-final.html#IDToken).
- @override
- final String clientId;
-
- /// This is the required provider issuer used to match the provider issuer of
- /// the ID token and to determine the corresponding OIDC discovery document, eg.
- /// [`/.well-known/openid-configuration`](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig).
- /// This is needed for the following:
- ///
- /// To verify the provided issuer.
- /// Determine the authentication/authorization endpoint during the OAuth
- /// `id_token` authentication flow.
- /// To retrieve the public signing keys via `jwks_uri` to verify the OIDC
- /// provider's ID token's signature.
- /// To determine the claims_supported to construct the user attributes to be
- /// returned in the additional user info response.
- ///
- /// ID token validation will be performed as defined in the
- /// [spec](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation).
- @override
- final String issuer;
-
- /// The OIDC provider's client secret to enable OIDC code flow.
- @override
- final String? clientSecret;
-
- /// The OIDC provider's response object for OAuth authorization flow.
- @override
- final OAuthResponseType? responseType;
-}
-
-/// The interface representing OIDC provider's response object for OAuth
-/// authorization flow.
-/// One of the following settings is required:
-///
-/// Set code to true for the code flow.
-/// Set idToken to true for the ID token flow.
-///
-class OAuthResponseType {
- OAuthResponseType._({required this.idToken, required this.code});
-
- /// Whether ID token is returned from IdP's authorization endpoint.
- final bool? idToken;
-
- /// Whether authorization code is returned from IdP's authorization endpoint.
- final bool? code;
-}
-
-class _OIDCConfig extends OIDCAuthProviderConfig {
- _OIDCConfig({
- required super.providerId,
- required super.displayName,
- required super.enabled,
- required super.clientId,
- required super.issuer,
- required super.clientSecret,
- required super.responseType,
- });
-
- factory _OIDCConfig.fromResponse(
+ factory OIDCAuthProviderConfig.fromResponse(
v2.GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig response,
) {
final issuer = response.issuer;
@@ -350,7 +448,9 @@ class _OIDCConfig extends OIDCAuthProviderConfig {
);
}
- final providerId = _OIDCConfig.getProviderIdFromResourceName(name);
+ final providerId = OIDCAuthProviderConfig.getProviderIdFromResourceName(
+ name,
+ );
if (providerId == null) {
throw FirebaseAuthAdminException(
AuthClientErrorCode.internalError,
@@ -358,7 +458,7 @@ class _OIDCConfig extends OIDCAuthProviderConfig {
);
}
- return _OIDCConfig(
+ return OIDCAuthProviderConfig(
providerId: providerId,
displayName: response.displayName,
enabled: response.enabled ?? false,
@@ -374,7 +474,39 @@ class _OIDCConfig extends OIDCAuthProviderConfig {
);
}
- static void validate(
+ /// This is the required client ID used to confirm the audience of an OIDC
+ /// provider's
+ /// [ID token](https://openid.net/specs/openid-connect-core-1_0-final.html#IDToken).
+ @override
+ final String clientId;
+
+ /// This is the required provider issuer used to match the provider issuer of
+ /// the ID token and to determine the corresponding OIDC discovery document, eg.
+ /// [`/.well-known/openid-configuration`](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig).
+ /// This is needed for the following:
+ ///
+ /// To verify the provided issuer.
+ /// Determine the authentication/authorization endpoint during the OAuth
+ /// `id_token` authentication flow.
+ /// To retrieve the public signing keys via `jwks_uri` to verify the OIDC
+ /// provider's ID token's signature.
+ /// To determine the claims_supported to construct the user attributes to be
+ /// returned in the additional user info response.
+ ///
+ /// ID token validation will be performed as defined in the
+ /// [spec](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation).
+ @override
+ final String issuer;
+
+ /// The OIDC provider's client secret to enable OIDC code flow.
+ @override
+ final String? clientSecret;
+
+ /// The OIDC provider's response object for OAuth authorization flow.
+ @override
+ final OAuthResponseType? responseType;
+
+ static void _validate(
_OIDCAuthProviderRequestBase options, {
required bool ignoreMissingFields,
}) {
@@ -446,14 +578,18 @@ class _OIDCConfig extends OIDCAuthProviderConfig {
}
}
- static v2.GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig? buildServerRequest(
+ static v2.GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig?
+ _buildServerRequest(
_OIDCAuthProviderRequestBase options, {
bool ignoreMissingFields = false,
}) {
final makeRequest = options.providerId != null || ignoreMissingFields;
if (!makeRequest) return null;
- _OIDCConfig.validate(options, ignoreMissingFields: ignoreMissingFields);
+ OIDCAuthProviderConfig._validate(
+ options,
+ ignoreMissingFields: ignoreMissingFields,
+ );
return v2.GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig(
enabled: options.enabled,
@@ -473,9 +609,10 @@ class _OIDCConfig extends OIDCAuthProviderConfig {
/// Returns the provider ID corresponding to the resource name if available.
static String? getProviderIdFromResourceName(String resourceName) {
// name is of form projects/project1/oauthIdpConfigs/providerId1
- final matchProviderRes =
- RegExp(r'\/oauthIdpConfigs\/(oidc\..*)$').firstMatch(resourceName);
- if (matchProviderRes == null || matchProviderRes.groupCount < 2) {
+ final matchProviderRes = RegExp(
+ r'\/oauthIdpConfigs\/(oidc\..*)$',
+ ).firstMatch(resourceName);
+ if (matchProviderRes == null || matchProviderRes.groupCount < 1) {
return null;
}
return matchProviderRes[1];
@@ -486,185 +623,21 @@ class _OIDCConfig extends OIDCAuthProviderConfig {
}
}
-class _SAMLConfig extends SAMLAuthProviderConfig {
- _SAMLConfig({
- required super.idpEntityId,
- required super.ssoURL,
- required super.x509Certificates,
- required super.rpEntityId,
- required super.callbackURL,
- required super.providerId,
- required super.displayName,
- required super.enabled,
- });
-
- factory _SAMLConfig.fromResponse(
- v2.GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig response,
- ) {
- final idpConfig = response.idpConfig;
- final idpEntityId = idpConfig?.idpEntityId;
- final ssoURL = idpConfig?.ssoUrl;
- final spConfig = response.spConfig;
- final spEntityId = spConfig?.spEntityId;
- final providerId =
- response.name.let(_SAMLConfig.getProviderIdFromResourceName);
-
- if (idpConfig == null ||
- idpEntityId == null ||
- ssoURL == null ||
- spConfig == null ||
- spEntityId == null ||
- providerId == null) {
- throw FirebaseAuthAdminException(
- AuthClientErrorCode.internalError,
- 'INTERNAL ASSERT FAILED: Invalid SAML configuration response',
- );
- }
-
- return _SAMLConfig(
- idpEntityId: idpEntityId,
- ssoURL: ssoURL,
- x509Certificates: [
- ...?idpConfig.idpCertificates?.map((c) => c.x509Certificate).nonNulls,
- ],
- rpEntityId: spEntityId,
- callbackURL: spConfig.callbackUri,
- providerId: providerId,
- displayName: response.displayName,
- enabled: response.enabled ?? false,
- );
- }
-
- static v2.GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig?
- buildServerRequest(
- _SAMLAuthProviderRequestBase options, {
- bool ignoreMissingFields = false,
- }) {
- final makeRequest = options.providerId != null || ignoreMissingFields;
- if (!makeRequest) return null;
-
- _SAMLConfig.validate(options, ignoreMissingFields: ignoreMissingFields);
-
- return v2.GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig(
- enabled: options.enabled,
- displayName: options.displayName,
- spConfig: options.callbackURL == null && options.rpEntityId == null
- ? null
- : v2.GoogleCloudIdentitytoolkitAdminV2SpConfig(
- callbackUri: options.callbackURL,
- spEntityId: options.rpEntityId,
- ),
- idpConfig: options.idpEntityId == null &&
- options.ssoURL == null &&
- options.x509Certificates == null
- ? null
- : v2.GoogleCloudIdentitytoolkitAdminV2IdpConfig(
- idpEntityId: options.idpEntityId,
- ssoUrl: options.ssoURL,
- signRequest: options.enableRequestSigning,
- idpCertificates: options.x509Certificates
- ?.map(
- (c) => v2.GoogleCloudIdentitytoolkitAdminV2IdpCertificate(
- x509Certificate: c,
- ),
- )
- .toList(),
- ),
- );
- }
-
- static String? getProviderIdFromResourceName(String resourceName) {
- // name is of form projects/project1/inboundSamlConfigs/providerId1
- final matchProviderRes =
- RegExp(r'\/inboundSamlConfigs\/(saml\..*)$').firstMatch(resourceName);
- if (matchProviderRes == null || matchProviderRes.groupCount < 2) {
- return null;
- }
- return matchProviderRes[1];
- }
-
- static bool isProviderId(String providerId) {
- return providerId.isNotEmpty && providerId.startsWith('saml.');
- }
-
- static void validate(
- _SAMLAuthProviderRequestBase options, {
- required bool ignoreMissingFields,
- }) {
- // Required fields.
- final providerId = options.providerId;
- if (providerId != null && providerId.isNotEmpty) {
- if (providerId.startsWith('saml.')) {
- throw FirebaseAuthAdminException(
- AuthClientErrorCode.invalidProviderId,
- '"SAMLAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "saml.".',
- );
- }
- } else if (!ignoreMissingFields) {
- // providerId is required and not provided correctly.
- throw FirebaseAuthAdminException(
- providerId == null
- ? AuthClientErrorCode.missingProviderId
- : AuthClientErrorCode.invalidProviderId,
- '"SAMLAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "saml.".',
- );
- }
-
- final idpEntityId = options.idpEntityId;
- if (!(ignoreMissingFields && idpEntityId == null) &&
- !(idpEntityId != null && idpEntityId.isNotEmpty)) {
- throw FirebaseAuthAdminException(
- AuthClientErrorCode.invalidConfig,
- '"SAMLAuthProviderConfig.idpEntityId" must be a valid non-empty string.',
- );
- }
-
- final ssoURL = options.ssoURL;
- if (!(ignoreMissingFields && ssoURL == null) &&
- Uri.tryParse(ssoURL ?? '') == null) {
- throw FirebaseAuthAdminException(
- AuthClientErrorCode.invalidConfig,
- '"SAMLAuthProviderConfig.ssoURL" must be a valid URL string.',
- );
- }
-
- final rpEntityId = options.rpEntityId;
- if (!(ignoreMissingFields && rpEntityId == null) &&
- !(rpEntityId != null && rpEntityId.isNotEmpty)) {
- throw FirebaseAuthAdminException(
- rpEntityId != null
- ? AuthClientErrorCode.missingSamlRelyingPartyConfig
- : AuthClientErrorCode.invalidConfig,
- '"SAMLAuthProviderConfig.rpEntityId" must be a valid non-empty string.',
- );
- }
+/// The interface representing OIDC provider's response object for OAuth
+/// authorization flow.
+/// One of the following settings is required:
+///
+/// Set code to true for the code flow.
+/// Set idToken to true for the ID token flow.
+///
+class OAuthResponseType {
+ OAuthResponseType._({required this.idToken, required this.code});
- final callbackURL = options.callbackURL;
- if (!(ignoreMissingFields && callbackURL == null) &&
- (callbackURL != null && Uri.tryParse(callbackURL) == null)) {
- throw FirebaseAuthAdminException(
- AuthClientErrorCode.invalidConfig,
- '"SAMLAuthProviderConfig.callbackURL" must be a valid URL string.',
- );
- }
+ /// Whether ID token is returned from IdP's authorization endpoint.
+ final bool? idToken;
- final x509Certificates = options.x509Certificates;
- if (!(ignoreMissingFields && x509Certificates == null) &&
- x509Certificates == null) {
- throw FirebaseAuthAdminException(
- AuthClientErrorCode.invalidConfig,
- '"SAMLAuthProviderConfig.x509Certificates" must be a valid array of X509 certificate strings.',
- );
- }
- for (final cert in x509Certificates ?? const []) {
- if (cert.isEmpty) {
- throw FirebaseAuthAdminException(
- AuthClientErrorCode.invalidConfig,
- '"SAMLAuthProviderConfig.x509Certificates" must be a valid array of X509 certificate strings.',
- );
- }
- }
- }
+ /// Whether authorization code is returned from IdP's authorization endpoint.
+ final bool? code;
}
const _sentinel = _Sentinel();
@@ -701,9 +674,9 @@ class CreateRequest extends _BaseUpdateRequest {
this.multiFactor,
this.uid,
}) : assert(
- multiFactor is! MultiFactorUpdateSettings,
- 'MultiFactorUpdateSettings is not supported for create requests.',
- );
+ multiFactor is! MultiFactorUpdateSettings,
+ 'MultiFactorUpdateSettings is not supported for create requests.',
+ );
/// The user's `uid`.
final String? uid;
@@ -798,9 +771,9 @@ class _BaseUpdateRequest {
required this.password,
Object? phoneNumber = _sentinel,
Object? photoURL = _sentinel,
- }) : displayName = _Box.unwrap(displayName),
- phoneNumber = _Box.unwrap(phoneNumber),
- photoURL = _Box.unwrap(photoURL);
+ }) : displayName = _Box.unwrap(displayName),
+ phoneNumber = _Box.unwrap(phoneNumber),
+ photoURL = _Box.unwrap(photoURL);
/// Whether or not the user is disabled: `true` for disabled;
/// `false` for enabled.
@@ -890,9 +863,7 @@ class MultiFactorUpdateSettings {
/// The multi-factor related user settings for create operations.
class MultiFactorCreateSettings {
- MultiFactorCreateSettings({
- required this.enrolledFactors,
- });
+ MultiFactorCreateSettings({required this.enrolledFactors});
/// The created user's list of enrolled second factors.
final List enrolledFactors;
@@ -908,30 +879,26 @@ class CreatePhoneMultiFactorInfoRequest extends CreateMultiFactorInfoRequest {
/// The phone number associated with a phone second factor.
final String phoneNumber;
-
- @override
- v1.GoogleCloudIdentitytoolkitV1MfaFactor
- toGoogleCloudIdentitytoolkitV1MfaFactor() {
- return v1.GoogleCloudIdentitytoolkitV1MfaFactor(
- displayName: displayName,
- // TODO param is optional, but phoneNumber is required.
- phoneInfo: phoneNumber,
- );
- }
}
/// Interface representing base properties of a user-enrolled second factor for a
/// `CreateRequest`.
sealed class CreateMultiFactorInfoRequest {
- CreateMultiFactorInfoRequest({
- required this.displayName,
- });
+ CreateMultiFactorInfoRequest({required this.displayName});
/// The optional display name for an enrolled second factor.
final String? displayName;
v1.GoogleCloudIdentitytoolkitV1MfaFactor
- toGoogleCloudIdentitytoolkitV1MfaFactor();
+ toGoogleCloudIdentitytoolkitV1MfaFactor() {
+ return switch (this) {
+ CreatePhoneMultiFactorInfoRequest(:final phoneNumber) =>
+ v1.GoogleCloudIdentitytoolkitV1MfaFactor(
+ displayName: displayName,
+ phoneInfo: phoneNumber,
+ ),
+ };
+ }
}
/// Interface representing a phone specific user-enrolled second factor
@@ -977,14 +944,13 @@ sealed class UpdateMultiFactorInfoRequest {
final DateTime? enrollmentTime;
v1.GoogleCloudIdentitytoolkitV1MfaEnrollment toMfaEnrollment() {
- final that = this;
- return switch (that) {
- UpdatePhoneMultiFactorInfoRequest() =>
+ return switch (this) {
+ UpdatePhoneMultiFactorInfoRequest(:final phoneNumber) =>
v1.GoogleCloudIdentitytoolkitV1MfaEnrollment(
mfaEnrollmentId: uid,
displayName: displayName,
// Required for all phone second factors.
- phoneInfo: that.phoneNumber,
+ phoneInfo: phoneNumber,
enrolledAt: enrollmentTime?.toUtc().toIso8601String(),
),
};
diff --git a/packages/dart_firebase_admin/lib/src/auth/auth_config_tenant.dart b/packages/dart_firebase_admin/lib/src/auth/auth_config_tenant.dart
new file mode 100644
index 00000000..7f6742c1
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/auth_config_tenant.dart
@@ -0,0 +1,923 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+part of '../auth.dart';
+
+// ============================================================================
+// Email Sign-In Configuration
+// ============================================================================
+
+/// The email sign in provider configuration.
+class EmailSignInProviderConfig {
+ EmailSignInProviderConfig({required this.enabled, this.passwordRequired});
+
+ /// Whether email provider is enabled.
+ final bool enabled;
+
+ /// Whether password is required for email sign-in. When not required,
+ /// email sign-in can be performed with password or via email link sign-in.
+ final bool? passwordRequired;
+
+ Map toJson() => {
+ 'enabled': enabled,
+ if (passwordRequired != null) 'passwordRequired': passwordRequired,
+ };
+}
+
+/// Internal class for email sign-in configuration.
+class _EmailSignInConfig implements EmailSignInProviderConfig {
+ _EmailSignInConfig({required this.enabled, this.passwordRequired});
+
+ factory _EmailSignInConfig.fromServerResponse(Map response) {
+ final allowPasswordSignup = response['allowPasswordSignup'];
+ if (allowPasswordSignup == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Invalid email sign-in configuration response',
+ );
+ }
+
+ return _EmailSignInConfig(
+ enabled: allowPasswordSignup as bool,
+ passwordRequired: response['enableEmailLinkSignin'] != null
+ ? !(response['enableEmailLinkSignin'] as bool)
+ : null,
+ );
+ }
+
+ static Map buildServerRequest(
+ EmailSignInProviderConfig options,
+ ) {
+ final request = {};
+
+ request['allowPasswordSignup'] = options.enabled;
+ if (options.passwordRequired != null) {
+ request['enableEmailLinkSignin'] = !options.passwordRequired!;
+ }
+
+ return request;
+ }
+
+ @override
+ final bool enabled;
+
+ @override
+ final bool? passwordRequired;
+
+ @override
+ Map toJson() => {
+ 'enabled': enabled,
+ if (passwordRequired != null) 'passwordRequired': passwordRequired,
+ };
+}
+
+// ============================================================================
+// Multi-Factor Authentication Configuration
+// ============================================================================
+
+/// Identifies a second factor type.
+typedef AuthFactorType = String;
+
+/// The 'phone' auth factor type constant.
+const authFactorTypePhone = 'phone';
+
+/// Identifies a multi-factor configuration state.
+enum MultiFactorConfigState {
+ enabled('ENABLED'),
+ disabled('DISABLED');
+
+ const MultiFactorConfigState(this.value);
+
+ final String value;
+
+ static MultiFactorConfigState fromString(String value) {
+ return MultiFactorConfigState.values.firstWhere(
+ (e) => e.value == value,
+ orElse: () => throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ 'Invalid MultiFactorConfigState: $value',
+ ),
+ );
+ }
+}
+
+/// Interface representing configuration settings for TOTP second factor auth.
+class TotpMultiFactorProviderConfig {
+ /// Creates a new [TotpMultiFactorProviderConfig] instance.
+ TotpMultiFactorProviderConfig({this.adjacentIntervals}) {
+ final intervals = adjacentIntervals;
+ if (intervals != null && (intervals < 0 || intervals > 10)) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ '"adjacentIntervals" must be a valid number between 0 and 10 (both inclusive).',
+ );
+ }
+ }
+
+ /// The allowed number of adjacent intervals that will be used for verification
+ /// to compensate for clock skew. Valid range is 0-10 (inclusive).
+ final int? adjacentIntervals;
+
+ Map toJson() {
+ return {
+ if (adjacentIntervals != null) 'adjacentIntervals': adjacentIntervals,
+ };
+ }
+}
+
+/// Interface representing a multi-factor auth provider configuration.
+/// This interface is used for second factor auth providers other than SMS.
+/// Currently, only TOTP is supported.
+class MultiFactorProviderConfig {
+ /// Creates a new [MultiFactorProviderConfig] instance.
+ MultiFactorProviderConfig({required this.state, this.totpProviderConfig}) {
+ // Since TOTP is the only provider config available right now, it must be defined
+ if (totpProviderConfig == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidConfig,
+ '"totpProviderConfig" must be defined.',
+ );
+ }
+ }
+
+ /// Indicates whether this multi-factor provider is enabled or disabled.
+ final MultiFactorConfigState state;
+
+ /// TOTP multi-factor provider config.
+ final TotpMultiFactorProviderConfig? totpProviderConfig;
+
+ Map toJson() {
+ return {
+ 'state': state.value,
+ if (totpProviderConfig != null)
+ 'totpProviderConfig': totpProviderConfig!.toJson(),
+ };
+ }
+}
+
+/// Interface representing a multi-factor configuration.
+class MultiFactorConfig {
+ MultiFactorConfig({
+ required this.state,
+ this.factorIds,
+ this.providerConfigs,
+ });
+
+ /// The multi-factor config state.
+ final MultiFactorConfigState state;
+
+ /// The list of identifiers for enabled second factors.
+ /// Currently 'phone' and 'totp' are supported.
+ final List? factorIds;
+
+ /// The configuration for multi-factor auth providers.
+ final List? providerConfigs;
+
+ Map toJson() => {
+ 'state': state.value,
+ if (factorIds != null) 'factorIds': factorIds,
+ if (providerConfigs != null)
+ 'providerConfigs': providerConfigs!.map((e) => e.toJson()).toList(),
+ };
+}
+
+/// Internal class for multi-factor authentication configuration.
+class _MultiFactorAuthConfig implements MultiFactorConfig {
+ _MultiFactorAuthConfig({
+ required this.state,
+ this.factorIds,
+ this.providerConfigs,
+ });
+
+ factory _MultiFactorAuthConfig.fromServerResponse(
+ Map response,
+ ) {
+ final stateValue = response['state'];
+ if (stateValue == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Invalid multi-factor configuration response',
+ );
+ }
+
+ final enabledProviders = response['enabledProviders'] as List?;
+ final factorIds = [];
+
+ if (enabledProviders != null) {
+ for (final provider in enabledProviders) {
+ // Map server types to client types
+ if (provider == 'PHONE_SMS') {
+ factorIds.add(authFactorTypePhone);
+ }
+ }
+ }
+
+ // Parse provider configs
+ final providerConfigsData = response['providerConfigs'] as List?;
+ final providerConfigs = [];
+
+ if (providerConfigsData != null) {
+ for (final configData in providerConfigsData) {
+ if (configData is! Map) continue;
+
+ final configState = configData['state'] as String?;
+ if (configState == null) continue;
+
+ final totpConfigData =
+ configData['totpProviderConfig'] as Map?;
+ if (totpConfigData != null) {
+ final adjacentIntervals = totpConfigData['adjacentIntervals'] as int?;
+ providerConfigs.add(
+ MultiFactorProviderConfig(
+ state: MultiFactorConfigState.fromString(configState),
+ totpProviderConfig: TotpMultiFactorProviderConfig(
+ adjacentIntervals: adjacentIntervals,
+ ),
+ ),
+ );
+ }
+ }
+ }
+
+ return _MultiFactorAuthConfig(
+ state: MultiFactorConfigState.fromString(stateValue as String),
+ factorIds: factorIds.isEmpty ? null : factorIds,
+ providerConfigs: providerConfigs,
+ );
+ }
+
+ static Map buildServerRequest(MultiFactorConfig options) {
+ final request = {};
+
+ request['state'] = options.state.value;
+
+ if (options.factorIds != null) {
+ final enabledProviders = [];
+ for (final factorId in options.factorIds!) {
+ // Map client types to server types
+ if (factorId == authFactorTypePhone) {
+ enabledProviders.add('PHONE_SMS');
+ }
+ }
+ request['enabledProviders'] = enabledProviders;
+ }
+
+ // Build provider configs
+ if (options.providerConfigs != null) {
+ final providerConfigsData = >[];
+ for (final config in options.providerConfigs!) {
+ final configData = {'state': config.state.value};
+
+ if (config.totpProviderConfig != null) {
+ final totpData = {};
+ if (config.totpProviderConfig!.adjacentIntervals != null) {
+ totpData['adjacentIntervals'] =
+ config.totpProviderConfig!.adjacentIntervals;
+ }
+ configData['totpProviderConfig'] = totpData;
+ }
+
+ providerConfigsData.add(configData);
+ }
+ request['providerConfigs'] = providerConfigsData;
+ }
+
+ return request;
+ }
+
+ @override
+ final MultiFactorConfigState state;
+
+ @override
+ final List? factorIds;
+
+ @override
+ final List? providerConfigs;
+
+ @override
+ Map toJson() => {
+ 'state': state.value,
+ if (factorIds != null) 'factorIds': factorIds,
+ if (providerConfigs != null)
+ 'providerConfigs': providerConfigs!.map((e) => e.toJson()).toList(),
+ };
+}
+
+// ============================================================================
+// SMS Region Configuration
+// ============================================================================
+
+/// The request interface for updating a SMS Region Config.
+/// Configures the regions where users are allowed to send verification SMS.
+/// This is based on the calling code of the destination phone number.
+sealed class SmsRegionConfig {
+ const SmsRegionConfig();
+
+ Map toJson();
+}
+
+/// Defines a policy of allowing every region by default and adding disallowed
+/// regions to a disallow list.
+class AllowByDefaultSmsRegionConfig extends SmsRegionConfig {
+ const AllowByDefaultSmsRegionConfig({required this.disallowedRegions});
+
+ /// Two letter unicode region codes to disallow as defined by
+ /// https://cldr.unicode.org/
+ final List disallowedRegions;
+
+ @override
+ Map toJson() => {
+ 'allowByDefault': {'disallowedRegions': disallowedRegions},
+ };
+}
+
+/// Defines a policy of only allowing regions by explicitly adding them to an
+/// allowlist.
+class AllowlistOnlySmsRegionConfig extends SmsRegionConfig {
+ const AllowlistOnlySmsRegionConfig({required this.allowedRegions});
+
+ /// Two letter unicode region codes to allow as defined by
+ /// https://cldr.unicode.org/
+ final List allowedRegions;
+
+ @override
+ Map toJson() => {
+ 'allowlistOnly': {'allowedRegions': allowedRegions},
+ };
+}
+
+// ============================================================================
+// reCAPTCHA Configuration
+// ============================================================================
+
+/// Enforcement state of reCAPTCHA protection.
+enum RecaptchaProviderEnforcementState {
+ off('OFF'),
+ audit('AUDIT'),
+ enforce('ENFORCE');
+
+ const RecaptchaProviderEnforcementState(this.value);
+
+ final String value;
+
+ static RecaptchaProviderEnforcementState fromString(String value) {
+ return RecaptchaProviderEnforcementState.values.firstWhere(
+ (e) => e.value == value,
+ orElse: () => RecaptchaProviderEnforcementState.off,
+ );
+ }
+}
+
+/// The actions to take for reCAPTCHA-protected requests.
+enum RecaptchaAction {
+ block('BLOCK');
+
+ const RecaptchaAction(this.value);
+
+ final String value;
+
+ static RecaptchaAction fromString(String value) {
+ return RecaptchaAction.values.firstWhere(
+ (e) => e.value == value,
+ orElse: () => RecaptchaAction.block,
+ );
+ }
+}
+
+/// The key's platform type.
+enum RecaptchaKeyClientType {
+ web('WEB'),
+ ios('IOS'),
+ android('ANDROID');
+
+ const RecaptchaKeyClientType(this.value);
+
+ final String value;
+
+ static RecaptchaKeyClientType fromString(String value) {
+ return RecaptchaKeyClientType.values.firstWhere(
+ (e) => e.value == value,
+ orElse: () => RecaptchaKeyClientType.web,
+ );
+ }
+}
+
+/// The config for a reCAPTCHA action rule.
+class RecaptchaManagedRule {
+ const RecaptchaManagedRule({required this.endScore, this.action});
+
+ /// The action will be enforced if the reCAPTCHA score of a request is larger than endScore.
+ final double endScore;
+
+ /// The action for reCAPTCHA-protected requests.
+ final RecaptchaAction? action;
+
+ Map toJson() => {
+ 'endScore': endScore,
+ if (action != null) 'action': action!.value,
+ };
+}
+
+/// The managed rules for toll fraud provider, containing the enforcement status.
+/// The toll fraud provider contains all SMS related user flows.
+class RecaptchaTollFraudManagedRule {
+ const RecaptchaTollFraudManagedRule({required this.startScore, this.action});
+
+ /// The action will be enforced if the reCAPTCHA score of a request is larger than startScore.
+ final double startScore;
+
+ /// The action for reCAPTCHA-protected requests.
+ final RecaptchaAction? action;
+
+ Map toJson() => {
+ 'startScore': startScore,
+ if (action != null) 'action': action!.value,
+ };
+}
+
+/// The reCAPTCHA key config.
+class RecaptchaKey {
+ const RecaptchaKey({required this.key, this.type});
+
+ /// The reCAPTCHA site key.
+ final String key;
+
+ /// The key's client platform type.
+ final RecaptchaKeyClientType? type;
+
+ Map toJson() => {
+ 'key': key,
+ if (type != null) 'type': type!.value,
+ };
+}
+
+/// The request interface for updating a reCAPTCHA Config.
+/// By enabling reCAPTCHA Enterprise Integration you are
+/// agreeing to reCAPTCHA Enterprise
+/// [Terms of Service](https://cloud.google.com/terms/service-terms).
+class RecaptchaConfig {
+ RecaptchaConfig({
+ this.emailPasswordEnforcementState,
+ this.phoneEnforcementState,
+ this.managedRules,
+ this.recaptchaKeys,
+ this.useAccountDefender,
+ this.useSmsBotScore,
+ this.useSmsTollFraudProtection,
+ this.smsTollFraudManagedRules,
+ });
+
+ /// The enforcement state of the email password provider.
+ final RecaptchaProviderEnforcementState? emailPasswordEnforcementState;
+
+ /// The enforcement state of the phone provider.
+ final RecaptchaProviderEnforcementState? phoneEnforcementState;
+
+ /// The reCAPTCHA managed rules.
+ final List? managedRules;
+
+ /// The reCAPTCHA keys.
+ final List? recaptchaKeys;
+
+ /// Whether to use account defender for reCAPTCHA assessment.
+ final bool? useAccountDefender;
+
+ /// Whether to use the rCE bot score for reCAPTCHA phone provider.
+ /// Can only be true when the phone_enforcement_state is AUDIT or ENFORCE.
+ final bool? useSmsBotScore;
+
+ /// Whether to use the rCE SMS toll fraud protection risk score for reCAPTCHA phone provider.
+ /// Can only be true when the phone_enforcement_state is AUDIT or ENFORCE.
+ final bool? useSmsTollFraudProtection;
+
+ /// The managed rules for toll fraud provider, containing the enforcement status.
+ /// The toll fraud provider contains all SMS related user flows.
+ final List? smsTollFraudManagedRules;
+
+ Map toJson() => {
+ if (emailPasswordEnforcementState != null)
+ 'emailPasswordEnforcementState': emailPasswordEnforcementState!.value,
+ if (phoneEnforcementState != null)
+ 'phoneEnforcementState': phoneEnforcementState!.value,
+ if (managedRules != null)
+ 'managedRules': managedRules!.map((e) => e.toJson()).toList(),
+ if (recaptchaKeys != null)
+ 'recaptchaKeys': recaptchaKeys!.map((e) => e.toJson()).toList(),
+ if (useAccountDefender != null) 'useAccountDefender': useAccountDefender,
+ if (useSmsBotScore != null) 'useSmsBotScore': useSmsBotScore,
+ if (useSmsTollFraudProtection != null)
+ 'useSmsTollFraudProtection': useSmsTollFraudProtection,
+ if (smsTollFraudManagedRules != null)
+ 'smsTollFraudManagedRules': smsTollFraudManagedRules!
+ .map((e) => e.toJson())
+ .toList(),
+ };
+}
+
+/// Internal class for reCAPTCHA authentication configuration.
+class _RecaptchaAuthConfig implements RecaptchaConfig {
+ _RecaptchaAuthConfig({
+ this.emailPasswordEnforcementState,
+ this.phoneEnforcementState,
+ this.managedRules,
+ this.recaptchaKeys,
+ this.useAccountDefender,
+ this.useSmsBotScore,
+ this.useSmsTollFraudProtection,
+ this.smsTollFraudManagedRules,
+ });
+
+ factory _RecaptchaAuthConfig.fromServerResponse(
+ Map response,
+ ) {
+ List? managedRules;
+ if (response['managedRules'] != null) {
+ final rulesList = response['managedRules'] as List;
+ managedRules = rulesList.map((rule) {
+ final ruleMap = rule as Map;
+ return RecaptchaManagedRule(
+ endScore: (ruleMap['endScore'] as num).toDouble(),
+ action: ruleMap['action'] != null
+ ? RecaptchaAction.fromString(ruleMap['action'] as String)
+ : null,
+ );
+ }).toList();
+ }
+
+ List? recaptchaKeys;
+ if (response['recaptchaKeys'] != null) {
+ final keysList = response['recaptchaKeys'] as List;
+ recaptchaKeys = keysList.map((key) {
+ final keyMap = key as Map;
+ return RecaptchaKey(
+ key: keyMap['key'] as String,
+ type: keyMap['type'] != null
+ ? RecaptchaKeyClientType.fromString(keyMap['type'] as String)
+ : null,
+ );
+ }).toList();
+ }
+
+ List? smsTollFraudManagedRules;
+ // Server response uses 'tollFraudManagedRules' but client uses 'smsTollFraudManagedRules'
+ final tollFraudRules =
+ response['tollFraudManagedRules'] ??
+ response['smsTollFraudManagedRules'];
+ if (tollFraudRules != null) {
+ final rulesList = tollFraudRules as List;
+ smsTollFraudManagedRules = rulesList.map((rule) {
+ final ruleMap = rule as Map;
+ return RecaptchaTollFraudManagedRule(
+ startScore: (ruleMap['startScore'] as num).toDouble(),
+ action: ruleMap['action'] != null
+ ? RecaptchaAction.fromString(ruleMap['action'] as String)
+ : null,
+ );
+ }).toList();
+ }
+
+ return _RecaptchaAuthConfig(
+ emailPasswordEnforcementState:
+ response['emailPasswordEnforcementState'] != null
+ ? RecaptchaProviderEnforcementState.fromString(
+ response['emailPasswordEnforcementState'] as String,
+ )
+ : null,
+ phoneEnforcementState: response['phoneEnforcementState'] != null
+ ? RecaptchaProviderEnforcementState.fromString(
+ response['phoneEnforcementState'] as String,
+ )
+ : null,
+ managedRules: managedRules,
+ recaptchaKeys: recaptchaKeys,
+ useAccountDefender: response['useAccountDefender'] as bool?,
+ useSmsBotScore: response['useSmsBotScore'] as bool?,
+ useSmsTollFraudProtection: response['useSmsTollFraudProtection'] as bool?,
+ smsTollFraudManagedRules: smsTollFraudManagedRules,
+ );
+ }
+
+ static Map buildServerRequest(RecaptchaConfig options) {
+ _validate(options);
+
+ final request = {};
+
+ if (options.emailPasswordEnforcementState != null) {
+ request['emailPasswordEnforcementState'] =
+ options.emailPasswordEnforcementState!.value;
+ }
+ if (options.phoneEnforcementState != null) {
+ request['phoneEnforcementState'] = options.phoneEnforcementState!.value;
+ }
+ if (options.managedRules != null) {
+ request['managedRules'] = options.managedRules!
+ .map((e) => e.toJson())
+ .toList();
+ }
+ if (options.recaptchaKeys != null) {
+ request['recaptchaKeys'] = options.recaptchaKeys!
+ .map((e) => e.toJson())
+ .toList();
+ }
+ if (options.useAccountDefender != null) {
+ request['useAccountDefender'] = options.useAccountDefender;
+ }
+ if (options.useSmsBotScore != null) {
+ request['useSmsBotScore'] = options.useSmsBotScore;
+ }
+ if (options.useSmsTollFraudProtection != null) {
+ request['useSmsTollFraudProtection'] = options.useSmsTollFraudProtection;
+ }
+ // Server expects 'tollFraudManagedRules' but client uses 'smsTollFraudManagedRules'
+ if (options.smsTollFraudManagedRules != null) {
+ request['tollFraudManagedRules'] = options.smsTollFraudManagedRules!
+ .map((e) => e.toJson())
+ .toList();
+ }
+
+ return request;
+ }
+
+ static void _validate(RecaptchaConfig options) {
+ if (options.managedRules != null) {
+ options.managedRules!.forEach(_validateManagedRule);
+ }
+
+ if (options.smsTollFraudManagedRules != null) {
+ options.smsTollFraudManagedRules!.forEach(_validateTollFraudManagedRule);
+ }
+ }
+
+ static void _validateManagedRule(RecaptchaManagedRule rule) {
+ if (rule.action != null && rule.action != RecaptchaAction.block) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidConfig,
+ '"RecaptchaManagedRule.action" must be "BLOCK".',
+ );
+ }
+ }
+
+ static void _validateTollFraudManagedRule(
+ RecaptchaTollFraudManagedRule rule,
+ ) {
+ if (rule.action != null && rule.action != RecaptchaAction.block) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidConfig,
+ '"RecaptchaTollFraudManagedRule.action" must be "BLOCK".',
+ );
+ }
+ }
+
+ @override
+ final RecaptchaProviderEnforcementState? emailPasswordEnforcementState;
+
+ @override
+ final RecaptchaProviderEnforcementState? phoneEnforcementState;
+
+ @override
+ final List? managedRules;
+
+ @override
+ final List? recaptchaKeys;
+
+ @override
+ final bool? useAccountDefender;
+
+ @override
+ final bool? useSmsBotScore;
+
+ @override
+ final bool? useSmsTollFraudProtection;
+
+ @override
+ final List? smsTollFraudManagedRules;
+
+ @override
+ Map toJson() => {
+ if (emailPasswordEnforcementState != null)
+ 'emailPasswordEnforcementState': emailPasswordEnforcementState!.value,
+ if (phoneEnforcementState != null)
+ 'phoneEnforcementState': phoneEnforcementState!.value,
+ if (managedRules != null)
+ 'managedRules': managedRules!.map((e) => e.toJson()).toList(),
+ if (recaptchaKeys != null)
+ 'recaptchaKeys': recaptchaKeys!.map((e) => e.toJson()).toList(),
+ if (useAccountDefender != null) 'useAccountDefender': useAccountDefender,
+ if (useSmsBotScore != null) 'useSmsBotScore': useSmsBotScore,
+ if (useSmsTollFraudProtection != null)
+ 'useSmsTollFraudProtection': useSmsTollFraudProtection,
+ if (smsTollFraudManagedRules != null)
+ 'smsTollFraudManagedRules': smsTollFraudManagedRules!
+ .map((e) => e.toJson())
+ .toList(),
+ };
+}
+
+// ============================================================================
+// Password Policy Configuration
+// ============================================================================
+
+/// A password policy's enforcement state.
+enum PasswordPolicyEnforcementState {
+ enforce('ENFORCE'),
+ off('OFF');
+
+ const PasswordPolicyEnforcementState(this.value);
+
+ final String value;
+
+ static PasswordPolicyEnforcementState fromString(String value) {
+ return PasswordPolicyEnforcementState.values.firstWhere(
+ (e) => e.value == value,
+ orElse: () => PasswordPolicyEnforcementState.off,
+ );
+ }
+}
+
+/// Constraints to be enforced on the password policy
+class CustomStrengthOptionsConfig {
+ CustomStrengthOptionsConfig({
+ this.requireUppercase,
+ this.requireLowercase,
+ this.requireNonAlphanumeric,
+ this.requireNumeric,
+ this.minLength,
+ this.maxLength,
+ });
+
+ /// The password must contain an upper case character
+ final bool? requireUppercase;
+
+ /// The password must contain a lower case character
+ final bool? requireLowercase;
+
+ /// The password must contain a non-alphanumeric character
+ final bool? requireNonAlphanumeric;
+
+ /// The password must contain a number
+ final bool? requireNumeric;
+
+ /// Minimum password length. Valid values are from 6 to 30
+ final int? minLength;
+
+ /// Maximum password length. No default max length
+ final int? maxLength;
+
+ Map toJson() => {
+ if (requireUppercase != null) 'requireUppercase': requireUppercase,
+ if (requireLowercase != null) 'requireLowercase': requireLowercase,
+ if (requireNonAlphanumeric != null)
+ 'requireNonAlphanumeric': requireNonAlphanumeric,
+ if (requireNumeric != null) 'requireNumeric': requireNumeric,
+ if (minLength != null) 'minLength': minLength,
+ if (maxLength != null) 'maxLength': maxLength,
+ };
+}
+
+/// A password policy configuration for a project or tenant
+class PasswordPolicyConfig {
+ PasswordPolicyConfig({
+ this.enforcementState,
+ this.forceUpgradeOnSignin,
+ this.constraints,
+ });
+
+ /// Enforcement state of the password policy
+ final PasswordPolicyEnforcementState? enforcementState;
+
+ /// Require users to have a policy-compliant password to sign in
+ final bool? forceUpgradeOnSignin;
+
+ /// The constraints that make up the password strength policy
+ final CustomStrengthOptionsConfig? constraints;
+
+ Map toJson() => {
+ if (enforcementState != null) 'enforcementState': enforcementState!.value,
+ if (forceUpgradeOnSignin != null)
+ 'forceUpgradeOnSignin': forceUpgradeOnSignin,
+ if (constraints != null) 'constraints': constraints!.toJson(),
+ };
+}
+
+/// Internal class for password policy authentication configuration.
+class _PasswordPolicyAuthConfig implements PasswordPolicyConfig {
+ _PasswordPolicyAuthConfig({
+ this.enforcementState,
+ this.forceUpgradeOnSignin,
+ this.constraints,
+ });
+
+ factory _PasswordPolicyAuthConfig.fromServerResponse(
+ Map response,
+ ) {
+ final stateValue = response['passwordPolicyEnforcementState'];
+ if (stateValue == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Invalid password policy configuration response',
+ );
+ }
+
+ CustomStrengthOptionsConfig? constraints;
+ final policyVersions = response['passwordPolicyVersions'] as List?;
+ if (policyVersions != null && policyVersions.isNotEmpty) {
+ final firstVersion = policyVersions.first as Map;
+ final options =
+ firstVersion['customStrengthOptions'] as Map?;
+ if (options != null) {
+ constraints = CustomStrengthOptionsConfig(
+ requireLowercase: options['containsLowercaseCharacter'] as bool?,
+ requireUppercase: options['containsUppercaseCharacter'] as bool?,
+ requireNonAlphanumeric:
+ options['containsNonAlphanumericCharacter'] as bool?,
+ requireNumeric: options['containsNumericCharacter'] as bool?,
+ minLength: options['minPasswordLength'] as int?,
+ maxLength: options['maxPasswordLength'] as int?,
+ );
+ }
+ }
+
+ return _PasswordPolicyAuthConfig(
+ enforcementState: PasswordPolicyEnforcementState.fromString(
+ stateValue as String,
+ ),
+ forceUpgradeOnSignin: response['forceUpgradeOnSignin'] as bool? ?? false,
+ constraints: constraints,
+ );
+ }
+
+ static Map buildServerRequest(PasswordPolicyConfig options) {
+ final request = {};
+
+ if (options.enforcementState != null) {
+ request['passwordPolicyEnforcementState'] =
+ options.enforcementState!.value;
+ }
+ request['forceUpgradeOnSignin'] = options.forceUpgradeOnSignin ?? false;
+
+ if (options.constraints != null) {
+ final constraintsRequest = {
+ 'containsUppercaseCharacter':
+ options.constraints!.requireUppercase ?? false,
+ 'containsLowercaseCharacter':
+ options.constraints!.requireLowercase ?? false,
+ 'containsNonAlphanumericCharacter':
+ options.constraints!.requireNonAlphanumeric ?? false,
+ 'containsNumericCharacter':
+ options.constraints!.requireNumeric ?? false,
+ 'minPasswordLength': options.constraints!.minLength ?? 6,
+ 'maxPasswordLength': options.constraints!.maxLength ?? 4096,
+ };
+ request['passwordPolicyVersions'] = [
+ {'customStrengthOptions': constraintsRequest},
+ ];
+ }
+
+ return request;
+ }
+
+ @override
+ final PasswordPolicyEnforcementState? enforcementState;
+
+ @override
+ final bool? forceUpgradeOnSignin;
+
+ @override
+ final CustomStrengthOptionsConfig? constraints;
+
+ @override
+ Map toJson() => {
+ if (enforcementState != null) 'enforcementState': enforcementState!.value,
+ if (forceUpgradeOnSignin != null)
+ 'forceUpgradeOnSignin': forceUpgradeOnSignin,
+ if (constraints != null) 'constraints': constraints!.toJson(),
+ };
+}
+
+// ============================================================================
+// Email Privacy Configuration
+// ============================================================================
+
+/// The email privacy configuration of a project or tenant.
+class EmailPrivacyConfig {
+ EmailPrivacyConfig({this.enableImprovedEmailPrivacy});
+
+ /// Whether enhanced email privacy is enabled.
+ final bool? enableImprovedEmailPrivacy;
+
+ Map toJson() => {
+ if (enableImprovedEmailPrivacy != null)
+ 'enableImprovedEmailPrivacy': enableImprovedEmailPrivacy,
+ };
+}
diff --git a/packages/dart_firebase_admin/lib/src/auth/auth_exception.dart b/packages/dart_firebase_admin/lib/src/auth/auth_exception.dart
index e5f49ecd..a7cfecf3 100644
--- a/packages/dart_firebase_admin/lib/src/auth/auth_exception.dart
+++ b/packages/dart_firebase_admin/lib/src/auth/auth_exception.dart
@@ -2,10 +2,12 @@ part of '../auth.dart';
class FirebaseAuthAdminException extends FirebaseAdminException
implements Exception {
- FirebaseAuthAdminException(
- this.errorCode, [
- String? message,
- ]) : super('auth', errorCode.code, message ?? errorCode.message);
+ FirebaseAuthAdminException(this.errorCode, [String? message])
+ : super(
+ FirebaseServiceType.auth.name,
+ errorCode.code,
+ message ?? errorCode.message,
+ );
factory FirebaseAuthAdminException.fromServerError({
required String serverErrorCode,
@@ -15,12 +17,16 @@ class FirebaseAuthAdminException extends FirebaseAdminException
// ERROR_CODE : Detailed message which can also contain colons
final colonSeparator = serverErrorCode.indexOf(':');
String? customMessage;
+ var effectiveErrorCode = serverErrorCode;
if (colonSeparator != -1) {
customMessage = serverErrorCode.substring(colonSeparator + 1).trim();
- serverErrorCode = serverErrorCode.substring(0, colonSeparator).trim();
+ // Treat empty string as null
+ if (customMessage.isEmpty) customMessage = null;
+ effectiveErrorCode = serverErrorCode.substring(0, colonSeparator).trim();
}
// If not found, default to internal error.
- final error = authServerToClientCode[serverErrorCode] ??
+ final error =
+ authServerToClientCode[effectiveErrorCode] ??
AuthClientErrorCode.internalError;
// Server detailed message should have highest priority.
customMessage = customMessage ?? error.message;
@@ -63,8 +69,8 @@ const authServerToClientCode = {
'INVALID_CONFIG_ID': AuthClientErrorCode.invalidProviderId,
// ActionCodeSettings missing continue URL.
'INVALID_CONTINUE_URI': AuthClientErrorCode.invalidContinueUri,
- // Dynamic link domain in provided ActionCodeSettings is not authorized.
- 'INVALID_DYNAMIC_LINK_DOMAIN': AuthClientErrorCode.invalidDynamicLinkDomain,
+ // Hosting link domain in provided ActionCodeSettings is not owned by the current project.
+ 'INVALID_HOSTING_LINK_DOMAIN': AuthClientErrorCode.invalidHostingLinkDomain,
// uploadAccount provides an email that already exists.
'DUPLICATE_EMAIL': AuthClientErrorCode.emailAlreadyExists,
// uploadAccount provides a localId that already exists.
@@ -102,6 +108,8 @@ const authServerToClientCode = {
'INVALID_PROJECT_ID': AuthClientErrorCode.invalidProjectId,
// Invalid provider ID.
'INVALID_PROVIDER_ID': AuthClientErrorCode.invalidProviderId,
+ // Invalid service account.
+ 'INVALID_SERVICE_ACCOUNT': AuthClientErrorCode.invalidServiceAccount,
// Invalid testing phone number.
'INVALID_TESTING_PHONE_NUMBER': AuthClientErrorCode.invalidTestingPhoneNumber,
// Invalid tenant type.
@@ -150,7 +158,7 @@ const authServerToClientCode = {
'TENANT_ID_MISMATCH': AuthClientErrorCode.mismatchingTenantId,
// Token expired error.
'TOKEN_EXPIRED': AuthClientErrorCode.idTokenExpired,
- // Continue URL provided in ActionCodeSettings has a domain that is not whitelisted.
+ // Continue URL provided in ActionCodeSettings has a domain that is not allowed.
'UNAUTHORIZED_DOMAIN': AuthClientErrorCode.unauthorizedDomain,
// A multi-factor user requires a supported first factor.
'UNSUPPORTED_FIRST_FACTOR': AuthClientErrorCode.unsupportedFirstFactor,
@@ -261,11 +269,6 @@ enum AuthClientErrorCode {
code: 'invalid-display-name',
message: 'The displayName field must be a valid string.',
),
- invalidDynamicLinkDomain(
- code: 'invalid-dynamic-link-domain',
- message: 'The provided dynamic link domain is not configured or authorized '
- 'for the current project.',
- ),
invalidEmailVerified(
code: 'invalid-email-verified',
message: 'The emailVerified field must be a boolean.',
@@ -290,7 +293,8 @@ enum AuthClientErrorCode {
),
invalidHashAlgorithm(
code: 'invalid-hash-algorithm',
- message: 'The hash algorithm must match one of the strings in the list of '
+ message:
+ 'The hash algorithm must match one of the strings in the list of '
'supported algorithms.',
),
invalidHashBlockSize(
@@ -322,6 +326,12 @@ enum AuthClientErrorCode {
message:
'The hashing algorithm salt separator field must be a valid byte buffer.',
),
+ invalidHostingLinkDomain(
+ code: 'invalid-hosting-link-domain',
+ message:
+ 'The provided hosting link domain is not configured or authorized '
+ 'for the current project.',
+ ),
invalidLastSignInTime(
code: 'invalid-last-sign-in-time',
message: 'The last sign-in time must be a valid UTC date string.',
@@ -362,7 +372,8 @@ enum AuthClientErrorCode {
),
invalidProjectId(
code: 'invalid-project-id',
- message: 'Invalid parent project. '
+ message:
+ 'Invalid parent project. '
"Either parent project doesn't exist or didn't enable multi-tenancy.",
),
invalidProviderData(
@@ -388,6 +399,10 @@ enum AuthClientErrorCode {
'The session cookie duration must be a valid number in milliseconds '
'between 5 minutes and 2 weeks.',
),
+ invalidServiceAccount(
+ code: 'invalid-service-account',
+ message: 'Invalid service account.',
+ ),
invalidTenantId(
code: 'invalid-tenant-id',
message: 'The tenant ID must be a valid non-empty string.',
@@ -419,7 +434,8 @@ enum AuthClientErrorCode {
),
missingAndroidPackageName(
code: 'missing-android-pkg-name',
- message: 'An Android Package Name must be provided if the Android App is '
+ message:
+ 'An Android Package Name must be provided if the Android App is '
'required to be installed.',
),
missingConfig(
@@ -451,7 +467,8 @@ enum AuthClientErrorCode {
),
missingHashAlgorithm(
code: 'missing-hash-algorithm',
- message: 'Importing users with password hashes requires that the hashing '
+ message:
+ 'Importing users with password hashes requires that the hashing '
'algorithm and its parameters be provided.',
),
missingOauthClientId(
@@ -540,7 +557,7 @@ enum AuthClientErrorCode {
unauthorizedDomain(
code: 'unauthorized-continue-uri',
message:
- 'The domain of the continue URL is not whitelisted. Whitelist the domain in the '
+ 'The domain of the continue URL is not allowed. Add the domain to the allow list in the '
'Firebase console.',
),
unsupportedFirstFactor(
@@ -566,14 +583,8 @@ enum AuthClientErrorCode {
message:
'There is no user record corresponding to the provided identifier.',
),
- notFound(
- code: 'not-found',
- message: 'The requested resource was not found.',
- ),
- userDisabled(
- code: 'user-disabled',
- message: 'The user record is disabled.',
- ),
+ notFound(code: 'not-found', message: 'The requested resource was not found.'),
+ userDisabled(code: 'user-disabled', message: 'The user record is disabled.'),
userNotDisabled(
code: 'user-not-disabled',
message:
@@ -593,10 +604,7 @@ enum AuthClientErrorCode {
message: 'reCAPTCHA enterprise is not enabled.',
);
- const AuthClientErrorCode({
- required this.code,
- required this.message,
- });
+ const AuthClientErrorCode({required this.code, required this.message});
final String code;
final String message;
diff --git a/packages/dart_firebase_admin/lib/src/auth/auth_http_client.dart b/packages/dart_firebase_admin/lib/src/auth/auth_http_client.dart
new file mode 100644
index 00000000..67988fbe
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/auth_http_client.dart
@@ -0,0 +1,505 @@
+part of '../auth.dart';
+
+class AuthHttpClient {
+ AuthHttpClient(this.app);
+
+ final FirebaseApp app;
+
+ /// Gets the Auth API host URL based on emulator configuration.
+ ///
+ /// When [Environment.firebaseAuthEmulatorHost] is set, routes requests to
+ /// the local Auth emulator. Otherwise, uses production Auth API.
+ Uri get _authApiHost {
+ final emulatorHost = Environment.getAuthEmulatorHost();
+
+ if (emulatorHost != null) {
+ return Uri.http(emulatorHost, 'identitytoolkit.googleapis.com/');
+ }
+
+ return Uri.https('identitytoolkit.googleapis.com', '/');
+ }
+
+ /// Lazy-initialized HTTP client that's cached for reuse.
+ /// Uses unauthenticated client for emulator, authenticated for production.
+ late final Future _client = _createClient();
+
+ Future get client => _client;
+
+ /// Creates the appropriate HTTP client based on emulator configuration.
+ Future _createClient() async {
+ // If app has custom httpClient (e.g., mock for testing), always use it
+ if (app.options.httpClient != null) {
+ return app.client;
+ }
+
+ if (Environment.isAuthEmulatorEnabled()) {
+ // Emulator: Create unauthenticated client to avoid loading ADC credentials
+ // which would cause emulator warnings. Wrap with EmulatorClient to add
+ // "Authorization: Bearer owner" header that the emulator requires.
+ return EmulatorClient(http.Client());
+ }
+ // Production: Use authenticated client from app
+ return app.client;
+ }
+
+ /// Builds the parent resource path for project-level operations.
+ String buildParent(String projectId) {
+ return 'projects/$projectId';
+ }
+
+ String buildProjectConfigParent(String projectId) {
+ return '${buildParent(projectId)}/config';
+ }
+
+ /// Builds the parent path for OAuth IDP config operations.
+ String buildOAuthIdpParent(String projectId, String parentId) {
+ return 'projects/$projectId/oauthIdpConfigs/$parentId';
+ }
+
+ /// Builds the parent path for SAML config operations.
+ String buildSamlParent(String projectId, String parentId) {
+ return 'projects/$projectId/inboundSamlConfigs/$parentId';
+ }
+
+ /// Builds the resource path for a specific tenant.
+ String buildTenantParent(String projectId, String tenantId) {
+ return 'projects/$projectId/tenants/$tenantId';
+ }
+
+ Future getOobCode(
+ auth1.GoogleCloudIdentitytoolkitV1GetOobCodeRequest request,
+ ) {
+ return v1((api, projectId) async {
+ final email = request.email;
+ if (email == null || !isEmail(email)) {
+ throw FirebaseAuthAdminException(AuthClientErrorCode.invalidEmail);
+ }
+
+ final newEmail = request.newEmail;
+ if (newEmail != null && !isEmail(newEmail)) {
+ throw FirebaseAuthAdminException(AuthClientErrorCode.invalidEmail);
+ }
+
+ if (!_emailActionRequestTypes.contains(request.requestType)) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ '"${request.requestType}" is not a supported email action request type.',
+ );
+ }
+
+ final response = await api.accounts.sendOobCode(request);
+
+ if (response.oobLink == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Unable to generate email action link',
+ );
+ }
+
+ return response;
+ });
+ }
+
+ Future
+ listInboundSamlConfigs({required int pageSize, String? pageToken}) {
+ return v2((api, projectId) async {
+ if (pageToken != null && pageToken.isEmpty) {
+ throw FirebaseAuthAdminException(AuthClientErrorCode.invalidPageToken);
+ }
+
+ if (pageSize <= 0 || pageSize > _maxListProviderConfigurationPageSize) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ 'Required "maxResults" must be a positive integer that does not exceed '
+ '$_maxListProviderConfigurationPageSize.',
+ );
+ }
+
+ return api.projects.inboundSamlConfigs.list(
+ buildParent(projectId),
+ pageSize: pageSize,
+ pageToken: pageToken,
+ );
+ });
+ }
+
+ Future
+ listOAuthIdpConfigs({required int pageSize, String? pageToken}) {
+ return v2((api, projectId) async {
+ if (pageToken != null && pageToken.isEmpty) {
+ throw FirebaseAuthAdminException(AuthClientErrorCode.invalidPageToken);
+ }
+
+ if (pageSize <= 0 || pageSize > _maxListProviderConfigurationPageSize) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ 'Required "maxResults" must be a positive integer that does not exceed '
+ '$_maxListProviderConfigurationPageSize.',
+ );
+ }
+
+ return api.projects.oauthIdpConfigs.list(
+ buildParent(projectId),
+ pageSize: pageSize,
+ pageToken: pageToken,
+ );
+ });
+ }
+
+ Future
+ createOAuthIdpConfig(
+ auth2.GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig request,
+ String providerId,
+ ) {
+ return v2((api, projectId) async {
+ final response = await api.projects.oauthIdpConfigs.create(
+ request,
+ buildParent(projectId),
+ oauthIdpConfigId: providerId,
+ );
+
+ final name = response.name;
+ if (name == null || name.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Unable to create OIDC configuration',
+ );
+ }
+
+ return response;
+ });
+ }
+
+ Future
+ createInboundSamlConfig(
+ auth2.GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig request,
+ String providerId,
+ ) {
+ return v2((api, projectId) async {
+ final response = await api.projects.inboundSamlConfigs.create(
+ request,
+ buildParent(projectId),
+ inboundSamlConfigId: providerId,
+ );
+
+ final name = response.name;
+ if (name == null || name.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Unable to create SAML configuration',
+ );
+ }
+
+ return response;
+ });
+ }
+
+ Future deleteOauthIdpConfig(String providerId) {
+ return v2((api, projectId) async {
+ await api.projects.oauthIdpConfigs.delete(
+ buildOAuthIdpParent(projectId, providerId),
+ );
+ });
+ }
+
+ Future deleteInboundSamlConfig(String providerId) {
+ return v2((api, projectId) async {
+ await api.projects.inboundSamlConfigs.delete(
+ buildSamlParent(projectId, providerId),
+ );
+ });
+ }
+
+ Future
+ updateInboundSamlConfig(
+ auth2.GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig request,
+ String providerId, {
+ required String? updateMask,
+ }) {
+ return v2((api, projectId) async {
+ final response = await api.projects.inboundSamlConfigs.patch(
+ request,
+ buildSamlParent(projectId, providerId),
+ updateMask: updateMask,
+ );
+
+ if (response.name == null || response.name!.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Unable to update SAML configuration',
+ );
+ }
+
+ return response;
+ });
+ }
+
+ Future
+ updateOAuthIdpConfig(
+ auth2.GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig request,
+ String providerId, {
+ required String? updateMask,
+ }) {
+ return v2((api, projectId) async {
+ final response = await api.projects.oauthIdpConfigs.patch(
+ request,
+ buildOAuthIdpParent(projectId, providerId),
+ updateMask: updateMask,
+ );
+
+ if (response.name == null || response.name!.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Unable to update OIDC configuration',
+ );
+ }
+
+ return response;
+ });
+ }
+
+ Future
+ setAccountInfo(
+ auth1.GoogleCloudIdentitytoolkitV1SetAccountInfoRequest request,
+ ) {
+ return v1((api, projectId) async {
+ // TODO should this use account/project/update or account/update?
+ // Or maybe both?
+ // ^ Depending on it, use tenantId... Or do we? The request seems to reject tenantID args
+ final response = await api.accounts.update(request);
+
+ final localId = response.localId;
+ if (localId == null) {
+ throw FirebaseAuthAdminException(AuthClientErrorCode.userNotFound);
+ }
+ return response;
+ });
+ }
+
+ Future
+ getOauthIdpConfig(String providerId) {
+ return v2((api, projectId) async {
+ final response = await api.projects.oauthIdpConfigs.get(
+ buildOAuthIdpParent(projectId, providerId),
+ );
+
+ final name = response.name;
+ if (name == null || name.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Unable to get OIDC configuration',
+ );
+ }
+
+ return response;
+ });
+ }
+
+ Future
+ getInboundSamlConfig(String providerId) {
+ return v2((api, projectId) async {
+ final response = await api.projects.inboundSamlConfigs.get(
+ buildSamlParent(projectId, providerId),
+ );
+
+ final name = response.name;
+ if (name == null || name.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Unable to get SAML configuration',
+ );
+ }
+
+ return response;
+ });
+ }
+
+ // Tenant management methods
+ Future getTenant(
+ String tenantId,
+ ) {
+ return v2((api, projectId) async {
+ if (tenantId.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidTenantId,
+ 'Tenant ID must be a non-empty string.',
+ );
+ }
+
+ final response = await api.projects.tenants.get(
+ buildTenantParent(projectId, tenantId),
+ );
+
+ if (response.name == null || response.name!.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Unable to get tenant',
+ );
+ }
+
+ return response;
+ });
+ }
+
+ Future
+ listTenants({required int maxResults, String? pageToken}) {
+ // TODO(demalaf): rename client below to identityApi or api
+ return v2((api, projectId) async {
+ final response = await api.projects.tenants.list(
+ buildParent(projectId),
+ pageSize: maxResults,
+ pageToken: pageToken,
+ );
+
+ return response;
+ });
+ }
+
+ Future deleteTenant(String tenantId) {
+ return v2((api, projectId) async {
+ if (tenantId.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidTenantId,
+ 'Tenant ID must be a non-empty string.',
+ );
+ }
+
+ return api.projects.tenants.delete(
+ buildTenantParent(projectId, tenantId),
+ );
+ });
+ }
+
+ Future createTenant(
+ auth2.GoogleCloudIdentitytoolkitAdminV2Tenant request,
+ ) {
+ return v2((api, projectId) async {
+ final response = await api.projects.tenants.create(
+ request,
+ buildParent(projectId),
+ );
+
+ if (response.name == null || response.name!.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Unable to create new tenant',
+ );
+ }
+
+ return response;
+ });
+ }
+
+ Future updateTenant(
+ String tenantId,
+ auth2.GoogleCloudIdentitytoolkitAdminV2Tenant request,
+ ) {
+ return v2((api, projectId) async {
+ if (tenantId.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidTenantId,
+ 'Tenant ID must be a non-empty string.',
+ );
+ }
+
+ final name = buildTenantParent(projectId, tenantId);
+ final updateMask = request.toJson().keys.join(',');
+
+ final response = await api.projects.tenants.patch(
+ request,
+ name,
+ updateMask: updateMask,
+ );
+
+ if (response.name == null || response.name!.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Unable to update tenant',
+ );
+ }
+
+ return response;
+ });
+ }
+
+ // Project Config management methods
+ Future getConfig() {
+ return v2((api, projectId) async {
+ final name = buildProjectConfigParent(projectId);
+ final response = await api.projects.getConfig(name);
+ return response;
+ });
+ }
+
+ Future updateConfig(
+ auth2.GoogleCloudIdentitytoolkitAdminV2Config request,
+ String updateMask,
+ ) {
+ return v2((api, projectId) async {
+ final name = buildProjectConfigParent(projectId);
+ final response = await api.projects.updateConfig(
+ request,
+ name,
+ updateMask: updateMask,
+ );
+ return response;
+ });
+ }
+
+ Future _run(
+ Future Function(googleapis_auth.AuthClient client, String projectId) fn,
+ ) {
+ return _authGuard(() async {
+ // Use the cached client (created once based on emulator configuration)
+ final client = await _client;
+ final projectId = await app.getProjectId();
+ return fn(client, projectId);
+ });
+ }
+
+ Future v1(
+ Future Function(auth1.IdentityToolkitApi api, String projectId) fn,
+ ) => _run(
+ (client, projectId) => fn(
+ auth1.IdentityToolkitApi(client, rootUrl: _authApiHost.toString()),
+ projectId,
+ ),
+ );
+
+ Future v2(
+ Future Function(auth2.IdentityToolkitApi api, String projectId) fn,
+ ) => _run(
+ (client, projectId) => fn(
+ auth2.IdentityToolkitApi(client, rootUrl: _authApiHost.toString()),
+ projectId,
+ ),
+ );
+
+ Future v3(
+ Future Function(auth3.IdentityToolkitApi api, String projectId) fn,
+ ) => _run(
+ (client, projectId) => fn(
+ auth3.IdentityToolkitApi(client, rootUrl: _authApiHost.toString()),
+ projectId,
+ ),
+ );
+}
+
+/// Tenant-aware HTTP client that builds tenant-specific resource paths.
+class _TenantAwareAuthHttpClient extends AuthHttpClient {
+ _TenantAwareAuthHttpClient(super.app, this.tenantId);
+
+ final String tenantId;
+
+ @override
+ String buildParent(String projectId) =>
+ 'projects/$projectId/tenants/$tenantId';
+
+ @override
+ String buildOAuthIdpParent(String projectId, String parentId) =>
+ 'projects/$projectId/tenants/$tenantId/oauthIdpConfigs/$parentId';
+
+ @override
+ String buildSamlParent(String projectId, String parentId) =>
+ 'projects/$projectId/tenants/$tenantId/inboundSamlConfigs/$parentId';
+}
diff --git a/packages/dart_firebase_admin/lib/src/auth/auth_api_request.dart b/packages/dart_firebase_admin/lib/src/auth/auth_request_handler.dart
similarity index 55%
rename from packages/dart_firebase_admin/lib/src/auth/auth_api_request.dart
rename to packages/dart_firebase_admin/lib/src/auth/auth_request_handler.dart
index 8d6da4fb..951b2aac 100644
--- a/packages/dart_firebase_admin/lib/src/auth/auth_api_request.dart
+++ b/packages/dart_firebase_admin/lib/src/auth/auth_request_handler.dart
@@ -21,6 +21,7 @@ const _minSessionCookieDurationSecs = 5 * 60;
/// Maximum allowed session cookie duration in seconds (2 weeks).
const _maxSessionCookieDurationSecs = 14 * 24 * 60 * 60;
+// TODO(demolaf): this could be an enum instead.
/// List of supported email action request types.
const _emailActionRequestTypes = {
'PASSWORD_RESET',
@@ -30,10 +31,13 @@ const _emailActionRequestTypes = {
};
abstract class _AbstractAuthRequestHandler {
- _AbstractAuthRequestHandler(this.app) : _httpClient = _AuthHttpClient(app);
+ _AbstractAuthRequestHandler(this.app, {@internal AuthHttpClient? httpClient})
+ : _httpClient = httpClient ?? AuthHttpClient(app);
- final FirebaseAdminApp app;
- final _AuthHttpClient _httpClient;
+ final FirebaseApp app;
+ final AuthHttpClient _httpClient;
+
+ AuthHttpClient get httpClient => _httpClient;
/// Generates the out of band email action link for the email specified using the action code settings provided.
/// Returns a promise that resolves with the generated link.
@@ -52,7 +56,8 @@ abstract class _AbstractAuthRequestHandler {
// ActionCodeSettings required for email link sign-in to determine the url where the sign-in will
// be completed.
-
+ // TODO(demolaf): find and replace anywhere _emailActionRequestTypes
+ // are hardcoded like the one below i.e. requestType == 'EMAIL_SIGNIN'
if (actionCodeSettings == null && requestType == 'EMAIL_SIGNIN') {
throw FirebaseAuthAdminException(
AuthClientErrorCode.invalidArgument,
@@ -79,7 +84,7 @@ abstract class _AbstractAuthRequestHandler {
/// Lists the OIDC configurations (single batch only) with a size of maxResults and starting from
/// the offset as specified by pageToken.
Future
- listOAuthIdpConfigs({int? maxResults, String? pageToken}) async {
+ listOAuthIdpConfigs({int? maxResults, String? pageToken}) async {
final response = await _httpClient.listOAuthIdpConfigs(
pageSize: maxResults ?? _maxListProviderConfigurationPageSize,
pageToken: pageToken,
@@ -94,7 +99,7 @@ abstract class _AbstractAuthRequestHandler {
/// Lists the SAML configurations (single batch only) with a size of maxResults and starting from
/// the offset as specified by pageToken.
Future
- listInboundSamlConfigs({int? maxResults, String? pageToken}) async {
+ listInboundSamlConfigs({int? maxResults, String? pageToken}) async {
final response = await _httpClient.listInboundSamlConfigs(
pageSize: maxResults ?? _maxListProviderConfigurationPageSize,
pageToken: pageToken,
@@ -108,17 +113,19 @@ abstract class _AbstractAuthRequestHandler {
/// Creates a new OIDC provider configuration with the properties provided.
Future