diff --git a/.firebaserc b/.firebaserc
new file mode 100644
index 0000000..23fc90b
--- /dev/null
+++ b/.firebaserc
@@ -0,0 +1,5 @@
+{
+ "projects": {
+ "default": "dart-firebase-admin"
+ }
+}
diff --git a/README.md b/README.md
deleted file mode 100644
index e7db164..0000000
--- a/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Dart Firebase Admin
-
-A Firebase Admin SDK implementation for Dart.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 120000
index 0000000..a65ba2f
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+packages/dart_firebase_admin/README.md
\ No newline at end of file
diff --git a/database.rules.json b/database.rules.json
new file mode 100644
index 0000000..f54493d
--- /dev/null
+++ b/database.rules.json
@@ -0,0 +1,7 @@
+{
+ /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */
+ "rules": {
+ ".read": false,
+ ".write": false
+ }
+}
\ No newline at end of file
diff --git a/firebase.json b/firebase.json
new file mode 100644
index 0000000..6e0fb1f
--- /dev/null
+++ b/firebase.json
@@ -0,0 +1,23 @@
+{
+ "emulators": {
+ "auth": {
+ "port": 9099
+ },
+ "functions": {
+ "port": 5001
+ },
+ "firestore": {
+ "port": 8080
+ },
+ "database": {
+ "port": 9000
+ },
+ "pubsub": {
+ "port": 8085
+ },
+ "ui": {
+ "enabled": true
+ },
+ "singleProjectMode": true
+ }
+}
diff --git a/firestore.indexes.json b/firestore.indexes.json
new file mode 100644
index 0000000..415027e
--- /dev/null
+++ b/firestore.indexes.json
@@ -0,0 +1,4 @@
+{
+ "indexes": [],
+ "fieldOverrides": []
+}
diff --git a/firestore.rules b/firestore.rules
new file mode 100644
index 0000000..921d4e7
--- /dev/null
+++ b/firestore.rules
@@ -0,0 +1,19 @@
+rules_version = '2';
+
+service cloud.firestore {
+ match /databases/{database}/documents {
+
+ // This rule allows anyone with your Firestore database reference to view, edit,
+ // and delete all data in your Firestore database. It is useful for getting
+ // started, but it is configured to expire after 30 days because it
+ // leaves your app open to attackers. At that time, all client
+ // requests to your Firestore database will be denied.
+ //
+ // Make sure to write security rules for your app before that time, or else
+ // all client requests to your Firestore database will be denied until you Update
+ // your rules
+ match /{document=**} {
+ allow read, write: if request.time < timestamp.date(2023, 8, 30);
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/dart_firebase_admin/README.md b/packages/dart_firebase_admin/README.md
index e69de29..8bf9656 100644
--- a/packages/dart_firebase_admin/README.md
+++ b/packages/dart_firebase_admin/README.md
@@ -0,0 +1,157 @@
+## Dart Firebase Admin
+
+Welcome! This project is a port of [Node's Fireabse Admin SDK](https://github.com/firebase/firebase-admin-node) to Dart.
+
+⚠️ 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).
+
+- [Dart Firebase Admin](#dart-firebase-admin)
+- [Usage](#usage)
+ - [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)
+ - [Using Firestore](#using-firestore)
+
+## Usage
+
+### Connecting to the SDK
+
+Before using Firebase, we must first authenticate.
+
+There are currently two options:
+
+- You can connect using environment variables
+- Alternatively, you can specify a `service-account.json` file
+
+#### Connecting using the environment
+
+To connect using environment variables, you will need to have
+the [Firebase CLI](https://firebaseopensource.com/projects/firebase/firebase-tools/) installed.
+
+Once done, you can run:
+
+```sh
+firebase login
+```
+
+And log-in to the project of your choice.
+
+From there, you can have your Dart program authenticate
+using the environment with:
+
+```dart
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+
+void main() {
+ final admin = FirebaseAdminApp.initializeApp(
+ '',
+ // This will obtain authentication informations from the environment
+ Credential.fromApplicationDefaultCredentials(),
+ );
+
+ // TODO use the Admin SDK
+ final firestore = Firestore(admin);
+ firestore.doc('hello/world').get();
+}
+```
+
+#### Connecting using a `service-account.json`` file
+
+Alternatively, you can choose to use a `service-account.json` file.
+This file can be obtained in your firebase console by going to:
+
+```
+https://console.firebase.google.com/u/0/project//settings/serviceaccounts/adminsdk
+```
+
+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.
+
+**⚠️ Note**:
+This file should be kept private. Do not commit it on public repositories.
+
+After all of that is done, you can now authenticate in your Dart program using:
+
+```dart
+import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+
+void main() {
+ final admin = FirebaseAdminApp.initializeApp(
+ '',
+ // Log-in using the newly downloaded file.
+ Credential.fromServiceAccount(
+ File(''),
+ ),
+ );
+
+ // TODO use the Admin SDK
+ final firestore = Firestore(admin);
+ firestore.doc('hello/world').get();
+}
+```
+
+### Using Firestore
+
+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 `Firestore` object as followed:
+
+```dart
+// Obtained in the previous steps
+FirebaseAdminApp admin;
+final firestore = Firestore(admin);
+```
+
+From this point onwards, using Firestore with the admin ADK
+is roughtly equivalent to using [FlutterFire](https://github.com/firebase/flutterfire).
+
+Using this `Firestore` object, you'll find your usual collection/query/document
+objects.
+
+For example you can perform a `where` query:
+
+```dart
+// The following lists all users above 18 years old
+final collection = firestore.collection('users');
+final adults = collection.where('age', WhereFilter.greaterThan, 18);
+
+final adultsSnapshot = await adults.get();
+
+for (final adult in adultsSnapshot.docs) {
+ print(adult.data()['age']);
+}
+```
+
+Composite queries are also supported:
+
+```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'),
+ ]),
+ );
+```
+
+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']);
+```
+
+---
+
+
+
+
+
+
+ Built and maintained by Invertase .
+
+
diff --git a/packages/dart_firebase_admin/example/lib/main.dart b/packages/dart_firebase_admin/example/lib/main.dart
index 5355abf..1a9c52f 100644
--- a/packages/dart_firebase_admin/example/lib/main.dart
+++ b/packages/dart_firebase_admin/example/lib/main.dart
@@ -1,4 +1,5 @@
import 'package:dart_firebase_admin/dart_firebase_admin.dart';
+import 'package:dart_firebase_admin/firestore.dart';
Future main() async {
final admin = FirebaseAdminApp.initializeApp(
@@ -6,12 +7,20 @@ Future main() async {
Credential.fromApplicationDefaultCredentials(),
);
- final auth = FirebaseAdminAuth(admin);
+ admin.useEmulator();
- // await auth.deleteUser('867gK70vkJNjOzlj4uQoMcg7a1d2');
- // await auth.createSessionCookie('867gK70vkJNjOzlj4uQoMcg7a1d2');
- final d = await auth.deleteUsers(['p9bj9If2i4eQlr7NxnaxWGZsmgq1']);
- print(d.errors);
- print(d.failureCount);
- print('Deleted!');
+ final firestore = Firestore(admin);
+
+ final collection = firestore.collection('users');
+
+ await collection.doc('123').set({
+ 'name': 'John Doe',
+ 'age': 30,
+ });
+
+ final snapshot = await collection.get();
+
+ for (final doc in snapshot.docs) {
+ print(doc.data());
+ }
}
diff --git a/packages/dart_firebase_admin/lib/dart_firebase_admin.dart b/packages/dart_firebase_admin/lib/dart_firebase_admin.dart
index 2e9ccde..c1a7e50 100644
--- a/packages/dart_firebase_admin/lib/dart_firebase_admin.dart
+++ b/packages/dart_firebase_admin/lib/dart_firebase_admin.dart
@@ -1,21 +1 @@
-library dart_firebase_admin;
-
-import 'dart:convert';
-import 'dart:io';
-
-import 'package:firebaseapis/identitytoolkit/v1.dart' as firebase_auth_v1;
-import 'package:firebaseapis/identitytoolkit/v2.dart' as firebase_auth_v2;
-import 'package:firebaseapis/identitytoolkit/v3.dart' as firebase_auth_v3;
-import 'package:googleapis_auth/auth_io.dart' as auth;
-
-part 'src/auth/auth_exception.dart';
-part 'src/auth/create_request.dart';
-part 'src/auth/delete_users_result.dart';
-part 'src/auth/firebase_admin_auth.dart';
-part 'src/auth/update_request.dart';
-part 'src/auth/user_info.dart';
-part 'src/auth/user_metadata.dart';
-part 'src/auth/user_record.dart';
-part 'src/credential.dart';
-part 'src/exception.dart';
-part 'src/firebase_admin.dart';
+export 'src/dart_firebase_admin.dart' show FirebaseAdminApp, Credential;
diff --git a/packages/dart_firebase_admin/lib/firestore.dart b/packages/dart_firebase_admin/lib/firestore.dart
new file mode 100644
index 0000000..d260942
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/firestore.dart
@@ -0,0 +1,2 @@
+export 'src/google_cloud_firestore/firestore.dart'
+ hide $SettingsCopyWith, DocumentReader, ApiMapValue;
diff --git a/packages/dart_firebase_admin/lib/src/app/core.dart b/packages/dart_firebase_admin/lib/src/app/core.dart
new file mode 100644
index 0000000..ddd3774
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app/core.dart
@@ -0,0 +1,150 @@
+import 'dart:io';
+
+import 'credential.dart';
+
+/// Available options to pass to {@link firebase-admin.app#initializeApp}.
+class AppOptions {
+ AppOptions({
+ required this.credential,
+ required this.databaseAuthVariableOverride,
+ required this.databaseURL,
+ required this.serviceAccountId,
+ required this.storageBucket,
+ required this.projectId,
+ required this.httpClient,
+ });
+
+ /// A {@link firebase-admin.app#Credential} object used to
+ /// authenticate the Admin SDK.
+ ///
+ /// See {@link https://firebase.google.com/docs/admin/setup#initialize_the_sdk | Initialize the SDK}
+ /// for detailed documentation and code samples.
+ final Credential? credential;
+
+ /// The object to use as the {@link https://firebase.google.com/docs/reference/security/database/#auth | auth}
+ /// variable in your Realtime Database Rules when the Admin SDK reads from or
+ /// writes to the Realtime Database. This allows you to downscope the Admin SDK
+ /// from its default full read and write privileges.
+ ///
+ /// You can pass `null` to act as an unauthenticated client.
+ ///
+ /// See
+ /// {@link https://firebase.google.com/docs/database/admin/start#authenticate-with-limited-privileges |
+ /// Authenticate with limited privileges}
+ /// for detailed documentation and code samples.
+ final Object? databaseAuthVariableOverride;
+
+ /// The URL of the Realtime Database from which to read and write data.
+ final String? databaseURL;
+
+ /// The ID of the service account to be used for signing custom tokens. This
+ /// can be found in the `client_email` field of a service account JSON file.
+ final String? serviceAccountId;
+
+ /// The name of the Google Cloud Storage bucket used for storing application data.
+ /// Use only the bucket name without any prefixes or additions (do *not* prefix
+ /// the name with "gs://").
+ final String? storageBucket;
+
+ /// The ID of the Google Cloud project associated with the App.
+ final String? projectId;
+
+ /// An {@link https://nodejs.org/api/http.html#http_class_http_agent | HTTP Agent}
+ /// to be used when making outgoing HTTP calls. This Agent instance is used
+ /// by all services that make REST calls (e.g. `auth`, `messaging`,
+ /// `projectManagement`).
+ ///
+ /// Realtime Database and Firestore use other means of communicating with
+ /// the backend servers, so they do not use this HTTP Agent. `Credential`
+ /// instances also do not use this HTTP Agent, but instead support
+ /// specifying an HTTP Agent in the corresponding factory methods.
+ final HttpClient? httpClient;
+}
+
+/// A Firebase app holds the initialization information for a collection of
+/// services.
+class App {
+ App({required this.name, required this.options});
+
+ /// The (read-only) name for this app.
+ ///
+ /// The default app's name is `"[DEFAULT]"`.
+ ///
+ /// @example
+ /// ```javascript
+ /// // The default app's name is "[DEFAULT]"
+ /// initializeApp(defaultAppConfig);
+ /// console.log(admin.app().name); // "[DEFAULT]"
+ /// ```
+ ///
+ /// @example
+ /// ```javascript
+ /// // A named app's name is what you provide to initializeApp()
+ /// const otherApp = initializeApp(otherAppConfig, "other");
+ /// console.log(otherApp.name); // "other"
+ /// ```
+ final String name;
+
+ /// The (read-only) configuration options for this app. These are the original
+ /// parameters given in {@link firebase-admin.app#initializeApp}.
+ ///
+ /// @example
+ /// ```javascript
+ /// const app = initializeApp(config);
+ /// console.log(app.options.credential === config.credential); // true
+ /// console.log(app.options.databaseURL === config.databaseURL); // true
+ /// ```
+ final AppOptions options;
+}
+
+/// `FirebaseError` is a subclass of the standard JavaScript `Error` object. In
+/// addition to a message String and stack trace, it contains a String code.
+abstract class FirebaseException implements Exception {
+ /// Error codes are strings using the following format: `"service/String-code"`.
+ /// Some examples include `"auth/invalid-uid"` and
+ /// `"messaging/invalid-recipient"`.
+ ///
+ /// While the message for a given error can change, the code will remain the same
+ /// between backward-compatible versions of the Firebase SDK.
+ String get code;
+
+ /// An explanatory message for the error that just occurred.
+ ///
+ /// This message is designed to be helpful to you, the developer. Because
+ /// it generally does not convey meaningful information to end users,
+ /// this message should not be displayed in your application.
+ String get message;
+}
+
+/// Composite type which includes both a `FirebaseError` object and an index
+/// which can be used to get the errored item.
+///
+/// @example
+/// ```javascript
+/// var registrationTokens = [token1, token2, token3];
+/// admin.messaging().subscribeToTopic(registrationTokens, 'topic-name')
+/// .then(function(response) {
+/// if (response.failureCount > 0) {
+/// console.log("Following devices unsucessfully subscribed to topic:");
+/// response.errors.forEach(function(error) {
+/// var invalidToken = registrationTokens[error.index];
+/// console.log(invalidToken, error.error);
+/// });
+/// } else {
+/// console.log("All devices successfully subscribed to topic:", response);
+/// }
+/// })
+/// .catch(function(error) {
+/// console.log("Error subscribing to topic:", error);
+/// });
+///```
+class FirebaseArrayIndexError {
+ FirebaseArrayIndexError({required this.index, required this.error});
+
+ /// The index of the errored item within the original array passed as part of the
+ /// called API method.
+ final int index;
+
+ /// The error object.
+ final FirebaseException error;
+}
diff --git a/packages/dart_firebase_admin/lib/src/app/core.ts b/packages/dart_firebase_admin/lib/src/app/core.ts
new file mode 100644
index 0000000..440fad2
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app/core.ts
@@ -0,0 +1,207 @@
+/*!
+ * @license
+ * Copyright 2021 Google Inc.
+ *
+ * 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.
+ */
+
+import { Agent } from 'http';
+
+import { Credential } from './credential';
+
+/**
+ * Available options to pass to {@link firebase-admin.app#initializeApp}.
+ */
+export interface AppOptions {
+
+ /**
+ * A {@link firebase-admin.app#Credential} object used to
+ * authenticate the Admin SDK.
+ *
+ * See {@link https://firebase.google.com/docs/admin/setup#initialize_the_sdk | Initialize the SDK}
+ * for detailed documentation and code samples.
+ */
+ credential?: Credential;
+
+ /**
+ * The object to use as the {@link https://firebase.google.com/docs/reference/security/database/#auth | auth}
+ * variable in your Realtime Database Rules when the Admin SDK reads from or
+ * writes to the Realtime Database. This allows you to downscope the Admin SDK
+ * from its default full read and write privileges.
+ *
+ * You can pass `null` to act as an unauthenticated client.
+ *
+ * See
+ * {@link https://firebase.google.com/docs/database/admin/start#authenticate-with-limited-privileges |
+ * Authenticate with limited privileges}
+ * for detailed documentation and code samples.
+ */
+ databaseAuthVariableOverride?: object | null;
+
+ /**
+ * The URL of the Realtime Database from which to read and write data.
+ */
+ databaseURL?: string;
+
+ /**
+ * The ID of the service account to be used for signing custom tokens. This
+ * can be found in the `client_email` field of a service account JSON file.
+ */
+ serviceAccountId?: string;
+
+ /**
+ * The name of the Google Cloud Storage bucket used for storing application data.
+ * Use only the bucket name without any prefixes or additions (do *not* prefix
+ * the name with "gs://").
+ */
+ storageBucket?: string;
+
+ /**
+ * The ID of the Google Cloud project associated with the App.
+ */
+ projectId?: string;
+
+ /**
+ * An {@link https://nodejs.org/api/http.html#http_class_http_agent | HTTP Agent}
+ * to be used when making outgoing HTTP calls. This Agent instance is used
+ * by all services that make REST calls (e.g. `auth`, `messaging`,
+ * `projectManagement`).
+ *
+ * Realtime Database and Firestore use other means of communicating with
+ * the backend servers, so they do not use this HTTP Agent. `Credential`
+ * instances also do not use this HTTP Agent, but instead support
+ * specifying an HTTP Agent in the corresponding factory methods.
+ */
+ httpAgent?: Agent;
+}
+
+/**
+ * A Firebase app holds the initialization information for a collection of
+ * services.
+ */
+export interface App {
+
+ /**
+ * The (read-only) name for this app.
+ *
+ * The default app's name is `"[DEFAULT]"`.
+ *
+ * @example
+ * ```javascript
+ * // The default app's name is "[DEFAULT]"
+ * initializeApp(defaultAppConfig);
+ * console.log(admin.app().name); // "[DEFAULT]"
+ * ```
+ *
+ * @example
+ * ```javascript
+ * // A named app's name is what you provide to initializeApp()
+ * const otherApp = initializeApp(otherAppConfig, "other");
+ * console.log(otherApp.name); // "other"
+ * ```
+ */
+ name: string;
+
+ /**
+ * The (read-only) configuration options for this app. These are the original
+ * parameters given in {@link firebase-admin.app#initializeApp}.
+ *
+ * @example
+ * ```javascript
+ * const app = initializeApp(config);
+ * console.log(app.options.credential === config.credential); // true
+ * console.log(app.options.databaseURL === config.databaseURL); // true
+ * ```
+ */
+ options: AppOptions;
+}
+
+/**
+ * `FirebaseError` is a subclass of the standard JavaScript `Error` object. In
+ * addition to a message string and stack trace, it contains a string code.
+ */
+export interface FirebaseError {
+
+ /**
+ * Error codes are strings using the following format: `"service/string-code"`.
+ * Some examples include `"auth/invalid-uid"` and
+ * `"messaging/invalid-recipient"`.
+ *
+ * While the message for a given error can change, the code will remain the same
+ * between backward-compatible versions of the Firebase SDK.
+ */
+ code: string;
+
+ /**
+ * An explanatory message for the error that just occurred.
+ *
+ * This message is designed to be helpful to you, the developer. Because
+ * it generally does not convey meaningful information to end users,
+ * this message should not be displayed in your application.
+ */
+ message: string;
+
+ /**
+ * A string value containing the execution backtrace when the error originally
+ * occurred.
+ *
+ * This information can be useful for troubleshooting the cause of the error with
+ * {@link https://firebase.google.com/support | Firebase Support}.
+ */
+ stack?: string;
+
+ /**
+ * Returns a JSON-serializable object representation of this error.
+ *
+ * @returns A JSON-serializable representation of this object.
+ */
+ toJSON(): object;
+}
+
+/**
+ * Composite type which includes both a `FirebaseError` object and an index
+ * which can be used to get the errored item.
+ *
+ * @example
+ * ```javascript
+ * var registrationTokens = [token1, token2, token3];
+ * admin.messaging().subscribeToTopic(registrationTokens, 'topic-name')
+ * .then(function(response) {
+ * if (response.failureCount > 0) {
+ * console.log("Following devices unsucessfully subscribed to topic:");
+ * response.errors.forEach(function(error) {
+ * var invalidToken = registrationTokens[error.index];
+ * console.log(invalidToken, error.error);
+ * });
+ * } else {
+ * console.log("All devices successfully subscribed to topic:", response);
+ * }
+ * })
+ * .catch(function(error) {
+ * console.log("Error subscribing to topic:", error);
+ * });
+ *```
+ */
+export interface FirebaseArrayIndexError {
+
+ /**
+ * The index of the errored item within the original array passed as part of the
+ * called API method.
+ */
+ index: number;
+
+ /**
+ * The error object.
+ */
+ error: FirebaseError;
+}
\ No newline at end of file
diff --git a/packages/dart_firebase_admin/lib/src/app/credential.dart b/packages/dart_firebase_admin/lib/src/app/credential.dart
new file mode 100644
index 0000000..dc697e2
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app/credential.dart
@@ -0,0 +1,32 @@
+class ServiceAccount {
+ ServiceAccount({
+ required this.projectId,
+ required this.clientEmail,
+ required this.privateKey,
+ });
+
+ final String? projectId;
+ final String? clientEmail;
+ final String? privateKey;
+}
+
+/// Interface for Google OAuth 2.0 access tokens.
+class GoogleOAuthAccessToken {
+ GoogleOAuthAccessToken({required this.accessToken, required this.expiresIn});
+
+ final String accessToken;
+ final int expiresIn;
+}
+
+/// Interface that provides Google OAuth2 access tokens used to authenticate
+/// with Firebase services.
+///
+/// In most cases, you will not need to implement this yourself and can instead
+/// use the default implementations provided by the `firebase-admin/app` module.
+abstract class Credential {
+ /// Returns a Google OAuth2 access token object used to authenticate with
+ /// Firebase services.
+ ///
+ /// @returns A Google OAuth2 access token object.
+ Future getAccessToken();
+}
diff --git a/packages/dart_firebase_admin/lib/src/app/credential.ts b/packages/dart_firebase_admin/lib/src/app/credential.ts
new file mode 100644
index 0000000..b585790
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/app/credential.ts
@@ -0,0 +1,47 @@
+/*!
+ * @license
+ * Copyright 2021 Google Inc.
+ *
+ * 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.
+ */
+
+export interface ServiceAccount {
+ projectId?: string;
+ clientEmail?: string;
+ privateKey?: string;
+}
+
+/**
+ * Interface for Google OAuth 2.0 access tokens.
+ */
+export interface GoogleOAuthAccessToken {
+ access_token: string;
+ expires_in: number;
+}
+
+/**
+ * Interface that provides Google OAuth2 access tokens used to authenticate
+ * with Firebase services.
+ *
+ * In most cases, you will not need to implement this yourself and can instead
+ * use the default implementations provided by the `firebase-admin/app` module.
+ */
+export interface Credential {
+ /**
+ * Returns a Google OAuth2 access token object used to authenticate with
+ * Firebase services.
+ *
+ * @returns A Google OAuth2 access token object.
+ */
+ getAccessToken(): Promise;
+}
\ No newline at end of file
diff --git a/packages/dart_firebase_admin/lib/src/auth/auth.dart b/packages/dart_firebase_admin/lib/src/auth/auth.dart
new file mode 100644
index 0000000..e69de29
diff --git a/packages/dart_firebase_admin/lib/src/auth/auth.ts b/packages/dart_firebase_admin/lib/src/auth/auth.ts
new file mode 100644
index 0000000..67a64af
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/auth.ts
@@ -0,0 +1,72 @@
+/*!
+ * @license
+ * Copyright 2017 Google Inc.
+ *
+ * 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.
+ */
+
+import { App } from '../app/index';
+import { AuthRequestHandler } from './auth-api-request';
+import { TenantManager } from './tenant-manager';
+import { BaseAuth } from './base-auth';
+import { ProjectConfigManager } from './project-config-manager';
+
+/**
+ * Auth service bound to the provided app.
+ * An Auth instance can have multiple tenants.
+ */
+export class Auth extends BaseAuth {
+
+ private readonly tenantManager_: TenantManager;
+ private readonly projectConfigManager_: ProjectConfigManager;
+ private readonly app_: App;
+
+ /**
+ * @param app - The app for this Auth service.
+ * @constructor
+ * @internal
+ */
+ constructor(app: App) {
+ super(app, new AuthRequestHandler(app));
+ this.app_ = app;
+ this.tenantManager_ = new TenantManager(app);
+ this.projectConfigManager_ = new ProjectConfigManager(app);
+ }
+
+ /**
+ * Returns the app associated with this Auth instance.
+ *
+ * @returns The app associated with this Auth instance.
+ */
+ get app(): App {
+ return this.app_;
+ }
+
+ /**
+ * Returns the tenant manager instance associated with the current project.
+ *
+ * @returns The tenant manager instance associated with the current project.
+ */
+ public tenantManager(): TenantManager {
+ return this.tenantManager_;
+ }
+
+ /**
+ * Returns the project config manager instance associated with the current project.
+ *
+ * @returns The project config manager instance associated with the current project.
+ */
+ public projectConfigManager(): ProjectConfigManager {
+ return this.projectConfigManager_;
+ }
+}
\ No newline at end of file
diff --git a/packages/dart_firebase_admin/lib/src/auth/auth_api_request.dart b/packages/dart_firebase_admin/lib/src/auth/auth_api_request.dart
new file mode 100644
index 0000000..8b5fe8e
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/auth_api_request.dart
@@ -0,0 +1,504 @@
+import 'package:firebaseapis/identitytoolkit/v1.dart' as auth1;
+import 'package:firebaseapis/identitytoolkit/v2.dart' as auth2;
+import 'package:firebaseapis/identitytoolkit/v3.dart' as auth3;
+import 'package:googleapis_auth/googleapis_auth.dart' as auth;
+
+import '../dart_firebase_admin.dart';
+import '../utils/validator.dart';
+import 'auth_config.dart';
+import 'base_auth.dart';
+import 'identifier.dart';
+import 'user_import_builder.dart';
+
+/// Maximum allowed number of users to batch get at one time.
+const maxGetAccountsBatchSize = 100;
+
+/// Maximum allowed number of users to batch download at one time.
+const maxDownloadAccountPageSize = 1000;
+
+/// Maximum allowed number of users to batch delete at one time.
+const maxDeleteAccountsBatchSize = 1000;
+
+/// Maximum allowed number of users to batch upload at one time.
+const maxUploadAccountBatchSize = 1000;
+
+abstract class AbstractAuthRequestHandler {
+ AbstractAuthRequestHandler(this.app) : _httpClient = _AuthHttpClient(app);
+
+ final FirebaseAdminApp app;
+ final _AuthHttpClient _httpClient;
+
+ /// Imports the list of users provided to Firebase Auth. This is useful when
+ /// migrating from an external authentication system without having to use the Firebase CLI SDK.
+ /// At most, 1000 users are allowed to be imported one at a time.
+ /// When importing a list of password users, UserImportOptions are required to be specified.
+ ///
+ /// - users - The list of user records to import to Firebase Auth.
+ /// - options - The user import options, required when the users provided
+ /// include password credentials.
+ ///
+ /// Returns a Future that resolves when the operation completes
+ /// with the result of the import. This includes the number of successful imports, the number
+ /// of failed uploads and their corresponding errors.
+ Future uploadAccount(
+ List users,
+ UserImportOptions? options,
+ ) async {
+ // This will throw if any error is detected in the hash options.
+ // For errors in the list of users, this will not throw and will report the errors and the
+ // corresponding user index in the user import generated response below.
+ // No need to validate raw request or raw response as this is done in UserImportBuilder.
+ final userImportBuilder = UserImportBuilder(
+ users: users,
+ options: options,
+ userRequestValidator: (userRequest) {
+ // Pass true to validate the uploadAccount specific fields.
+ // TODO validateCreateEditRequest
+ },
+ );
+
+ final request = userImportBuilder.buildRequest();
+ final requestUsers = request.users;
+ // Fail quickly if more users than allowed are to be imported.
+ if (requestUsers != null &&
+ requestUsers.length > maxUploadAccountBatchSize) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.maximumUserCountExceeded,
+ 'A maximum of $maxUploadAccountBatchSize users can be imported at once.',
+ );
+ }
+ // If no remaining user in request after client side processing, there is no need
+ // to send the request to the server.
+ if (requestUsers == null || requestUsers.isEmpty) {
+ return userImportBuilder.buildResponse([]);
+ }
+
+ return _httpClient.v1((client) async {
+ final response = await client.projects.accounts_1.batchCreate(
+ request,
+ app.projectId,
+ );
+ // No error object is returned if no error encountered.
+ // Rewrite response as UserImportResult and re-insert client previously detected errors.
+ return userImportBuilder.buildResponse(response.error ?? const []);
+ });
+ }
+
+ /// Exports the users (single batch only) with a size of maxResults and starting from
+ /// the offset as specified by pageToken.
+ ///
+ /// maxResults - The page size, 1000 if undefined. This is also the maximum
+ /// allowed limit.
+ ///
+ /// pageToken - The next page token. If not specified, returns users starting
+ /// without any offset. Users are returned in the order they were created from oldest to
+ /// newest, relative to the page token offset.
+ ///
+ /// Returns a Future that resolves with the current batch of downloaded
+ /// users and the next page token if available. For the last page, an empty list of users
+ /// and no page token are returned.
+ Future
+ downloadAccount({
+ required int? maxResults,
+ required String? pageToken,
+ }) {
+ maxResults ??= maxDownloadAccountPageSize;
+ if (pageToken != null && pageToken.isEmpty) {
+ throw FirebaseAuthAdminException(AuthClientErrorCode.invalidPageToken);
+ }
+ if (maxResults <= 0 || maxResults > maxDownloadAccountPageSize) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ 'Required "maxResults" must be a positive integer that does not exceed '
+ '$maxDownloadAccountPageSize.',
+ );
+ }
+
+ return _httpClient.v1((client) async {
+ // TODO handle tenants
+ return client.projects.accounts_1.batchGet(
+ app.projectId,
+ maxResults: maxResults,
+ nextPageToken: pageToken,
+ );
+ });
+ }
+
+ /// Deletes an account identified by a uid.
+ Future deleteAccount(
+ String uid,
+ ) async {
+ assertIsUid(uid);
+
+ // TODO handle tenants
+ return _httpClient.v1((client) async {
+ return client.projects.accounts_1.delete(
+ auth1.GoogleCloudIdentitytoolkitV1DeleteAccountRequest(localId: uid),
+ app.projectId,
+ );
+ });
+ }
+
+ Future
+ deleteAccounts(
+ List uids, {
+ required bool force,
+ }) async {
+ if (uids.isEmpty) {
+ return auth1.GoogleCloudIdentitytoolkitV1BatchDeleteAccountsResponse();
+ } else if (uids.length > maxDeleteAccountsBatchSize) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.maximumUserCountExceeded,
+ '`uids` parameter must have <= $maxDeleteAccountsBatchSize entries.',
+ );
+ }
+
+ return _httpClient.v1((client) async {
+ // TODO handle tenants
+ return client.projects.accounts_1.batchDelete(
+ auth1.GoogleCloudIdentitytoolkitV1BatchDeleteAccountsRequest(
+ localIds: uids,
+ force: force,
+ ),
+ app.projectId,
+ );
+ });
+ }
+
+ /// Create a new user with the properties supplied.
+ ///
+ /// A [Future] that resolves when the operation completes
+ /// with the user id that was created.
+ Future createNewAccount(CreateRequest properties) async {
+ return _httpClient.v1((client) async {
+ var mfaInfo = properties.multiFactor?.enrolledFactors
+ .map((info) => info.toGoogleCloudIdentitytoolkitV1MfaFactor())
+ .toList();
+ if (mfaInfo != null && mfaInfo.isEmpty) mfaInfo = null;
+
+ // TODO support tenants
+ final response = await client.projects.accounts(
+ auth1.GoogleCloudIdentitytoolkitV1SignUpRequest(
+ captchaChallenge: null,
+ captchaResponse: null,
+ disabled: properties.disabled,
+ displayName: properties.displayName?.value,
+ email: properties.email,
+ emailVerified: properties.emailVerified,
+ idToken: null,
+ instanceId: null,
+ localId: properties.uid,
+ mfaInfo: mfaInfo,
+ password: properties.password,
+ phoneNumber: properties.phoneNumber?.value,
+ photoUrl: properties.photoURL?.value,
+ targetProjectId: null,
+ tenantId: null,
+ ),
+ app.projectId,
+ );
+
+ final localId = response.localId;
+ if (localId == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Unable to create new user',
+ );
+ }
+
+ return localId;
+ });
+ }
+
+ Future
+ _accountsLookup(
+ auth1.GoogleCloudIdentitytoolkitV1GetAccountInfoRequest request,
+ ) async {
+ // TODO handle tenants
+ return _httpClient.v1((client) async {
+ final response = await client.accounts.lookup(request);
+ final users = response.users;
+ if (users == null || users.isEmpty) {
+ throw FirebaseAuthAdminException(AuthClientErrorCode.userNotFound);
+ }
+ return response;
+ });
+ }
+
+ /// Looks up a user by uid.
+ ///
+ /// Returns a Future that resolves with the user information.
+ Future getAccountInfoByUid(
+ String uid,
+ ) async {
+ final response = await _accountsLookup(
+ auth1.GoogleCloudIdentitytoolkitV1GetAccountInfoRequest(localId: [uid]),
+ );
+
+ return response.users!.single;
+ }
+
+ /// Looks up a user by email.
+ Future getAccountInfoByEmail(
+ String email,
+ ) async {
+ assertIsEmail(email);
+
+ final response = await _accountsLookup(
+ auth1.GoogleCloudIdentitytoolkitV1GetAccountInfoRequest(email: [email]),
+ );
+
+ return response.users!.single;
+ }
+
+ /// Looks up a user by phone number.
+ Future
+ getAccountInfoByPhoneNumber(
+ String phoneNumber,
+ ) async {
+ assertIsPhoneNumber(phoneNumber);
+
+ final response = await _accountsLookup(
+ auth1.GoogleCloudIdentitytoolkitV1GetAccountInfoRequest(
+ phoneNumber: [phoneNumber],
+ ),
+ );
+
+ return response.users!.single;
+ }
+
+ Future
+ getAccountInfoByFederatedUid({
+ required String providerId,
+ required String rawId,
+ }) async {
+ if (providerId.isEmpty || rawId.isEmpty) {
+ throw FirebaseAuthAdminException(AuthClientErrorCode.invalidProviderId);
+ }
+
+ final response = await _accountsLookup(
+ auth1.GoogleCloudIdentitytoolkitV1GetAccountInfoRequest(
+ federatedUserId: [
+ auth1.GoogleCloudIdentitytoolkitV1FederatedUserIdentifier(
+ providerId: providerId,
+ rawId: rawId,
+ ),
+ ],
+ ),
+ );
+
+ return response.users!.single;
+ }
+
+ /// Looks up multiple users by their identifiers (uid, email, etc).
+ Future
+ getAccountInfoByIdentifiers(
+ List identifiers,
+ ) async {
+ if (identifiers.isEmpty) {
+ return auth1.GoogleCloudIdentitytoolkitV1GetAccountInfoResponse(
+ users: [],
+ );
+ } else if (identifiers.length > maxGetAccountsBatchSize) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.maximumUserCountExceeded,
+ '`identifiers` parameter must have <= $maxGetAccountsBatchSize entries.',
+ );
+ }
+
+ final request = auth1.GoogleCloudIdentitytoolkitV1GetAccountInfoRequest();
+
+ for (final id in identifiers) {
+ switch (id) {
+ case UidIdentifier():
+ final localIds = request.localId ?? [];
+ localIds.add(id.uid);
+ case EmailIdentifier():
+ final emails = request.email ?? [];
+ emails.add(id.email);
+ case PhoneIdentifier():
+ final phoneNumbers = request.phoneNumber ?? [];
+ phoneNumbers.add(id.phoneNumber);
+ case ProviderIdentifier():
+ final providerIds = request.federatedUserId ?? [];
+ providerIds.add(id.providerId);
+ }
+ }
+
+ // TODO handle tenants
+ return _httpClient.v1((client) => client.accounts.lookup(request));
+ }
+
+ /// Edits an existing user.
+ ///
+ /// - uid - The user to edit.
+ /// - properties - The properties to set on the user.
+ ///
+ /// Returns a [Future] that resolves when the operation completes
+ /// with the user id that was edited.
+ Future updateExistingAccount(
+ String uid,
+ UpdateRequest properties,
+ ) async {
+ assertIsUid(uid);
+
+ final providerToLink = properties.providerToLink;
+ if (providerToLink != null) {
+ if (providerToLink.providerId?.isNotEmpty ?? false) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ 'providerToLink.providerId of properties argument must be a non-empty string.',
+ );
+ }
+ if (providerToLink.uid?.isNotEmpty ?? false) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ 'providerToLink.uid of properties argument must be a non-empty string.',
+ );
+ }
+ }
+ final providersToUnlink = properties.providersToUnlink;
+ if (providersToUnlink != null) {
+ for (final provider in providersToUnlink) {
+ if (provider.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ 'providersToUnlink of properties argument must be a non-empty string.',
+ );
+ }
+ }
+ }
+
+ // For deleting some attributes, these values must be passed as Box(null).
+ final isPhotoDeleted =
+ properties.photoURL != null && properties.photoURL?.value == null;
+ final isDisplayNameDeleted =
+ properties.displayName != null && properties.displayName?.value == null;
+ final isPhoneNumberDeleted =
+ properties.phoneNumber != null && properties.phoneNumber?.value == null;
+
+ // They will be removed from the backend request and an additional parameter
+ // deleteAttribute: ['PHOTO_URL', 'DISPLAY_NAME']
+ // with an array of the parameter names to delete will be passed.
+ final deleteAttribute = [
+ if (isPhotoDeleted) 'PHOTO_URL',
+ if (isDisplayNameDeleted) 'DISPLAY_NAME',
+ ];
+
+ // Phone will be removed from the backend request and an additional parameter
+ // deleteProvider: ['phone'] with an array of providerIds (phone in this case),
+ // will be passed.
+ List? deleteProvider;
+ if (isPhoneNumberDeleted) deleteProvider = ['phone'];
+
+ final linkProviderUserInfo =
+ properties.providerToLink?.toProviderUserInfo();
+
+ final providerToUnlink = properties.providersToUnlink;
+ if (providerToUnlink != null) {
+ deleteProvider ??= [];
+ deleteProvider.addAll(providerToUnlink);
+ }
+
+ final mfa = properties.multiFactor?.toMfaInfo();
+
+ return _httpClient.v1((client) async {
+ final response = await client.accounts.update(
+ auth1.GoogleCloudIdentitytoolkitV1SetAccountInfoRequest(
+ captchaChallenge: null,
+ captchaResponse: null,
+ createdAt: null,
+ customAttributes: null,
+ delegatedProjectNumber: null,
+ deleteAttribute: deleteAttribute.isEmpty ? null : deleteAttribute,
+ deleteProvider: deleteProvider,
+ disableUser: properties.disabled,
+ // Will be null if deleted or set to null. "deleteAttribute" will take over
+ displayName: properties.displayName?.value,
+ email: properties.email,
+ emailVerified: properties.emailVerified,
+ idToken: null,
+ instanceId: null,
+ lastLoginAt: null,
+ linkProviderUserInfo: linkProviderUserInfo,
+ localId: null,
+ mfa: mfa,
+ oobCode: null,
+ password: properties.password,
+ // Will be null if deleted or set to null. "deleteProvider" will take over
+ phoneNumber: properties.phoneNumber?.value,
+ // Will be null if deleted or set to null. "deleteAttribute" will take over
+ photoUrl: properties.photoURL?.value,
+ provider: null,
+ returnSecureToken: null,
+ targetProjectId: null,
+ tenantId: null,
+ upgradeToFederatedLogin: null,
+ validSince: null,
+ ),
+ );
+
+ final localId = response.localId;
+ if (localId == null) {
+ throw FirebaseAuthAdminException(AuthClientErrorCode.userNotFound);
+ }
+
+ return localId;
+ });
+ }
+}
+
+class _AuthHttpClient {
+ _AuthHttpClient(this.app);
+
+ // TODO needs to send "owner" as bearer token when using the emulator
+ final FirebaseAdminApp app;
+
+ auth.AuthClient? _client;
+
+ Future _getClient() async {
+ return _client ??= await app.credential.getAuthClient([
+ auth3.IdentityToolkitApi.cloudPlatformScope,
+ auth3.IdentityToolkitApi.firebaseScope,
+ ]);
+ }
+
+ Future v1(
+ Future Function(auth1.IdentityToolkitApi client) fn,
+ ) {
+ return authGuard(
+ () async => fn(
+ auth1.IdentityToolkitApi(
+ await _getClient(),
+ rootUrl: app.authApiHost.toString(),
+ ),
+ ),
+ );
+ }
+
+ Future v2(
+ Future Function(auth2.IdentityToolkitApi client) fn,
+ ) async {
+ return authGuard(
+ () async => fn(
+ auth2.IdentityToolkitApi(
+ await _getClient(),
+ rootUrl: app.authApiHost.toString(),
+ ),
+ ),
+ );
+ }
+
+ Future v3(
+ Future Function(auth3.IdentityToolkitApi client) fn,
+ ) async {
+ return authGuard(
+ () async => fn(
+ auth3.IdentityToolkitApi(
+ await _getClient(),
+ rootUrl: app.authApiHost.toString(),
+ ),
+ ),
+ );
+ }
+}
diff --git a/packages/dart_firebase_admin/lib/src/auth/auth_api_request.ts b/packages/dart_firebase_admin/lib/src/auth/auth_api_request.ts
new file mode 100644
index 0000000..b3029d9
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/auth_api_request.ts
@@ -0,0 +1,2323 @@
+/*!
+ * @license
+ * Copyright 2017 Google Inc.
+ *
+ * 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.
+ */
+
+import * as validator from '../utils/validator';
+
+import { App } from '../app/index';
+import { FirebaseApp } from '../app/firebase-app';
+import { deepCopy, deepExtend } from '../utils/deep-copy';
+import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error';
+import {
+ ApiSettings, AuthorizedHttpClient, HttpRequestConfig, HttpError,
+} from '../utils/api-request';
+import * as utils from '../utils/index';
+
+import {
+ UserImportOptions, UserImportRecord, UserImportResult,
+ UserImportBuilder, AuthFactorInfo, convertMultiFactorInfoToServerFormat,
+} from './user-import-builder';
+import { ActionCodeSettings, ActionCodeSettingsBuilder } from './action-code-settings-builder';
+import { Tenant, TenantServerResponse, CreateTenantRequest, UpdateTenantRequest } from './tenant';
+import {
+ isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier,
+ UserIdentifier, UidIdentifier, EmailIdentifier,PhoneIdentifier, ProviderIdentifier,
+} from './identifier';
+import {
+ SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse,
+ OIDCConfigServerRequest, SAMLConfigServerRequest, CreateRequest, UpdateRequest,
+ OIDCAuthProviderConfig, SAMLAuthProviderConfig, OIDCUpdateAuthProviderRequest,
+ SAMLUpdateAuthProviderRequest
+} from './auth-config';
+import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config';
+
+/** Firebase Auth request header. */
+const FIREBASE_AUTH_HEADER = {
+ 'X-Client-Version': `Node/Admin/${utils.getSdkVersion()}`,
+};
+/** Firebase Auth request timeout duration in milliseconds. */
+const FIREBASE_AUTH_TIMEOUT = 25000;
+
+
+/** List of reserved claims which cannot be provided when creating a custom token. */
+export const RESERVED_CLAIMS = [
+ 'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', 'exp', 'iat',
+ 'iss', 'jti', 'nbf', 'nonce', 'sub', 'firebase',
+];
+
+/** List of supported email action request types. */
+export const EMAIL_ACTION_REQUEST_TYPES = [
+ 'PASSWORD_RESET', 'VERIFY_EMAIL', 'EMAIL_SIGNIN', 'VERIFY_AND_CHANGE_EMAIL',
+];
+
+/** Maximum allowed number of characters in the custom claims payload. */
+const MAX_CLAIMS_PAYLOAD_SIZE = 1000;
+
+/** Maximum allowed number of users to batch download at one time. */
+const MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE = 1000;
+
+/** Maximum allowed number of users to batch upload at one time. */
+const MAX_UPLOAD_ACCOUNT_BATCH_SIZE = 1000;
+
+/** Maximum allowed number of users to batch get at one time. */
+const MAX_GET_ACCOUNTS_BATCH_SIZE = 100;
+
+/** Maximum allowed number of users to batch delete at one time. */
+const MAX_DELETE_ACCOUNTS_BATCH_SIZE = 1000;
+
+/** Minimum allowed session cookie duration in seconds (5 minutes). */
+const MIN_SESSION_COOKIE_DURATION_SECS = 5 * 60;
+
+/** Maximum allowed session cookie duration in seconds (2 weeks). */
+const MAX_SESSION_COOKIE_DURATION_SECS = 14 * 24 * 60 * 60;
+
+/** Maximum allowed number of provider configurations to batch download at one time. */
+const MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE = 100;
+
+/** The Firebase Auth backend base URL format. */
+const FIREBASE_AUTH_BASE_URL_FORMAT =
+ 'https://identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}';
+
+/** Firebase Auth base URlLformat when using the auth emultor. */
+const FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT =
+ 'http://{host}/identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}';
+
+/** The Firebase Auth backend multi-tenancy base URL format. */
+const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace(
+ 'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}');
+
+/** Firebase Auth base URL format when using the auth emultor with multi-tenancy. */
+const FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT = FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT.replace(
+ 'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}');
+
+/** Maximum allowed number of tenants to download at one time. */
+const MAX_LIST_TENANT_PAGE_SIZE = 1000;
+
+
+/**
+ * Enum for the user write operation type.
+ */
+enum WriteOperationType {
+ Create = 'create',
+ Update = 'update',
+ Upload = 'upload',
+}
+
+
+/** Defines a base utility to help with resource URL construction. */
+class AuthResourceUrlBuilder {
+
+ protected urlFormat: string;
+ private projectId: string;
+
+ /**
+ * The resource URL builder constructor.
+ *
+ * @param projectId - The resource project ID.
+ * @param version - The endpoint API version.
+ * @constructor
+ */
+ constructor(protected app: App, protected version: string = 'v1') {
+ if (useEmulator()) {
+ this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, {
+ host: emulatorHost()
+ });
+ } else {
+ this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT;
+ }
+ }
+
+ /**
+ * Returns the resource URL corresponding to the provided parameters.
+ *
+ * @param api - The backend API name.
+ * @param params - The optional additional parameters to substitute in the
+ * URL path.
+ * @returns The corresponding resource URL.
+ */
+ public getUrl(api?: string, params?: object): Promise {
+ return this.getProjectId()
+ .then((projectId) => {
+ const baseParams = {
+ version: this.version,
+ projectId,
+ api: api || '',
+ };
+ const baseUrl = utils.formatString(this.urlFormat, baseParams);
+ // Substitute additional api related parameters.
+ return utils.formatString(baseUrl, params || {});
+ });
+ }
+
+ private getProjectId(): Promise {
+ if (this.projectId) {
+ return Promise.resolve(this.projectId);
+ }
+
+ return utils.findProjectId(this.app)
+ .then((projectId) => {
+ if (!validator.isNonEmptyString(projectId)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CREDENTIAL,
+ 'Failed to determine project ID for Auth. Initialize the '
+ + 'SDK with service account credentials or set project ID as an app option. '
+ + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.',
+ );
+ }
+
+ this.projectId = projectId;
+ return projectId;
+ });
+ }
+}
+
+
+/** Tenant aware resource builder utility. */
+class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder {
+ /**
+ * The tenant aware resource URL builder constructor.
+ *
+ * @param projectId - The resource project ID.
+ * @param version - The endpoint API version.
+ * @param tenantId - The tenant ID.
+ * @constructor
+ */
+ constructor(protected app: App, protected version: string, protected tenantId: string) {
+ super(app, version);
+ if (useEmulator()) {
+ this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT, {
+ host: emulatorHost()
+ });
+ } else {
+ this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT;
+ }
+ }
+
+ /**
+ * Returns the resource URL corresponding to the provided parameters.
+ *
+ * @param api - The backend API name.
+ * @param params - The optional additional parameters to substitute in the
+ * URL path.
+ * @returns The corresponding resource URL.
+ */
+ public getUrl(api?: string, params?: object): Promise {
+ return super.getUrl(api, params)
+ .then((url) => {
+ return utils.formatString(url, { tenantId: this.tenantId });
+ });
+ }
+}
+
+/**
+ * Auth-specific HTTP client which uses the special "owner" token
+ * when communicating with the Auth Emulator.
+ */
+class AuthHttpClient extends AuthorizedHttpClient {
+
+ protected getToken(): Promise {
+ if (useEmulator()) {
+ return Promise.resolve('owner');
+ }
+
+ return super.getToken();
+ }
+
+}
+
+/**
+ * Validates an AuthFactorInfo object. All unsupported parameters
+ * are removed from the original request. If an invalid field is passed
+ * an error is thrown.
+ *
+ * @param request - The AuthFactorInfo request object.
+ */
+function validateAuthFactorInfo(request: AuthFactorInfo): void {
+ const validKeys = {
+ mfaEnrollmentId: true,
+ displayName: true,
+ phoneInfo: true,
+ enrolledAt: true,
+ };
+ // Remove unsupported keys from the original request.
+ for (const key in request) {
+ if (!(key in validKeys)) {
+ delete request[key];
+ }
+ }
+ // No enrollment ID is available for signupNewUser. Use another identifier.
+ const authFactorInfoIdentifier =
+ request.mfaEnrollmentId || request.phoneInfo || JSON.stringify(request);
+ // Enrollment uid may or may not be specified for update operations.
+ if (typeof request.mfaEnrollmentId !== 'undefined' &&
+ !validator.isNonEmptyString(request.mfaEnrollmentId)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_UID,
+ 'The second factor "uid" must be a valid non-empty string.',
+ );
+ }
+ if (typeof request.displayName !== 'undefined' &&
+ !validator.isString(request.displayName)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_DISPLAY_NAME,
+ `The second factor "displayName" for "${authFactorInfoIdentifier}" must be a valid string.`,
+ );
+ }
+ // enrolledAt must be a valid UTC date string.
+ if (typeof request.enrolledAt !== 'undefined' &&
+ !validator.isISODateString(request.enrolledAt)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ENROLLMENT_TIME,
+ `The second factor "enrollmentTime" for "${authFactorInfoIdentifier}" must be a valid ` +
+ 'UTC date string.');
+ }
+ // Validate required fields depending on second factor type.
+ if (typeof request.phoneInfo !== 'undefined') {
+ // phoneNumber should be a string and a valid phone number.
+ if (!validator.isPhoneNumber(request.phoneInfo)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_PHONE_NUMBER,
+ `The second factor "phoneNumber" for "${authFactorInfoIdentifier}" must be a non-empty ` +
+ 'E.164 standard compliant identifier string.');
+ }
+ } else {
+ // Invalid second factor. For example, a phone second factor may have been provided without
+ // a phone number. A TOTP based second factor may require a secret key, etc.
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ENROLLED_FACTORS,
+ 'MFAInfo object provided is invalid.');
+ }
+}
+
+
+/**
+ * Validates a providerUserInfo object. All unsupported parameters
+ * are removed from the original request. If an invalid field is passed
+ * an error is thrown.
+ *
+ * @param request - The providerUserInfo request object.
+ */
+function validateProviderUserInfo(request: any): void {
+ const validKeys = {
+ rawId: true,
+ providerId: true,
+ email: true,
+ displayName: true,
+ photoUrl: true,
+ };
+ // Remove invalid keys from original request.
+ for (const key in request) {
+ if (!(key in validKeys)) {
+ delete request[key];
+ }
+ }
+ if (!validator.isNonEmptyString(request.providerId)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID);
+ }
+ if (typeof request.displayName !== 'undefined' &&
+ typeof request.displayName !== 'string') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_DISPLAY_NAME,
+ `The provider "displayName" for "${request.providerId}" must be a valid string.`,
+ );
+ }
+ if (!validator.isNonEmptyString(request.rawId)) {
+ // This is called localId on the backend but the developer specifies this as
+ // uid externally. So the error message should use the client facing name.
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_UID,
+ `The provider "uid" for "${request.providerId}" must be a valid non-empty string.`,
+ );
+ }
+ // email should be a string and a valid email.
+ if (typeof request.email !== 'undefined' && !validator.isEmail(request.email)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_EMAIL,
+ `The provider "email" for "${request.providerId}" must be a valid email string.`,
+ );
+ }
+ // photoUrl should be a URL.
+ if (typeof request.photoUrl !== 'undefined' &&
+ !validator.isURL(request.photoUrl)) {
+ // This is called photoUrl on the backend but the developer specifies this as
+ // photoURL externally. So the error message should use the client facing name.
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_PHOTO_URL,
+ `The provider "photoURL" for "${request.providerId}" must be a valid URL string.`,
+ );
+ }
+}
+
+
+/**
+ * Validates a create/edit request object. All unsupported parameters
+ * are removed from the original request. If an invalid field is passed
+ * an error is thrown.
+ *
+ * @param request - The create/edit request object.
+ * @param writeOperationType - The write operation type.
+ */
+function validateCreateEditRequest(request: any, writeOperationType: WriteOperationType): void {
+ const uploadAccountRequest = writeOperationType === WriteOperationType.Upload;
+ // Hash set of whitelisted parameters.
+ const validKeys = {
+ displayName: true,
+ localId: true,
+ email: true,
+ password: true,
+ rawPassword: true,
+ emailVerified: true,
+ photoUrl: true,
+ disabled: true,
+ disableUser: true,
+ deleteAttribute: true,
+ deleteProvider: true,
+ sanityCheck: true,
+ phoneNumber: true,
+ customAttributes: true,
+ validSince: true,
+ // Pass linkProviderUserInfo only for updates (i.e. not for uploads.)
+ linkProviderUserInfo: !uploadAccountRequest,
+ // Pass tenantId only for uploadAccount requests.
+ tenantId: uploadAccountRequest,
+ passwordHash: uploadAccountRequest,
+ salt: uploadAccountRequest,
+ createdAt: uploadAccountRequest,
+ lastLoginAt: uploadAccountRequest,
+ providerUserInfo: uploadAccountRequest,
+ mfaInfo: uploadAccountRequest,
+ // Only for non-uploadAccount requests.
+ mfa: !uploadAccountRequest,
+ };
+ // Remove invalid keys from original request.
+ for (const key in request) {
+ if (!(key in validKeys)) {
+ delete request[key];
+ }
+ }
+ if (typeof request.tenantId !== 'undefined' &&
+ !validator.isNonEmptyString(request.tenantId)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID);
+ }
+ // For any invalid parameter, use the external key name in the error description.
+ // displayName should be a string.
+ if (typeof request.displayName !== 'undefined' &&
+ !validator.isString(request.displayName)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_DISPLAY_NAME);
+ }
+ if ((typeof request.localId !== 'undefined' || uploadAccountRequest) &&
+ !validator.isUid(request.localId)) {
+ // This is called localId on the backend but the developer specifies this as
+ // uid externally. So the error message should use the client facing name.
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID);
+ }
+ // email should be a string and a valid email.
+ if (typeof request.email !== 'undefined' && !validator.isEmail(request.email)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL);
+ }
+ // phoneNumber should be a string and a valid phone number.
+ if (typeof request.phoneNumber !== 'undefined' &&
+ !validator.isPhoneNumber(request.phoneNumber)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER);
+ }
+ // password should be a string and a minimum of 6 chars.
+ if (typeof request.password !== 'undefined' &&
+ !validator.isPassword(request.password)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD);
+ }
+ // rawPassword should be a string and a minimum of 6 chars.
+ if (typeof request.rawPassword !== 'undefined' &&
+ !validator.isPassword(request.rawPassword)) {
+ // This is called rawPassword on the backend but the developer specifies this as
+ // password externally. So the error message should use the client facing name.
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD);
+ }
+ // emailVerified should be a boolean.
+ if (typeof request.emailVerified !== 'undefined' &&
+ typeof request.emailVerified !== 'boolean') {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL_VERIFIED);
+ }
+ // photoUrl should be a URL.
+ if (typeof request.photoUrl !== 'undefined' &&
+ !validator.isURL(request.photoUrl)) {
+ // This is called photoUrl on the backend but the developer specifies this as
+ // photoURL externally. So the error message should use the client facing name.
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PHOTO_URL);
+ }
+ // disabled should be a boolean.
+ if (typeof request.disabled !== 'undefined' &&
+ typeof request.disabled !== 'boolean') {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD);
+ }
+ // validSince should be a number.
+ if (typeof request.validSince !== 'undefined' &&
+ !validator.isNumber(request.validSince)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_TOKENS_VALID_AFTER_TIME);
+ }
+ // createdAt should be a number.
+ if (typeof request.createdAt !== 'undefined' &&
+ !validator.isNumber(request.createdAt)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_CREATION_TIME);
+ }
+ // lastSignInAt should be a number.
+ if (typeof request.lastLoginAt !== 'undefined' &&
+ !validator.isNumber(request.lastLoginAt)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_LAST_SIGN_IN_TIME);
+ }
+ // disableUser should be a boolean.
+ if (typeof request.disableUser !== 'undefined' &&
+ typeof request.disableUser !== 'boolean') {
+ // This is called disableUser on the backend but the developer specifies this as
+ // disabled externally. So the error message should use the client facing name.
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD);
+ }
+ // customAttributes should be stringified JSON with no blacklisted claims.
+ // The payload should not exceed 1KB.
+ if (typeof request.customAttributes !== 'undefined') {
+ let developerClaims: object;
+ try {
+ developerClaims = JSON.parse(request.customAttributes);
+ } catch (error) {
+ // JSON parsing error. This should never happen as we stringify the claims internally.
+ // However, we still need to check since setAccountInfo via edit requests could pass
+ // this field.
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_CLAIMS, error.message);
+ }
+ const invalidClaims: string[] = [];
+ // Check for any invalid claims.
+ RESERVED_CLAIMS.forEach((blacklistedClaim) => {
+ if (Object.prototype.hasOwnProperty.call(developerClaims, blacklistedClaim)) {
+ invalidClaims.push(blacklistedClaim);
+ }
+ });
+ // Throw an error if an invalid claim is detected.
+ if (invalidClaims.length > 0) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.FORBIDDEN_CLAIM,
+ invalidClaims.length > 1 ?
+ `Developer claims "${invalidClaims.join('", "')}" are reserved and cannot be specified.` :
+ `Developer claim "${invalidClaims[0]}" is reserved and cannot be specified.`,
+ );
+ }
+ // Check claims payload does not exceed maxmimum size.
+ if (request.customAttributes.length > MAX_CLAIMS_PAYLOAD_SIZE) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.CLAIMS_TOO_LARGE,
+ `Developer claims payload should not exceed ${MAX_CLAIMS_PAYLOAD_SIZE} characters.`,
+ );
+ }
+ }
+ // passwordHash has to be a base64 encoded string.
+ if (typeof request.passwordHash !== 'undefined' &&
+ !validator.isString(request.passwordHash)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH);
+ }
+ // salt has to be a base64 encoded string.
+ if (typeof request.salt !== 'undefined' &&
+ !validator.isString(request.salt)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT);
+ }
+ // providerUserInfo has to be an array of valid UserInfo requests.
+ if (typeof request.providerUserInfo !== 'undefined' &&
+ !validator.isArray(request.providerUserInfo)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_DATA);
+ } else if (validator.isArray(request.providerUserInfo)) {
+ request.providerUserInfo.forEach((providerUserInfoEntry: any) => {
+ validateProviderUserInfo(providerUserInfoEntry);
+ });
+ }
+
+ // linkProviderUserInfo must be a (single) UserProvider value.
+ if (typeof request.linkProviderUserInfo !== 'undefined') {
+ validateProviderUserInfo(request.linkProviderUserInfo);
+ }
+
+ // mfaInfo is used for importUsers.
+ // mfa.enrollments is used for setAccountInfo.
+ // enrollments has to be an array of valid AuthFactorInfo requests.
+ let enrollments: AuthFactorInfo[] | null = null;
+ if (request.mfaInfo) {
+ enrollments = request.mfaInfo;
+ } else if (request.mfa && request.mfa.enrollments) {
+ enrollments = request.mfa.enrollments;
+ }
+ if (enrollments) {
+ if (!validator.isArray(enrollments)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ENROLLED_FACTORS);
+ }
+ enrollments.forEach((authFactorInfoEntry: AuthFactorInfo) => {
+ validateAuthFactorInfo(authFactorInfoEntry);
+ });
+ }
+}
+
+
+/**
+ * Instantiates the createSessionCookie endpoint settings.
+ *
+ * @internal
+ */
+export const FIREBASE_AUTH_CREATE_SESSION_COOKIE =
+ new ApiSettings(':createSessionCookie', 'POST')
+ // Set request validator.
+ .setRequestValidator((request: any) => {
+ // Validate the ID token is a non-empty string.
+ if (!validator.isNonEmptyString(request.idToken)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN);
+ }
+ // Validate the custom session cookie duration.
+ if (!validator.isNumber(request.validDuration) ||
+ request.validDuration < MIN_SESSION_COOKIE_DURATION_SECS ||
+ request.validDuration > MAX_SESSION_COOKIE_DURATION_SECS) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION);
+ }
+ })
+ // Set response validator.
+ .setResponseValidator((response: any) => {
+ // Response should always contain the session cookie.
+ if (!validator.isNonEmptyString(response.sessionCookie)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR);
+ }
+ });
+
+
+/**
+ * Instantiates the uploadAccount endpoint settings.
+ *
+ * @internal
+ */
+export const FIREBASE_AUTH_UPLOAD_ACCOUNT = new ApiSettings('/accounts:batchCreate', 'POST');
+
+
+/**
+ * Instantiates the downloadAccount endpoint settings.
+ *
+ * @internal
+ */
+export const FIREBASE_AUTH_DOWNLOAD_ACCOUNT = new ApiSettings('/accounts:batchGet', 'GET')
+ // Set request validator.
+ .setRequestValidator((request: any) => {
+ // Validate next page token.
+ if (typeof request.nextPageToken !== 'undefined' &&
+ !validator.isNonEmptyString(request.nextPageToken)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PAGE_TOKEN);
+ }
+ // Validate max results.
+ if (!validator.isNumber(request.maxResults) ||
+ request.maxResults <= 0 ||
+ request.maxResults > MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'Required "maxResults" must be a positive integer that does not exceed ' +
+ `${MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE}.`,
+ );
+ }
+ });
+
+interface GetAccountInfoRequest {
+ localId?: string[];
+ email?: string[];
+ phoneNumber?: string[];
+ federatedUserId?: Array<{
+ providerId: string;
+ rawId: string;
+ }>;
+}
+
+/**
+ * Instantiates the getAccountInfo endpoint settings.
+ *
+ * @internal
+ */
+export const FIREBASE_AUTH_GET_ACCOUNT_INFO = new ApiSettings('/accounts:lookup', 'POST')
+ // Set request validator.
+ .setRequestValidator((request: GetAccountInfoRequest) => {
+ if (!request.localId && !request.email && !request.phoneNumber && !request.federatedUserId) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Server request is missing user identifier');
+ }
+ })
+ // Set response validator.
+ .setResponseValidator((response: any) => {
+ if (!response.users || !response.users.length) {
+ throw new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND);
+ }
+ });
+
+/**
+ * Instantiates the getAccountInfo endpoint settings for use when fetching info
+ * for multiple accounts.
+ *
+ * @internal
+ */
+export const FIREBASE_AUTH_GET_ACCOUNTS_INFO = new ApiSettings('/accounts:lookup', 'POST')
+ // Set request validator.
+ .setRequestValidator((request: GetAccountInfoRequest) => {
+ if (!request.localId && !request.email && !request.phoneNumber && !request.federatedUserId) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Server request is missing user identifier');
+ }
+ });
+
+
+/**
+ * Instantiates the deleteAccount endpoint settings.
+ *
+ * @internal
+ */
+export const FIREBASE_AUTH_DELETE_ACCOUNT = new ApiSettings('/accounts:delete', 'POST')
+ // Set request validator.
+ .setRequestValidator((request: any) => {
+ if (!request.localId) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Server request is missing user identifier');
+ }
+ });
+
+interface BatchDeleteAccountsRequest {
+ localIds?: string[];
+ force?: boolean;
+}
+
+interface BatchDeleteErrorInfo {
+ index?: number;
+ localId?: string;
+ message?: string;
+}
+
+export interface BatchDeleteAccountsResponse {
+ errors?: BatchDeleteErrorInfo[];
+}
+
+/**
+ * @internal
+ */
+export const FIREBASE_AUTH_BATCH_DELETE_ACCOUNTS = new ApiSettings('/accounts:batchDelete', 'POST')
+ .setRequestValidator((request: BatchDeleteAccountsRequest) => {
+ if (!request.localIds) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Server request is missing user identifiers');
+ }
+ if (typeof request.force === 'undefined' || request.force !== true) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Server request is missing force=true field');
+ }
+ })
+ .setResponseValidator((response: BatchDeleteAccountsResponse) => {
+ const errors = response.errors || [];
+ errors.forEach((batchDeleteErrorInfo) => {
+ if (typeof batchDeleteErrorInfo.index === 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Server BatchDeleteAccountResponse is missing an errors.index field');
+ }
+ if (!batchDeleteErrorInfo.localId) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Server BatchDeleteAccountResponse is missing an errors.localId field');
+ }
+ // Allow the (error) message to be missing/undef.
+ });
+ });
+
+/**
+ * Instantiates the setAccountInfo endpoint settings for updating existing accounts.
+ *
+ * @internal
+ */
+export const FIREBASE_AUTH_SET_ACCOUNT_INFO = new ApiSettings('/accounts:update', 'POST')
+ // Set request validator.
+ .setRequestValidator((request: any) => {
+ // localId is a required parameter.
+ if (typeof request.localId === 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Server request is missing user identifier');
+ }
+ // Throw error when tenantId is passed in POST body.
+ if (typeof request.tenantId !== 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"tenantId" is an invalid "UpdateRequest" property.');
+ }
+ validateCreateEditRequest(request, WriteOperationType.Update);
+ })
+ // Set response validator.
+ .setResponseValidator((response: any) => {
+ // If the localId is not returned, then the request failed.
+ if (!response.localId) {
+ throw new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND);
+ }
+ });
+
+/**
+ * Instantiates the signupNewUser endpoint settings for creating a new user with or without
+ * uid being specified. The backend will create a new one if not provided and return it.
+ *
+ * @internal
+ */
+export const FIREBASE_AUTH_SIGN_UP_NEW_USER = new ApiSettings('/accounts', 'POST')
+ // Set request validator.
+ .setRequestValidator((request: any) => {
+ // signupNewUser does not support customAttributes.
+ if (typeof request.customAttributes !== 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"customAttributes" cannot be set when creating a new user.',
+ );
+ }
+ // signupNewUser does not support validSince.
+ if (typeof request.validSince !== 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"validSince" cannot be set when creating a new user.',
+ );
+ }
+ // Throw error when tenantId is passed in POST body.
+ if (typeof request.tenantId !== 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"tenantId" is an invalid "CreateRequest" property.');
+ }
+ validateCreateEditRequest(request, WriteOperationType.Create);
+ })
+ // Set response validator.
+ .setResponseValidator((response: any) => {
+ // If the localId is not returned, then the request failed.
+ if (!response.localId) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to create new user');
+ }
+ });
+
+const FIREBASE_AUTH_GET_OOB_CODE = new ApiSettings('/accounts:sendOobCode', 'POST')
+ // Set request validator.
+ .setRequestValidator((request: any) => {
+ if (!validator.isEmail(request.email)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_EMAIL,
+ );
+ }
+ if (typeof request.newEmail !== 'undefined' && !validator.isEmail(request.newEmail)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_NEW_EMAIL,
+ );
+ }
+ if (EMAIL_ACTION_REQUEST_TYPES.indexOf(request.requestType) === -1) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ `"${request.requestType}" is not a supported email action request type.`,
+ );
+ }
+ })
+ // Set response validator.
+ .setResponseValidator((response: any) => {
+ // If the oobLink is not returned, then the request failed.
+ if (!response.oobLink) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to create the email action link');
+ }
+ });
+
+/**
+ * Instantiates the retrieve OIDC configuration endpoint settings.
+ *
+ * @internal
+ */
+const GET_OAUTH_IDP_CONFIG = new ApiSettings('/oauthIdpConfigs/{providerId}', 'GET')
+ // Set response validator.
+ .setResponseValidator((response: any) => {
+ // Response should always contain the OIDC provider resource name.
+ if (!validator.isNonEmptyString(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to get OIDC configuration',
+ );
+ }
+ });
+
+/**
+ * Instantiates the delete OIDC configuration endpoint settings.
+ *
+ * @internal
+ */
+const DELETE_OAUTH_IDP_CONFIG = new ApiSettings('/oauthIdpConfigs/{providerId}', 'DELETE');
+
+/**
+ * Instantiates the create OIDC configuration endpoint settings.
+ *
+ * @internal
+ */
+const CREATE_OAUTH_IDP_CONFIG = new ApiSettings('/oauthIdpConfigs?oauthIdpConfigId={providerId}', 'POST')
+ // Set response validator.
+ .setResponseValidator((response: any) => {
+ // Response should always contain the OIDC provider resource name.
+ if (!validator.isNonEmptyString(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to create new OIDC configuration',
+ );
+ }
+ });
+
+/**
+ * Instantiates the update OIDC configuration endpoint settings.
+ *
+ * @internal
+ */
+const UPDATE_OAUTH_IDP_CONFIG = new ApiSettings('/oauthIdpConfigs/{providerId}?updateMask={updateMask}', 'PATCH')
+ // Set response validator.
+ .setResponseValidator((response: any) => {
+ // Response should always contain the configuration resource name.
+ if (!validator.isNonEmptyString(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to update OIDC configuration',
+ );
+ }
+ });
+
+/**
+ * Instantiates the list OIDC configuration endpoint settings.
+ *
+ * @internal
+ */
+const LIST_OAUTH_IDP_CONFIGS = new ApiSettings('/oauthIdpConfigs', 'GET')
+ // Set request validator.
+ .setRequestValidator((request: any) => {
+ // Validate next page token.
+ if (typeof request.pageToken !== 'undefined' &&
+ !validator.isNonEmptyString(request.pageToken)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PAGE_TOKEN);
+ }
+ // Validate max results.
+ if (!validator.isNumber(request.pageSize) ||
+ request.pageSize <= 0 ||
+ request.pageSize > MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'Required "maxResults" must be a positive integer that does not exceed ' +
+ `${MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE}.`,
+ );
+ }
+ });
+
+/**
+ * Instantiates the retrieve SAML configuration endpoint settings.
+ *
+ * @internal
+ */
+const GET_INBOUND_SAML_CONFIG = new ApiSettings('/inboundSamlConfigs/{providerId}', 'GET')
+ // Set response validator.
+ .setResponseValidator((response: any) => {
+ // Response should always contain the SAML provider resource name.
+ if (!validator.isNonEmptyString(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to get SAML configuration',
+ );
+ }
+ });
+
+/**
+ * Instantiates the delete SAML configuration endpoint settings.
+ *
+ * @internal
+ */
+const DELETE_INBOUND_SAML_CONFIG = new ApiSettings('/inboundSamlConfigs/{providerId}', 'DELETE');
+
+/**
+ * Instantiates the create SAML configuration endpoint settings.
+ *
+ * @internal
+ */
+const CREATE_INBOUND_SAML_CONFIG = new ApiSettings('/inboundSamlConfigs?inboundSamlConfigId={providerId}', 'POST')
+ // Set response validator.
+ .setResponseValidator((response: any) => {
+ // Response should always contain the SAML provider resource name.
+ if (!validator.isNonEmptyString(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to create new SAML configuration',
+ );
+ }
+ });
+
+/**
+ * Instantiates the update SAML configuration endpoint settings.
+ *
+ * @internal
+ */
+const UPDATE_INBOUND_SAML_CONFIG = new ApiSettings('/inboundSamlConfigs/{providerId}?updateMask={updateMask}', 'PATCH')
+ // Set response validator.
+ .setResponseValidator((response: any) => {
+ // Response should always contain the configuration resource name.
+ if (!validator.isNonEmptyString(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to update SAML configuration',
+ );
+ }
+ });
+
+/**
+ * Instantiates the list SAML configuration endpoint settings.
+ *
+ * @internal
+ */
+const LIST_INBOUND_SAML_CONFIGS = new ApiSettings('/inboundSamlConfigs', 'GET')
+ // Set request validator.
+ .setRequestValidator((request: any) => {
+ // Validate next page token.
+ if (typeof request.pageToken !== 'undefined' &&
+ !validator.isNonEmptyString(request.pageToken)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PAGE_TOKEN);
+ }
+ // Validate max results.
+ if (!validator.isNumber(request.pageSize) ||
+ request.pageSize <= 0 ||
+ request.pageSize > MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'Required "maxResults" must be a positive integer that does not exceed ' +
+ `${MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE}.`,
+ );
+ }
+ });
+
+/**
+ * Class that provides the mechanism to send requests to the Firebase Auth backend endpoints.
+ *
+ * @internal
+ */
+export abstract class AbstractAuthRequestHandler {
+
+ protected readonly httpClient: AuthorizedHttpClient;
+ private authUrlBuilder: AuthResourceUrlBuilder;
+ private projectConfigUrlBuilder: AuthResourceUrlBuilder;
+
+ /**
+ * @param response - The response to check for errors.
+ * @returns The error code if present; null otherwise.
+ */
+ private static getErrorCode(response: any): string | null {
+ return (validator.isNonNullObject(response) && response.error && response.error.message) || null;
+ }
+
+ private static addUidToRequest(id: UidIdentifier, request: GetAccountInfoRequest): GetAccountInfoRequest {
+ if (!validator.isUid(id.uid)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID);
+ }
+ request.localId ? request.localId.push(id.uid) : request.localId = [id.uid];
+ return request;
+ }
+
+ private static addEmailToRequest(id: EmailIdentifier, request: GetAccountInfoRequest): GetAccountInfoRequest {
+ if (!validator.isEmail(id.email)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL);
+ }
+ request.email ? request.email.push(id.email) : request.email = [id.email];
+ return request;
+ }
+
+ private static addPhoneToRequest(id: PhoneIdentifier, request: GetAccountInfoRequest): GetAccountInfoRequest {
+ if (!validator.isPhoneNumber(id.phoneNumber)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER);
+ }
+ request.phoneNumber ? request.phoneNumber.push(id.phoneNumber) : request.phoneNumber = [id.phoneNumber];
+ return request;
+ }
+
+ private static addProviderToRequest(id: ProviderIdentifier, request: GetAccountInfoRequest): GetAccountInfoRequest {
+ if (!validator.isNonEmptyString(id.providerId)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID);
+ }
+ if (!validator.isNonEmptyString(id.providerUid)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_UID);
+ }
+ const federatedUserId = {
+ providerId: id.providerId,
+ rawId: id.providerUid,
+ };
+ request.federatedUserId
+ ? request.federatedUserId.push(federatedUserId)
+ : request.federatedUserId = [federatedUserId];
+ return request;
+ }
+
+ /**
+ * @param app - The app used to fetch access tokens to sign API requests.
+ * @constructor
+ */
+ constructor(protected readonly app: App) {
+ if (typeof app !== 'object' || app === null || !('options' in app)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'First argument passed to admin.auth() must be a valid Firebase app instance.',
+ );
+ }
+
+ this.httpClient = new AuthHttpClient(app as FirebaseApp);
+ }
+
+ /**
+ * Creates a new Firebase session cookie with the specified duration that can be used for
+ * session management (set as a server side session cookie with custom cookie policy).
+ * The session cookie JWT will have the same payload claims as the provided ID token.
+ *
+ * @param idToken - The Firebase ID token to exchange for a session cookie.
+ * @param expiresIn - The session cookie duration in milliseconds.
+ *
+ * @returns A promise that resolves on success with the created session cookie.
+ */
+ public createSessionCookie(idToken: string, expiresIn: number): Promise {
+ const request = {
+ idToken,
+ // Convert to seconds.
+ validDuration: expiresIn / 1000,
+ };
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_CREATE_SESSION_COOKIE, request)
+ .then((response: any) => response.sessionCookie);
+ }
+
+ /**
+ * Looks up a user by uid.
+ *
+ * @param uid - The uid of the user to lookup.
+ * @returns A promise that resolves with the user information.
+ */
+ public getAccountInfoByUid(uid: string): Promise {
+ if (!validator.isUid(uid)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_UID));
+ }
+
+ const request = {
+ localId: [uid],
+ };
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNT_INFO, request);
+ }
+
+ /**
+ * Looks up a user by email.
+ *
+ * @param email - The email of the user to lookup.
+ * @returns A promise that resolves with the user information.
+ */
+ public getAccountInfoByEmail(email: string): Promise {
+ if (!validator.isEmail(email)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL));
+ }
+
+ const request = {
+ email: [email],
+ };
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNT_INFO, request);
+ }
+
+ /**
+ * Looks up a user by phone number.
+ *
+ * @param phoneNumber - The phone number of the user to lookup.
+ * @returns A promise that resolves with the user information.
+ */
+ public getAccountInfoByPhoneNumber(phoneNumber: string): Promise {
+ if (!validator.isPhoneNumber(phoneNumber)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER));
+ }
+
+ const request = {
+ phoneNumber: [phoneNumber],
+ };
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNT_INFO, request);
+ }
+
+ public getAccountInfoByFederatedUid(providerId: string, rawId: string): Promise {
+ if (!validator.isNonEmptyString(providerId) || !validator.isNonEmptyString(rawId)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID);
+ }
+
+ const request = {
+ federatedUserId: [{
+ providerId,
+ rawId,
+ }],
+ };
+
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNT_INFO, request);
+ }
+
+ /**
+ * Looks up multiple users by their identifiers (uid, email, etc).
+ *
+ * @param identifiers - The identifiers indicating the users
+ * to be looked up. Must have <= 100 entries.
+ * @param A - promise that resolves with the set of successfully
+ * looked up users. Possibly empty if no users were looked up.
+ */
+ public getAccountInfoByIdentifiers(identifiers: UserIdentifier[]): Promise {
+ if (identifiers.length === 0) {
+ return Promise.resolve({ users: [] });
+ } else if (identifiers.length > MAX_GET_ACCOUNTS_BATCH_SIZE) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED,
+ '`identifiers` parameter must have <= ' + MAX_GET_ACCOUNTS_BATCH_SIZE + ' entries.');
+ }
+
+ let request: GetAccountInfoRequest = {};
+
+ for (const id of identifiers) {
+ if (isUidIdentifier(id)) {
+ request = AbstractAuthRequestHandler.addUidToRequest(id, request);
+ } else if (isEmailIdentifier(id)) {
+ request = AbstractAuthRequestHandler.addEmailToRequest(id, request);
+ } else if (isPhoneIdentifier(id)) {
+ request = AbstractAuthRequestHandler.addPhoneToRequest(id, request);
+ } else if (isProviderIdentifier(id)) {
+ request = AbstractAuthRequestHandler.addProviderToRequest(id, request);
+ } else {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'Unrecognized identifier: ' + id);
+ }
+ }
+
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNTS_INFO, request);
+ }
+
+ /**
+ * Exports the users (single batch only) with a size of maxResults and starting from
+ * the offset as specified by pageToken.
+ *
+ * @param maxResults - The page size, 1000 if undefined. This is also the maximum
+ * allowed limit.
+ * @param pageToken - The next page token. If not specified, returns users starting
+ * without any offset. Users are returned in the order they were created from oldest to
+ * newest, relative to the page token offset.
+ * @returns A promise that resolves with the current batch of downloaded
+ * users and the next page token if available. For the last page, an empty list of users
+ * and no page token are returned.
+ */
+ public downloadAccount(
+ maxResults: number = MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE,
+ pageToken?: string): Promise<{users: object[]; nextPageToken?: string}> {
+ // Construct request.
+ const request = {
+ maxResults,
+ nextPageToken: pageToken,
+ };
+ // Remove next page token if not provided.
+ if (typeof request.nextPageToken === 'undefined') {
+ delete request.nextPageToken;
+ }
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_DOWNLOAD_ACCOUNT, request)
+ .then((response: any) => {
+ // No more users available.
+ if (!response.users) {
+ response.users = [];
+ }
+ return response as {users: object[]; nextPageToken?: string};
+ });
+ }
+
+ /**
+ * Imports the list of users provided to Firebase Auth. This is useful when
+ * migrating from an external authentication system without having to use the Firebase CLI SDK.
+ * At most, 1000 users are allowed to be imported one at a time.
+ * When importing a list of password users, UserImportOptions are required to be specified.
+ *
+ * @param users - The list of user records to import to Firebase Auth.
+ * @param options - The user import options, required when the users provided
+ * include password credentials.
+ * @returns A promise that resolves when the operation completes
+ * with the result of the import. This includes the number of successful imports, the number
+ * of failed uploads and their corresponding errors.
+ */
+ public uploadAccount(
+ users: UserImportRecord[], options?: UserImportOptions): Promise {
+ // This will throw if any error is detected in the hash options.
+ // For errors in the list of users, this will not throw and will report the errors and the
+ // corresponding user index in the user import generated response below.
+ // No need to validate raw request or raw response as this is done in UserImportBuilder.
+ const userImportBuilder = new UserImportBuilder(users, options, (userRequest: any) => {
+ // Pass true to validate the uploadAccount specific fields.
+ validateCreateEditRequest(userRequest, WriteOperationType.Upload);
+ });
+ const request = userImportBuilder.buildRequest();
+ // Fail quickly if more users than allowed are to be imported.
+ if (validator.isArray(users) && users.length > MAX_UPLOAD_ACCOUNT_BATCH_SIZE) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED,
+ `A maximum of ${MAX_UPLOAD_ACCOUNT_BATCH_SIZE} users can be imported at once.`,
+ );
+ }
+ // If no remaining user in request after client side processing, there is no need
+ // to send the request to the server.
+ if (!request.users || request.users.length === 0) {
+ return Promise.resolve(userImportBuilder.buildResponse([]));
+ }
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_UPLOAD_ACCOUNT, request)
+ .then((response: any) => {
+ // No error object is returned if no error encountered.
+ const failedUploads = (response.error || []) as Array<{index: number; message: string}>;
+ // Rewrite response as UserImportResult and re-insert client previously detected errors.
+ return userImportBuilder.buildResponse(failedUploads);
+ });
+ }
+
+ /**
+ * Deletes an account identified by a uid.
+ *
+ * @param uid - The uid of the user to delete.
+ * @returns A promise that resolves when the user is deleted.
+ */
+ public deleteAccount(uid: string): Promise {
+ if (!validator.isUid(uid)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_UID));
+ }
+
+ const request = {
+ localId: uid,
+ };
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_DELETE_ACCOUNT, request);
+ }
+
+ public deleteAccounts(uids: string[], force: boolean): Promise {
+ if (uids.length === 0) {
+ return Promise.resolve({});
+ } else if (uids.length > MAX_DELETE_ACCOUNTS_BATCH_SIZE) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED,
+ '`uids` parameter must have <= ' + MAX_DELETE_ACCOUNTS_BATCH_SIZE + ' entries.');
+ }
+
+ const request: BatchDeleteAccountsRequest = {
+ localIds: [],
+ force,
+ };
+
+ uids.forEach((uid) => {
+ if (!validator.isUid(uid)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID);
+ }
+ request.localIds!.push(uid);
+ });
+
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_BATCH_DELETE_ACCOUNTS, request);
+ }
+
+ /**
+ * Sets additional developer claims on an existing user identified by provided UID.
+ *
+ * @param uid - The user to edit.
+ * @param customUserClaims - The developer claims to set.
+ * @returns A promise that resolves when the operation completes
+ * with the user id that was edited.
+ */
+ public setCustomUserClaims(uid: string, customUserClaims: object | null): Promise {
+ // Validate user UID.
+ if (!validator.isUid(uid)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_UID));
+ } else if (!validator.isObject(customUserClaims)) {
+ return Promise.reject(
+ new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'CustomUserClaims argument must be an object or null.',
+ ),
+ );
+ }
+ // Delete operation. Replace null with an empty object.
+ if (customUserClaims === null) {
+ customUserClaims = {};
+ }
+ // Construct custom user attribute editting request.
+ const request: any = {
+ localId: uid,
+ customAttributes: JSON.stringify(customUserClaims),
+ };
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SET_ACCOUNT_INFO, request)
+ .then((response: any) => {
+ return response.localId as string;
+ });
+ }
+
+ /**
+ * Edits an existing user.
+ *
+ * @param uid - The user to edit.
+ * @param properties - The properties to set on the user.
+ * @returns A promise that resolves when the operation completes
+ * with the user id that was edited.
+ */
+ public updateExistingAccount(uid: string, properties: UpdateRequest): Promise {
+ if (!validator.isUid(uid)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_UID));
+ } else if (!validator.isNonNullObject(properties)) {
+ return Promise.reject(
+ new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'Properties argument must be a non-null object.',
+ ),
+ );
+ } else if (validator.isNonNullObject(properties.providerToLink)) {
+ // TODO(rsgowman): These checks overlap somewhat with
+ // validateProviderUserInfo. It may be possible to refactor a bit.
+ if (!validator.isNonEmptyString(properties.providerToLink.providerId)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'providerToLink.providerId of properties argument must be a non-empty string.');
+ }
+ if (!validator.isNonEmptyString(properties.providerToLink.uid)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'providerToLink.uid of properties argument must be a non-empty string.');
+ }
+ } else if (typeof properties.providersToUnlink !== 'undefined') {
+ if (!validator.isArray(properties.providersToUnlink)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'providersToUnlink of properties argument must be an array of strings.');
+ }
+
+ properties.providersToUnlink.forEach((providerId) => {
+ if (!validator.isNonEmptyString(providerId)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'providersToUnlink of properties argument must be an array of strings.');
+ }
+ });
+ }
+
+ // Build the setAccountInfo request.
+ const request: any = deepCopy(properties);
+ request.localId = uid;
+ // For deleting displayName or photoURL, these values must be passed as null.
+ // They will be removed from the backend request and an additional parameter
+ // deleteAttribute: ['PHOTO_URL', 'DISPLAY_NAME']
+ // with an array of the parameter names to delete will be passed.
+
+ // Parameters that are deletable and their deleteAttribute names.
+ // Use client facing names, photoURL instead of photoUrl.
+ const deletableParams: {[key: string]: string} = {
+ displayName: 'DISPLAY_NAME',
+ photoURL: 'PHOTO_URL',
+ };
+ // Properties to delete if available.
+ request.deleteAttribute = [];
+ for (const key in deletableParams) {
+ if (request[key] === null) {
+ // Add property identifier to list of attributes to delete.
+ request.deleteAttribute.push(deletableParams[key]);
+ // Remove property from request.
+ delete request[key];
+ }
+ }
+ if (request.deleteAttribute.length === 0) {
+ delete request.deleteAttribute;
+ }
+
+ // For deleting phoneNumber, this value must be passed as null.
+ // It will be removed from the backend request and an additional parameter
+ // deleteProvider: ['phone'] with an array of providerIds (phone in this case),
+ // will be passed.
+ if (request.phoneNumber === null) {
+ request.deleteProvider ? request.deleteProvider.push('phone') : request.deleteProvider = ['phone'];
+ delete request.phoneNumber;
+ }
+
+ if (typeof(request.providerToLink) !== 'undefined') {
+ request.linkProviderUserInfo = deepCopy(request.providerToLink);
+ delete request.providerToLink;
+
+ request.linkProviderUserInfo.rawId = request.linkProviderUserInfo.uid;
+ delete request.linkProviderUserInfo.uid;
+ }
+
+ if (typeof(request.providersToUnlink) !== 'undefined') {
+ if (!validator.isArray(request.deleteProvider)) {
+ request.deleteProvider = [];
+ }
+ request.deleteProvider = request.deleteProvider.concat(request.providersToUnlink);
+ delete request.providersToUnlink;
+ }
+
+ // Rewrite photoURL to photoUrl.
+ if (typeof request.photoURL !== 'undefined') {
+ request.photoUrl = request.photoURL;
+ delete request.photoURL;
+ }
+ // Rewrite disabled to disableUser.
+ if (typeof request.disabled !== 'undefined') {
+ request.disableUser = request.disabled;
+ delete request.disabled;
+ }
+ // Construct mfa related user data.
+ if (validator.isNonNullObject(request.multiFactor)) {
+ if (request.multiFactor.enrolledFactors === null) {
+ // Remove all second factors.
+ request.mfa = {};
+ } else if (validator.isArray(request.multiFactor.enrolledFactors)) {
+ request.mfa = {
+ enrollments: [],
+ };
+ try {
+ request.multiFactor.enrolledFactors.forEach((multiFactorInfo: any) => {
+ request.mfa.enrollments.push(convertMultiFactorInfoToServerFormat(multiFactorInfo));
+ });
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ if (request.mfa.enrollments.length === 0) {
+ delete request.mfa.enrollments;
+ }
+ }
+ delete request.multiFactor;
+ }
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SET_ACCOUNT_INFO, request)
+ .then((response: any) => {
+ return response.localId as string;
+ });
+ }
+
+ /**
+ * Revokes all refresh tokens for the specified user identified by the uid provided.
+ * In addition to revoking all refresh tokens for a user, all ID tokens issued
+ * before revocation will also be revoked on the Auth backend. Any request with an
+ * ID token generated before revocation will be rejected with a token expired error.
+ * Note that due to the fact that the timestamp is stored in seconds, any tokens minted in
+ * the same second as the revocation will still be valid. If there is a chance that a token
+ * was minted in the last second, delay for 1 second before revoking.
+ *
+ * @param uid - The user whose tokens are to be revoked.
+ * @returns A promise that resolves when the operation completes
+ * successfully with the user id of the corresponding user.
+ */
+ public revokeRefreshTokens(uid: string): Promise {
+ // Validate user UID.
+ if (!validator.isUid(uid)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_UID));
+ }
+ const request: any = {
+ localId: uid,
+ // validSince is in UTC seconds.
+ validSince: Math.floor(new Date().getTime() / 1000),
+ };
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SET_ACCOUNT_INFO, request)
+ .then((response: any) => {
+ return response.localId as string;
+ });
+ }
+
+ /**
+ * Create a new user with the properties supplied.
+ *
+ * @param properties - The properties to set on the user.
+ * @returns A promise that resolves when the operation completes
+ * with the user id that was created.
+ */
+ public createNewAccount(properties: CreateRequest): Promise {
+ if (!validator.isNonNullObject(properties)) {
+ return Promise.reject(
+ new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'Properties argument must be a non-null object.',
+ ),
+ );
+ }
+
+ // Build the signupNewUser request.
+ type SignUpNewUserRequest = CreateRequest & {
+ photoUrl?: string | null;
+ localId?: string;
+ mfaInfo?: AuthFactorInfo[];
+ };
+ const request: SignUpNewUserRequest = deepCopy(properties);
+ // Rewrite photoURL to photoUrl.
+ if (typeof request.photoURL !== 'undefined') {
+ request.photoUrl = request.photoURL;
+ delete request.photoURL;
+ }
+ // Rewrite uid to localId if it exists.
+ if (typeof request.uid !== 'undefined') {
+ request.localId = request.uid;
+ delete request.uid;
+ }
+ // Construct mfa related user data.
+ if (validator.isNonNullObject(request.multiFactor)) {
+ if (validator.isNonEmptyArray(request.multiFactor.enrolledFactors)) {
+ const mfaInfo: AuthFactorInfo[] = [];
+ try {
+ request.multiFactor.enrolledFactors.forEach((multiFactorInfo) => {
+ // Enrollment time and uid are not allowed for signupNewUser endpoint.
+ // They will automatically be provisioned server side.
+ if ('enrollmentTime' in multiFactorInfo) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"enrollmentTime" is not supported when adding second factors via "createUser()"');
+ } else if ('uid' in multiFactorInfo) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"uid" is not supported when adding second factors via "createUser()"');
+ }
+ mfaInfo.push(convertMultiFactorInfoToServerFormat(multiFactorInfo));
+ });
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ request.mfaInfo = mfaInfo;
+ }
+ delete request.multiFactor;
+ }
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SIGN_UP_NEW_USER, request)
+ .then((response: any) => {
+ // Return the user id.
+ return response.localId as string;
+ });
+ }
+
+ /**
+ * 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.
+ *
+ * @param requestType - The request type. This could be either used for password reset,
+ * email verification, email link sign-in.
+ * @param email - The email of the user the link is being sent to.
+ * @param actionCodeSettings - The optional action code setings which defines whether
+ * the link is to be handled by a mobile app and the additional state information to be passed in the
+ * deep link, etc. Required when requestType === 'EMAIL_SIGNIN'
+ * @param newEmail - The email address the account is being updated to.
+ * Required only for VERIFY_AND_CHANGE_EMAIL requests.
+ * @returns A promise that resolves with the email action link.
+ */
+ public getEmailActionLink(
+ requestType: string, email: string,
+ actionCodeSettings?: ActionCodeSettings, newEmail?: string): Promise {
+ let request = {
+ requestType,
+ email,
+ returnOobLink: true,
+ ...(typeof newEmail !== 'undefined') && { newEmail },
+ };
+ // ActionCodeSettings required for email link sign-in to determine the url where the sign-in will
+ // be completed.
+ if (typeof actionCodeSettings === 'undefined' && requestType === 'EMAIL_SIGNIN') {
+ return Promise.reject(
+ new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ "`actionCodeSettings` is required when `requestType` === 'EMAIL_SIGNIN'",
+ ),
+ );
+ }
+ if (typeof actionCodeSettings !== 'undefined' || requestType === 'EMAIL_SIGNIN') {
+ try {
+ const builder = new ActionCodeSettingsBuilder(actionCodeSettings!);
+ request = deepExtend(request, builder.buildRequest());
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ }
+ if (requestType === 'VERIFY_AND_CHANGE_EMAIL' && typeof newEmail === 'undefined') {
+ return Promise.reject(
+ new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ "`newEmail` is required when `requestType` === 'VERIFY_AND_CHANGE_EMAIL'",
+ ),
+ );
+ }
+ return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_OOB_CODE, request)
+ .then((response: any) => {
+ // Return the link.
+ return response.oobLink as string;
+ });
+ }
+
+ /**
+ * Looks up an OIDC provider configuration by provider ID.
+ *
+ * @param providerId - The provider identifier of the configuration to lookup.
+ * @returns A promise that resolves with the provider configuration information.
+ */
+ public getOAuthIdpConfig(providerId: string): Promise {
+ if (!OIDCConfig.isProviderId(providerId)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID));
+ }
+ return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_OAUTH_IDP_CONFIG, {}, { providerId });
+ }
+
+ /**
+ * Lists the OIDC configurations (single batch only) with a size of maxResults and starting from
+ * the offset as specified by pageToken.
+ *
+ * @param maxResults - The page size, 100 if undefined. This is also the maximum
+ * allowed limit.
+ * @param pageToken - The next page token. If not specified, returns OIDC configurations
+ * without any offset. Configurations are returned in the order they were created from oldest to
+ * newest, relative to the page token offset.
+ * @returns A promise that resolves with the current batch of downloaded
+ * OIDC configurations and the next page token if available. For the last page, an empty list of provider
+ * configuration and no page token are returned.
+ */
+ public listOAuthIdpConfigs(
+ maxResults: number = MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE,
+ pageToken?: string): Promise {
+ const request: {pageSize: number; pageToken?: string} = {
+ pageSize: maxResults,
+ };
+ // Add next page token if provided.
+ if (typeof pageToken !== 'undefined') {
+ request.pageToken = pageToken;
+ }
+ return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), LIST_OAUTH_IDP_CONFIGS, request)
+ .then((response: any) => {
+ if (!response.oauthIdpConfigs) {
+ response.oauthIdpConfigs = [];
+ delete response.nextPageToken;
+ }
+ return response as {oauthIdpConfigs: object[]; nextPageToken?: string};
+ });
+ }
+
+ /**
+ * Deletes an OIDC configuration identified by a providerId.
+ *
+ * @param providerId - The identifier of the OIDC configuration to delete.
+ * @returns A promise that resolves when the OIDC provider is deleted.
+ */
+ public deleteOAuthIdpConfig(providerId: string): Promise {
+ if (!OIDCConfig.isProviderId(providerId)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID));
+ }
+ return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_OAUTH_IDP_CONFIG, {}, { providerId })
+ .then(() => {
+ // Return nothing.
+ });
+ }
+
+ /**
+ * Creates a new OIDC provider configuration with the properties provided.
+ *
+ * @param options - The properties to set on the new OIDC provider configuration to be created.
+ * @returns A promise that resolves with the newly created OIDC
+ * configuration.
+ */
+ public createOAuthIdpConfig(options: OIDCAuthProviderConfig): Promise {
+ // Construct backend request.
+ let request;
+ try {
+ request = OIDCConfig.buildServerRequest(options) || {};
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ const providerId = options.providerId;
+ return this.invokeRequestHandler(
+ this.getProjectConfigUrlBuilder(), CREATE_OAUTH_IDP_CONFIG, request, { providerId })
+ .then((response: any) => {
+ if (!OIDCConfig.getProviderIdFromResourceName(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to create new OIDC provider configuration');
+ }
+ return response as OIDCConfigServerResponse;
+ });
+ }
+
+ /**
+ * Updates an existing OIDC provider configuration with the properties provided.
+ *
+ * @param providerId - The provider identifier of the OIDC configuration to update.
+ * @param options - The properties to update on the existing configuration.
+ * @returns A promise that resolves with the modified provider
+ * configuration.
+ */
+ public updateOAuthIdpConfig(
+ providerId: string, options: OIDCUpdateAuthProviderRequest): Promise {
+ if (!OIDCConfig.isProviderId(providerId)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID));
+ }
+ // Construct backend request.
+ let request: OIDCConfigServerRequest;
+ try {
+ request = OIDCConfig.buildServerRequest(options, true) || {};
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ const updateMask = utils.generateUpdateMask(request);
+ return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), UPDATE_OAUTH_IDP_CONFIG, request,
+ { providerId, updateMask: updateMask.join(',') })
+ .then((response: any) => {
+ if (!OIDCConfig.getProviderIdFromResourceName(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to update OIDC provider configuration');
+ }
+ return response as OIDCConfigServerResponse;
+ });
+ }
+
+ /**
+ * Looks up an SAML provider configuration by provider ID.
+ *
+ * @param providerId - The provider identifier of the configuration to lookup.
+ * @returns A promise that resolves with the provider configuration information.
+ */
+ public getInboundSamlConfig(providerId: string): Promise {
+ if (!SAMLConfig.isProviderId(providerId)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID));
+ }
+ return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_INBOUND_SAML_CONFIG, {}, { providerId });
+ }
+
+ /**
+ * Lists the SAML configurations (single batch only) with a size of maxResults and starting from
+ * the offset as specified by pageToken.
+ *
+ * @param maxResults - The page size, 100 if undefined. This is also the maximum
+ * allowed limit.
+ * @param pageToken - The next page token. If not specified, returns SAML configurations starting
+ * without any offset. Configurations are returned in the order they were created from oldest to
+ * newest, relative to the page token offset.
+ * @returns A promise that resolves with the current batch of downloaded
+ * SAML configurations and the next page token if available. For the last page, an empty list of provider
+ * configuration and no page token are returned.
+ */
+ public listInboundSamlConfigs(
+ maxResults: number = MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE,
+ pageToken?: string): Promise {
+ const request: {pageSize: number; pageToken?: string} = {
+ pageSize: maxResults,
+ };
+ // Add next page token if provided.
+ if (typeof pageToken !== 'undefined') {
+ request.pageToken = pageToken;
+ }
+ return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), LIST_INBOUND_SAML_CONFIGS, request)
+ .then((response: any) => {
+ if (!response.inboundSamlConfigs) {
+ response.inboundSamlConfigs = [];
+ delete response.nextPageToken;
+ }
+ return response as {inboundSamlConfigs: object[]; nextPageToken?: string};
+ });
+ }
+
+ /**
+ * Deletes a SAML configuration identified by a providerId.
+ *
+ * @param providerId - The identifier of the SAML configuration to delete.
+ * @returns A promise that resolves when the SAML provider is deleted.
+ */
+ public deleteInboundSamlConfig(providerId: string): Promise {
+ if (!SAMLConfig.isProviderId(providerId)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID));
+ }
+ return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_INBOUND_SAML_CONFIG, {}, { providerId })
+ .then(() => {
+ // Return nothing.
+ });
+ }
+
+ /**
+ * Creates a new SAML provider configuration with the properties provided.
+ *
+ * @param options - The properties to set on the new SAML provider configuration to be created.
+ * @returns A promise that resolves with the newly created SAML
+ * configuration.
+ */
+ public createInboundSamlConfig(options: SAMLAuthProviderConfig): Promise {
+ // Construct backend request.
+ let request;
+ try {
+ request = SAMLConfig.buildServerRequest(options) || {};
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ const providerId = options.providerId;
+ return this.invokeRequestHandler(
+ this.getProjectConfigUrlBuilder(), CREATE_INBOUND_SAML_CONFIG, request, { providerId })
+ .then((response: any) => {
+ if (!SAMLConfig.getProviderIdFromResourceName(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to create new SAML provider configuration');
+ }
+ return response as SAMLConfigServerResponse;
+ });
+ }
+
+ /**
+ * Updates an existing SAML provider configuration with the properties provided.
+ *
+ * @param providerId - The provider identifier of the SAML configuration to update.
+ * @param options - The properties to update on the existing configuration.
+ * @returns A promise that resolves with the modified provider
+ * configuration.
+ */
+ public updateInboundSamlConfig(
+ providerId: string, options: SAMLUpdateAuthProviderRequest): Promise {
+ if (!SAMLConfig.isProviderId(providerId)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID));
+ }
+ // Construct backend request.
+ let request: SAMLConfigServerRequest;
+ try {
+ request = SAMLConfig.buildServerRequest(options, true) || {};
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ const updateMask = utils.generateUpdateMask(request);
+ return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), UPDATE_INBOUND_SAML_CONFIG, request,
+ { providerId, updateMask: updateMask.join(',') })
+ .then((response: any) => {
+ if (!SAMLConfig.getProviderIdFromResourceName(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to update SAML provider configuration');
+ }
+ return response as SAMLConfigServerResponse;
+ });
+ }
+
+ /**
+ * Invokes the request handler based on the API settings object passed.
+ *
+ * @param urlBuilder - The URL builder for Auth endpoints.
+ * @param apiSettings - The API endpoint settings to apply to request and response.
+ * @param requestData - The request data.
+ * @param additionalResourceParams - Additional resource related params if needed.
+ * @returns A promise that resolves with the response.
+ */
+ protected invokeRequestHandler(
+ urlBuilder: AuthResourceUrlBuilder, apiSettings: ApiSettings,
+ requestData: object | undefined, additionalResourceParams?: object): Promise {
+ return urlBuilder.getUrl(apiSettings.getEndpoint(), additionalResourceParams)
+ .then((url) => {
+ // Validate request.
+ if (requestData) {
+ const requestValidator = apiSettings.getRequestValidator();
+ requestValidator(requestData);
+ }
+ // Process request.
+ const req: HttpRequestConfig = {
+ method: apiSettings.getHttpMethod(),
+ url,
+ headers: FIREBASE_AUTH_HEADER,
+ data: requestData,
+ timeout: FIREBASE_AUTH_TIMEOUT,
+ };
+ return this.httpClient.send(req);
+ })
+ .then((response) => {
+ // Validate response.
+ const responseValidator = apiSettings.getResponseValidator();
+ responseValidator(response.data);
+ // Return entire response.
+ return response.data;
+ })
+ .catch((err) => {
+ if (err instanceof HttpError) {
+ const error = err.response.data;
+ const errorCode = AbstractAuthRequestHandler.getErrorCode(error);
+ if (!errorCode) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'Error returned from server: ' + error + '. Additionally, an ' +
+ 'internal error occurred while attempting to extract the ' +
+ 'errorcode from the error.',
+ );
+ }
+ throw FirebaseAuthError.fromServerError(errorCode, /* message */ undefined, error);
+ }
+ throw err;
+ });
+ }
+
+ /**
+ * @returns A new Auth user management resource URL builder instance.
+ */
+ protected abstract newAuthUrlBuilder(): AuthResourceUrlBuilder;
+
+ /**
+ * @returns A new project config resource URL builder instance.
+ */
+ protected abstract newProjectConfigUrlBuilder(): AuthResourceUrlBuilder;
+
+ /**
+ * @returns The current Auth user management resource URL builder.
+ */
+ private getAuthUrlBuilder(): AuthResourceUrlBuilder {
+ if (!this.authUrlBuilder) {
+ this.authUrlBuilder = this.newAuthUrlBuilder();
+ }
+ return this.authUrlBuilder;
+ }
+
+ /**
+ * @returns The current project config resource URL builder.
+ */
+ private getProjectConfigUrlBuilder(): AuthResourceUrlBuilder {
+ if (!this.projectConfigUrlBuilder) {
+ this.projectConfigUrlBuilder = this.newProjectConfigUrlBuilder();
+ }
+ return this.projectConfigUrlBuilder;
+ }
+}
+
+/** Instantiates the getConfig endpoint settings. */
+const GET_PROJECT_CONFIG = new ApiSettings('/config', 'GET')
+ .setResponseValidator((response: any) => {
+ // Response should always contain at least the config name.
+ if (!validator.isNonEmptyString(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to get project config',
+ );
+ }
+ });
+
+/** Instantiates the updateConfig endpoint settings. */
+const UPDATE_PROJECT_CONFIG = new ApiSettings('/config?updateMask={updateMask}', 'PATCH')
+ .setResponseValidator((response: any) => {
+ // Response should always contain at least the config name.
+ if (!validator.isNonEmptyString(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to update project config',
+ );
+ }
+ });
+
+/** Instantiates the getTenant endpoint settings. */
+const GET_TENANT = new ApiSettings('/tenants/{tenantId}', 'GET')
+// Set response validator.
+ .setResponseValidator((response: any) => {
+ // Response should always contain at least the tenant name.
+ if (!validator.isNonEmptyString(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to get tenant',
+ );
+ }
+ });
+
+/** Instantiates the deleteTenant endpoint settings. */
+const DELETE_TENANT = new ApiSettings('/tenants/{tenantId}', 'DELETE');
+
+/** Instantiates the updateTenant endpoint settings. */
+const UPDATE_TENANT = new ApiSettings('/tenants/{tenantId}?updateMask={updateMask}', 'PATCH')
+ // Set response validator.
+ .setResponseValidator((response: any) => {
+ // Response should always contain at least the tenant name.
+ if (!validator.isNonEmptyString(response.name) ||
+ !Tenant.getTenantIdFromResourceName(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to update tenant',
+ );
+ }
+ });
+
+/** Instantiates the listTenants endpoint settings. */
+const LIST_TENANTS = new ApiSettings('/tenants', 'GET')
+ // Set request validator.
+ .setRequestValidator((request: any) => {
+ // Validate next page token.
+ if (typeof request.pageToken !== 'undefined' &&
+ !validator.isNonEmptyString(request.pageToken)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PAGE_TOKEN);
+ }
+ // Validate max results.
+ if (!validator.isNumber(request.pageSize) ||
+ request.pageSize <= 0 ||
+ request.pageSize > MAX_LIST_TENANT_PAGE_SIZE) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'Required "maxResults" must be a positive non-zero number that does not exceed ' +
+ `the allowed ${MAX_LIST_TENANT_PAGE_SIZE}.`,
+ );
+ }
+ });
+
+/** Instantiates the createTenant endpoint settings. */
+const CREATE_TENANT = new ApiSettings('/tenants', 'POST')
+// Set response validator.
+ .setResponseValidator((response: any) => {
+ // Response should always contain at least the tenant name.
+ if (!validator.isNonEmptyString(response.name) ||
+ !Tenant.getTenantIdFromResourceName(response.name)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Unable to create new tenant',
+ );
+ }
+ });
+
+
+/**
+ * Utility for sending requests to Auth server that are Auth instance related. This includes user, tenant,
+ * and project config management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines
+ * additional tenant management related APIs.
+ */
+export class AuthRequestHandler extends AbstractAuthRequestHandler {
+
+ protected readonly authResourceUrlBuilder: AuthResourceUrlBuilder;
+
+ /**
+ * The FirebaseAuthRequestHandler constructor used to initialize an instance using a FirebaseApp.
+ *
+ * @param app - The app used to fetch access tokens to sign API requests.
+ * @constructor.
+ */
+ constructor(app: App) {
+ super(app);
+ this.authResourceUrlBuilder = new AuthResourceUrlBuilder(app, 'v2');
+ }
+
+ /**
+ * @returns A new Auth user management resource URL builder instance.
+ */
+ protected newAuthUrlBuilder(): AuthResourceUrlBuilder {
+ return new AuthResourceUrlBuilder(this.app, 'v1');
+ }
+
+ /**
+ * @returns A new project config resource URL builder instance.
+ */
+ protected newProjectConfigUrlBuilder(): AuthResourceUrlBuilder {
+ return new AuthResourceUrlBuilder(this.app, 'v2');
+ }
+
+ /**
+ * Get the current project's config
+ * @returns A promise that resolves with the project config information.
+ */
+ public getProjectConfig(): Promise {
+ return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_PROJECT_CONFIG, {}, {})
+ .then((response: any) => {
+ return response as ProjectConfigServerResponse;
+ });
+ }
+
+ /**
+ * Update the current project's config.
+ * @returns A promise that resolves with the project config information.
+ */
+ public updateProjectConfig(options: UpdateProjectConfigRequest): Promise {
+ try {
+ const request = ProjectConfig.buildServerRequest(options);
+ const updateMask = utils.generateUpdateMask(request);
+ return this.invokeRequestHandler(
+ this.authResourceUrlBuilder, UPDATE_PROJECT_CONFIG, request, { updateMask: updateMask.join(',') })
+ .then((response: any) => {
+ return response as ProjectConfigServerResponse;
+ });
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ }
+
+ /**
+ * Looks up a tenant by tenant ID.
+ *
+ * @param tenantId - The tenant identifier of the tenant to lookup.
+ * @returns A promise that resolves with the tenant information.
+ */
+ public getTenant(tenantId: string): Promise {
+ if (!validator.isNonEmptyString(tenantId)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID));
+ }
+ return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_TENANT, {}, { tenantId })
+ .then((response: any) => {
+ return response as TenantServerResponse;
+ });
+ }
+
+ /**
+ * Exports the tenants (single batch only) with a size of maxResults and starting from
+ * the offset as specified by pageToken.
+ *
+ * @param maxResults - The page size, 1000 if undefined. This is also the maximum
+ * allowed limit.
+ * @param pageToken - The next page token. If not specified, returns tenants starting
+ * without any offset. Tenants are returned in the order they were created from oldest to
+ * newest, relative to the page token offset.
+ * @returns A promise that resolves with the current batch of downloaded
+ * tenants and the next page token if available. For the last page, an empty list of tenants
+ * and no page token are returned.
+ */
+ public listTenants(
+ maxResults: number = MAX_LIST_TENANT_PAGE_SIZE,
+ pageToken?: string): Promise<{tenants: TenantServerResponse[]; nextPageToken?: string}> {
+ const request = {
+ pageSize: maxResults,
+ pageToken,
+ };
+ // Remove next page token if not provided.
+ if (typeof request.pageToken === 'undefined') {
+ delete request.pageToken;
+ }
+ return this.invokeRequestHandler(this.authResourceUrlBuilder, LIST_TENANTS, request)
+ .then((response: any) => {
+ if (!response.tenants) {
+ response.tenants = [];
+ delete response.nextPageToken;
+ }
+ return response as {tenants: TenantServerResponse[]; nextPageToken?: string};
+ });
+ }
+
+ /**
+ * Deletes a tenant identified by a tenantId.
+ *
+ * @param tenantId - The identifier of the tenant to delete.
+ * @returns A promise that resolves when the tenant is deleted.
+ */
+ public deleteTenant(tenantId: string): Promise {
+ if (!validator.isNonEmptyString(tenantId)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID));
+ }
+ return this.invokeRequestHandler(this.authResourceUrlBuilder, DELETE_TENANT, undefined, { tenantId })
+ .then(() => {
+ // Return nothing.
+ });
+ }
+
+ /**
+ * Creates a new tenant with the properties provided.
+ *
+ * @param tenantOptions - The properties to set on the new tenant to be created.
+ * @returns A promise that resolves with the newly created tenant object.
+ */
+ public createTenant(tenantOptions: CreateTenantRequest): Promise {
+ try {
+ // Construct backend request.
+ const request = Tenant.buildServerRequest(tenantOptions, true);
+ return this.invokeRequestHandler(this.authResourceUrlBuilder, CREATE_TENANT, request)
+ .then((response: any) => {
+ return response as TenantServerResponse;
+ });
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ }
+
+ /**
+ * Updates an existing tenant with the properties provided.
+ *
+ * @param tenantId - The tenant identifier of the tenant to update.
+ * @param tenantOptions - The properties to update on the existing tenant.
+ * @returns A promise that resolves with the modified tenant object.
+ */
+ public updateTenant(tenantId: string, tenantOptions: UpdateTenantRequest): Promise {
+ if (!validator.isNonEmptyString(tenantId)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID));
+ }
+ try {
+ // Construct backend request.
+ const request = Tenant.buildServerRequest(tenantOptions, false);
+ // Do not traverse deep into testPhoneNumbers. The entire content should be replaced
+ // and not just specific phone numbers.
+ const updateMask = utils.generateUpdateMask(request, ['testPhoneNumbers']);
+ return this.invokeRequestHandler(this.authResourceUrlBuilder, UPDATE_TENANT, request,
+ { tenantId, updateMask: updateMask.join(',') })
+ .then((response: any) => {
+ return response as TenantServerResponse;
+ });
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ }
+}
+
+/**
+ * Utility for sending requests to Auth server that are tenant Auth instance related. This includes user
+ * management related APIs for specified tenants.
+ * This extends the BaseFirebaseAuthRequestHandler class.
+ */
+export class TenantAwareAuthRequestHandler extends AbstractAuthRequestHandler {
+ /**
+ * The FirebaseTenantRequestHandler constructor used to initialize an instance using a
+ * FirebaseApp and a tenant ID.
+ *
+ * @param app - The app used to fetch access tokens to sign API requests.
+ * @param tenantId - The request handler's tenant ID.
+ * @constructor
+ */
+ constructor(app: App, private readonly tenantId: string) {
+ super(app);
+ }
+
+ /**
+ * @returns A new Auth user management resource URL builder instance.
+ */
+ protected newAuthUrlBuilder(): AuthResourceUrlBuilder {
+ return new TenantAwareAuthResourceUrlBuilder(this.app, 'v1', this.tenantId);
+ }
+
+ /**
+ * @returns A new project config resource URL builder instance.
+ */
+ protected newProjectConfigUrlBuilder(): AuthResourceUrlBuilder {
+ return new TenantAwareAuthResourceUrlBuilder(this.app, 'v2', this.tenantId);
+ }
+
+ /**
+ * Imports the list of users provided to Firebase Auth. This is useful when
+ * migrating from an external authentication system without having to use the Firebase CLI SDK.
+ * At most, 1000 users are allowed to be imported one at a time.
+ * When importing a list of password users, UserImportOptions are required to be specified.
+ *
+ * Overrides the superclass methods by adding an additional check to match tenant IDs of
+ * imported user records if present.
+ *
+ * @param users - The list of user records to import to Firebase Auth.
+ * @param options - The user import options, required when the users provided
+ * include password credentials.
+ * @returns A promise that resolves when the operation completes
+ * with the result of the import. This includes the number of successful imports, the number
+ * of failed uploads and their corresponding errors.
+ */
+ public uploadAccount(
+ users: UserImportRecord[], options?: UserImportOptions): Promise {
+ // Add additional check to match tenant ID of imported user records.
+ users.forEach((user: UserImportRecord, index: number) => {
+ if (validator.isNonEmptyString(user.tenantId) &&
+ user.tenantId !== this.tenantId) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.MISMATCHING_TENANT_ID,
+ `UserRecord of index "${index}" has mismatching tenant ID "${user.tenantId}"`);
+ }
+ });
+ return super.uploadAccount(users, options);
+ }
+}
+
+function emulatorHost(): string | undefined {
+ return process.env.FIREBASE_AUTH_EMULATOR_HOST
+}
+
+/**
+ * When true the SDK should communicate with the Auth Emulator for all API
+ * calls and also produce unsigned tokens.
+ */
+export function useEmulator(): boolean {
+ return !!emulatorHost();
+}
\ No newline at end of file
diff --git a/packages/dart_firebase_admin/lib/src/auth/auth_config.dart b/packages/dart_firebase_admin/lib/src/auth/auth_config.dart
new file mode 100644
index 0000000..e4f5ffa
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/auth_config.dart
@@ -0,0 +1,329 @@
+import 'package:firebaseapis/identitytoolkit/v1.dart' as v1;
+
+import '../dart_firebase_admin.dart';
+
+const _sentinel = _Sentinel();
+
+class _Sentinel {
+ const _Sentinel();
+}
+
+/// An object used to differentiate "no value" from "a null value".
+///
+/// This is typically used to enable `update(displayName: null)`.
+class Box {
+ Box(this.value);
+
+ static Box? unwrap(Object? value) {
+ if (value == _sentinel) return null;
+ return Box(value as T?);
+ }
+
+ final T value;
+}
+
+/// Interface representing the properties to set on a new user record to be
+/// created.
+class CreateRequest extends _BaseUpdateRequest {
+ CreateRequest({
+ super.disabled,
+ super.displayName,
+ super.email,
+ super.emailVerified,
+ super.password,
+ super.phoneNumber,
+ super.photoURL,
+ this.multiFactor,
+ this.uid,
+ }) : assert(
+ multiFactor is! MultiFactorUpdateSettings,
+ 'MultiFactorUpdateSettings is not supported for create requests.',
+ );
+
+ /// The user's `uid`.
+ final String? uid;
+
+ /// The user's multi-factor related properties.
+ final MultiFactorCreateSettings? multiFactor;
+}
+
+/// Interface representing the properties to update on the provided user.
+class UpdateRequest extends _BaseUpdateRequest {
+ /// Interface representing the properties to update on the provided user.
+ UpdateRequest({
+ super.disabled,
+ String? super.displayName,
+ super.email,
+ super.emailVerified,
+ super.password,
+ String? super.phoneNumber,
+ String? super.photoURL,
+ this.multiFactor,
+ this.providerToLink,
+ this.providersToUnlink,
+ });
+
+ UpdateRequest._({
+ super.disabled,
+ super.displayName,
+ super.email,
+ super.emailVerified,
+ super.password,
+ super.phoneNumber,
+ super.photoURL,
+ this.multiFactor,
+ this.providerToLink,
+ this.providersToUnlink,
+ });
+
+ /// The user's updated multi-factor related properties.
+ final MultiFactorUpdateSettings? multiFactor;
+
+ /// Links this user to the specified provider.
+ ///
+ /// Linking a provider to an existing user account does not invalidate the
+ /// refresh token of that account. In other words, the existing account
+ /// would continue to be able to access resources, despite not having used
+ /// the newly linked provider to log in. If you wish to force the user to
+ /// authenticate with this new provider, you need to (a) revoke their
+ /// refresh token (see
+ /// https://firebase.google.com/docs/auth/admin/manage-sessions#revoke_refresh_tokens),
+ /// and (b) ensure no other authentication methods are present on this
+ /// account.
+ final UserProvider? providerToLink;
+
+ /// Unlinks this user from the specified providers.
+ final List? providersToUnlink;
+
+ UpdateRequest Function({String? email, String? phoneNumber}) get copyWith {
+ // ignore: avoid_types_on_closure_parameters, false positive
+ return ({Object? email = _sentinel, Object? phoneNumber = _sentinel}) {
+ return UpdateRequest._(
+ disabled: disabled,
+ displayName: displayName,
+ email: email == _sentinel ? this.email : email as String?,
+ emailVerified: emailVerified,
+ password: password,
+ phoneNumber: phoneNumber == _sentinel
+ ? this.phoneNumber
+ : phoneNumber as String?,
+ photoURL: photoURL,
+ multiFactor: multiFactor,
+ providerToLink: providerToLink,
+ providersToUnlink: providersToUnlink,
+ );
+ };
+ }
+}
+
+class _BaseUpdateRequest {
+ /// A base request to update a user.
+ /// This supports differentiating between unset properties and clearing
+ /// properties by setting them to `null`.
+ ///
+ /// As in `UpdateRequest()` vs `UpdateRequest(displayName: null)`.
+ ///
+ /// Use [UpdateRequest] directly instead, as this constructor has some
+ /// untyped parameters.
+ _BaseUpdateRequest({
+ required this.disabled,
+ Object? displayName = _sentinel,
+ required this.email,
+ required this.emailVerified,
+ required this.password,
+ Object? phoneNumber = _sentinel,
+ Object? photoURL = _sentinel,
+ }) : 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.
+ final bool? disabled;
+
+ /// The user's display name.
+ final Box? displayName;
+
+ /// The user's primary email.
+ final String? email;
+
+ /// Whether or not the user's primary email is verified.
+ final bool? emailVerified;
+
+ /// The user's unhashed password.
+ final String? password;
+
+ /// The user's primary phone number.
+ final Box? phoneNumber;
+
+ /// The user's photo URL.
+ final Box? photoURL;
+}
+
+/// Represents a user identity provider that can be associated with a Firebase user.
+class UserProvider {
+ UserProvider({
+ this.uid,
+ this.displayName,
+ this.email,
+ this.phoneNumber,
+ this.photoURL,
+ this.providerId,
+ });
+
+ /// The user identifier for the linked provider.
+ final String? uid;
+
+ /// The display name for the linked provider.
+ final String? displayName;
+
+ /// The email for the linked provider.
+ final String? email;
+
+ /// The phone number for the linked provider.
+ final String? phoneNumber;
+
+ /// The photo URL for the linked provider.
+ final String? photoURL;
+
+ /// The linked provider ID (for example, "google.com" for the Google provider).
+ final String? providerId;
+
+ v1.GoogleCloudIdentitytoolkitV1ProviderUserInfo toProviderUserInfo() {
+ return v1.GoogleCloudIdentitytoolkitV1ProviderUserInfo(
+ displayName: displayName,
+ email: email,
+ phoneNumber: phoneNumber,
+ photoUrl: photoURL,
+ providerId: providerId,
+ rawId: uid,
+ federatedId: null,
+ screenName: null,
+ );
+ }
+}
+
+/// The multi-factor related user settings for update operations.
+class MultiFactorUpdateSettings {
+ MultiFactorUpdateSettings({this.enrolledFactors});
+
+ /// The updated list of enrolled second factors. The provided list overwrites the user's
+ /// existing list of second factors.
+ /// When null is passed, all of the user's existing second factors are removed.
+ final List? enrolledFactors;
+
+ v1.GoogleCloudIdentitytoolkitV1MfaInfo toMfaInfo() {
+ final enrolledFactors = this.enrolledFactors;
+ if (enrolledFactors == null || enrolledFactors.isEmpty) {
+ // Remove all second factors.
+ return v1.GoogleCloudIdentitytoolkitV1MfaInfo();
+ }
+
+ return v1.GoogleCloudIdentitytoolkitV1MfaInfo(
+ enrollments: enrolledFactors.map((e) => e.toMfaEnrollment()).toList(),
+ );
+ }
+}
+
+/// The multi-factor related user settings for create operations.
+class MultiFactorCreateSettings {
+ MultiFactorCreateSettings({
+ required this.enrolledFactors,
+ });
+
+ /// The created user's list of enrolled second factors.
+ final List enrolledFactors;
+}
+
+/// Interface representing a phone specific user-enrolled second factor for a
+/// `CreateRequest`.
+class CreatePhoneMultiFactorInfoRequest extends CreateMultiFactorInfoRequest {
+ CreatePhoneMultiFactorInfoRequest({
+ required super.displayName,
+ required this.phoneNumber,
+ });
+
+ /// 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,
+ });
+
+ /// The optional display name for an enrolled second factor.
+ final String? displayName;
+
+ v1.GoogleCloudIdentitytoolkitV1MfaFactor
+ toGoogleCloudIdentitytoolkitV1MfaFactor();
+}
+
+/// Interface representing a phone specific user-enrolled second factor
+/// for an `UpdateRequest`.
+class UpdatePhoneMultiFactorInfoRequest extends UpdateMultiFactorInfoRequest {
+ UpdatePhoneMultiFactorInfoRequest({
+ required this.phoneNumber,
+ super.uid,
+ super.displayName,
+ super.enrollmentTime,
+ });
+
+ /// The phone number associated with a phone second factor.
+ final String phoneNumber;
+}
+
+/// Interface representing common properties of a user-enrolled second factor
+/// for an `UpdateRequest`.
+sealed class UpdateMultiFactorInfoRequest {
+ UpdateMultiFactorInfoRequest({
+ this.uid,
+ this.displayName,
+ this.enrollmentTime,
+ }) {
+ final enrollmentTime = this.enrollmentTime;
+ if (enrollmentTime != null && !enrollmentTime.isUtc) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidEnrollmentTime,
+ 'The second factor "enrollmentTime" for "$uid" must be a valid '
+ 'UTC date.',
+ );
+ }
+ }
+
+ /// The ID of the enrolled second factor. This ID is unique to the user. When not provided,
+ /// a new one is provisioned by the Auth server.
+ final String? uid;
+
+ /// The optional display name for an enrolled second factor.
+ final String? displayName;
+
+ /// The optional date the second factor was enrolled.
+ final DateTime? enrollmentTime;
+
+ v1.GoogleCloudIdentitytoolkitV1MfaEnrollment toMfaEnrollment() {
+ final that = this;
+ return switch (that) {
+ UpdatePhoneMultiFactorInfoRequest() =>
+ v1.GoogleCloudIdentitytoolkitV1MfaEnrollment(
+ mfaEnrollmentId: uid,
+ displayName: displayName,
+ // Required for all phone second factors.
+ phoneInfo: that.phoneNumber,
+ enrolledAt: enrollmentTime?.toUtc().toIso8601String(),
+ ),
+ };
+ }
+}
diff --git a/packages/dart_firebase_admin/lib/src/auth/auth_config.ts b/packages/dart_firebase_admin/lib/src/auth/auth_config.ts
new file mode 100644
index 0000000..3629157
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/auth_config.ts
@@ -0,0 +1,2281 @@
+/*!
+ * Copyright 2018 Google Inc.
+ *
+ * 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.
+ */
+
+import * as validator from '../utils/validator';
+import { deepCopy } from '../utils/deep-copy';
+import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error';
+
+/**
+ * Interface representing base properties of a user-enrolled second factor for a
+ * `CreateRequest`.
+ */
+export interface BaseCreateMultiFactorInfoRequest {
+
+ /**
+ * The optional display name for an enrolled second factor.
+ */
+ displayName?: string;
+
+ /**
+ * The type identifier of the second factor. For SMS second factors, this is `phone`.
+ */
+ factorId: string;
+}
+
+/**
+ * Interface representing a phone specific user-enrolled second factor for a
+ * `CreateRequest`.
+ */
+export interface CreatePhoneMultiFactorInfoRequest extends BaseCreateMultiFactorInfoRequest {
+
+ /**
+ * The phone number associated with a phone second factor.
+ */
+ phoneNumber: string;
+}
+
+/**
+ * Type representing the properties of a user-enrolled second factor
+ * for a `CreateRequest`.
+ */
+export type CreateMultiFactorInfoRequest = | CreatePhoneMultiFactorInfoRequest;
+
+/**
+ * Interface representing common properties of a user-enrolled second factor
+ * for an `UpdateRequest`.
+ */
+export interface BaseUpdateMultiFactorInfoRequest {
+
+ /**
+ * The ID of the enrolled second factor. This ID is unique to the user. When not provided,
+ * a new one is provisioned by the Auth server.
+ */
+ uid?: string;
+
+ /**
+ * The optional display name for an enrolled second factor.
+ */
+ displayName?: string;
+
+ /**
+ * The optional date the second factor was enrolled, formatted as a UTC string.
+ */
+ enrollmentTime?: string;
+
+ /**
+ * The type identifier of the second factor. For SMS second factors, this is `phone`.
+ */
+ factorId: string;
+}
+
+/**
+ * Interface representing a phone specific user-enrolled second factor
+ * for an `UpdateRequest`.
+ */
+export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactorInfoRequest {
+
+ /**
+ * The phone number associated with a phone second factor.
+ */
+ phoneNumber: string;
+}
+
+/**
+ * Type representing the properties of a user-enrolled second factor
+ * for an `UpdateRequest`.
+ */
+export type UpdateMultiFactorInfoRequest = | UpdatePhoneMultiFactorInfoRequest;
+
+/**
+ * The multi-factor related user settings for create operations.
+ */
+export interface MultiFactorCreateSettings {
+
+ /**
+ * The created user's list of enrolled second factors.
+ */
+ enrolledFactors: CreateMultiFactorInfoRequest[];
+}
+
+/**
+ * The multi-factor related user settings for update operations.
+ */
+export interface MultiFactorUpdateSettings {
+
+ /**
+ * The updated list of enrolled second factors. The provided list overwrites the user's
+ * existing list of second factors.
+ * When null is passed, all of the user's existing second factors are removed.
+ */
+ enrolledFactors: UpdateMultiFactorInfoRequest[] | null;
+}
+
+/**
+ * Interface representing the properties to update on the provided user.
+ */
+export interface UpdateRequest {
+
+ /**
+ * Whether or not the user is disabled: `true` for disabled;
+ * `false` for enabled.
+ */
+ disabled?: boolean;
+
+ /**
+ * The user's display name.
+ */
+ displayName?: string | null;
+
+ /**
+ * The user's primary email.
+ */
+ email?: string;
+
+ /**
+ * Whether or not the user's primary email is verified.
+ */
+ emailVerified?: boolean;
+
+ /**
+ * The user's unhashed password.
+ */
+ password?: string;
+
+ /**
+ * The user's primary phone number.
+ */
+ phoneNumber?: string | null;
+
+ /**
+ * The user's photo URL.
+ */
+ photoURL?: string | null;
+
+ /**
+ * The user's updated multi-factor related properties.
+ */
+ multiFactor?: MultiFactorUpdateSettings;
+
+ /**
+ * Links this user to the specified provider.
+ *
+ * Linking a provider to an existing user account does not invalidate the
+ * refresh token of that account. In other words, the existing account
+ * would continue to be able to access resources, despite not having used
+ * the newly linked provider to log in. If you wish to force the user to
+ * authenticate with this new provider, you need to (a) revoke their
+ * refresh token (see
+ * https://firebase.google.com/docs/auth/admin/manage-sessions#revoke_refresh_tokens),
+ * and (b) ensure no other authentication methods are present on this
+ * account.
+ */
+ providerToLink?: UserProvider;
+
+ /**
+ * Unlinks this user from the specified providers.
+ */
+ providersToUnlink?: string[];
+}
+
+/**
+ * Represents a user identity provider that can be associated with a Firebase user.
+ */
+export interface UserProvider {
+
+ /**
+ * The user identifier for the linked provider.
+ */
+ uid?: string;
+
+ /**
+ * The display name for the linked provider.
+ */
+ displayName?: string;
+
+ /**
+ * The email for the linked provider.
+ */
+ email?: string;
+
+ /**
+ * The phone number for the linked provider.
+ */
+ phoneNumber?: string;
+
+ /**
+ * The photo URL for the linked provider.
+ */
+ photoURL?: string;
+
+ /**
+ * The linked provider ID (for example, "google.com" for the Google provider).
+ */
+ providerId?: string;
+}
+
+
+/**
+ * Interface representing the properties to set on a new user record to be
+ * created.
+ */
+export interface CreateRequest extends UpdateRequest {
+
+ /**
+ * The user's `uid`.
+ */
+ uid?: string;
+
+ /**
+ * The user's multi-factor related properties.
+ */
+ multiFactor?: MultiFactorCreateSettings;
+}
+
+/**
+ * The response interface for listing provider configs. This is only available
+ * when listing all identity providers' configurations via
+ * {@link BaseAuth.listProviderConfigs}.
+ */
+export interface ListProviderConfigResults {
+
+ /**
+ * The list of providers for the specified type in the current page.
+ */
+ providerConfigs: AuthProviderConfig[];
+
+ /**
+ * The next page token, if available.
+ */
+ pageToken?: string;
+}
+
+/**
+ * The filter interface used for listing provider configurations. This is used
+ * when specifying how to list configured identity providers via
+ * {@link BaseAuth.listProviderConfigs}.
+ */
+export interface AuthProviderConfigFilter {
+
+ /**
+ * 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
+ * for OIDC providers.
+ */
+ type: 'saml' | 'oidc';
+
+ /**
+ * The maximum number of results to return per page. The default and maximum is
+ * 100.
+ */
+ maxResults?: number;
+
+ /**
+ * The next page token. When not specified, the lookup starts from the beginning
+ * of the list.
+ */
+ pageToken?: string;
+}
+
+/**
+ * The request interface for updating a SAML Auth provider. This is used
+ * when updating a SAML provider's configuration via
+ * {@link BaseAuth.updateProviderConfig}.
+ */
+export interface SAMLUpdateAuthProviderRequest {
+
+ /**
+ * The SAML provider's updated display name. If not provided, the existing
+ * configuration's value is not modified.
+ */
+ displayName?: string;
+
+ /**
+ * Whether the SAML provider is enabled or not. If not provided, the existing
+ * configuration's setting is not modified.
+ */
+ enabled?: boolean;
+
+ /**
+ * The SAML provider's updated IdP entity ID. If not provided, the existing
+ * configuration's value is not modified.
+ */
+ idpEntityId?: string;
+
+ /**
+ * The SAML provider's updated SSO URL. If not provided, the existing
+ * configuration's value is not modified.
+ */
+ ssoURL?: string;
+
+ /**
+ * The SAML provider's updated list of X.509 certificated. If not provided, the
+ * existing configuration list is not modified.
+ */
+ x509Certificates?: string[];
+
+ /**
+ * The SAML provider's updated RP entity ID. If not provided, the existing
+ * configuration's value is not modified.
+ */
+ rpEntityId?: string;
+
+ /**
+ * The SAML provider's callback URL. If not provided, the existing
+ * configuration's value is not modified.
+ */
+ callbackURL?: string;
+}
+
+/**
+ * The request interface for updating an OIDC Auth provider. This is used
+ * when updating an OIDC provider's configuration via
+ * {@link BaseAuth.updateProviderConfig}.
+ */
+export interface OIDCUpdateAuthProviderRequest {
+
+ /**
+ * The OIDC provider's updated display name. If not provided, the existing
+ * configuration's value is not modified.
+ */
+ displayName?: string;
+
+ /**
+ * Whether the OIDC provider is enabled or not. If not provided, the existing
+ * configuration's setting is not modified.
+ */
+ enabled?: boolean;
+
+ /**
+ * The OIDC provider's updated client ID. If not provided, the existing
+ * configuration's value is not modified.
+ */
+ clientId?: string;
+
+ /**
+ * The OIDC provider's updated issuer. If not provided, the existing
+ * configuration's value is not modified.
+ */
+ issuer?: string;
+
+ /**
+ * The OIDC provider's client secret to enable OIDC code flow.
+ * If not provided, the existing configuration's value is not modified.
+ */
+ clientSecret?: string;
+
+ /**
+ * The OIDC provider's response object for OAuth authorization flow.
+ */
+ responseType?: OAuthResponseType;
+}
+
+export type UpdateAuthProviderRequest =
+ SAMLUpdateAuthProviderRequest | OIDCUpdateAuthProviderRequest;
+
+/** A maximum of 10 test phone number / code pairs can be configured. */
+export const MAXIMUM_TEST_PHONE_NUMBERS = 10;
+
+/** The server side SAML configuration request interface. */
+export interface SAMLConfigServerRequest {
+ idpConfig?: {
+ idpEntityId?: string;
+ ssoUrl?: string;
+ idpCertificates?: Array<{
+ x509Certificate: string;
+ }>;
+ signRequest?: boolean;
+ };
+ spConfig?: {
+ spEntityId?: string;
+ callbackUri?: string;
+ };
+ displayName?: string;
+ enabled?: boolean;
+ [key: string]: any;
+}
+
+/** The server side SAML configuration response interface. */
+export interface SAMLConfigServerResponse {
+ // Used when getting config.
+ // projects/${projectId}/inboundSamlConfigs/${providerId}
+ name?: string;
+ idpConfig?: {
+ idpEntityId?: string;
+ ssoUrl?: string;
+ idpCertificates?: Array<{
+ x509Certificate: string;
+ }>;
+ signRequest?: boolean;
+ };
+ spConfig?: {
+ spEntityId?: string;
+ callbackUri?: string;
+ };
+ displayName?: string;
+ enabled?: boolean;
+}
+
+/** The server side OIDC configuration request interface. */
+export interface OIDCConfigServerRequest {
+ clientId?: string;
+ issuer?: string;
+ displayName?: string;
+ enabled?: boolean;
+ clientSecret?: string;
+ responseType?: OAuthResponseType;
+ [key: string]: any;
+}
+
+/** The server side OIDC configuration response interface. */
+export interface OIDCConfigServerResponse {
+ // Used when getting config.
+ // projects/${projectId}/oauthIdpConfigs/${providerId}
+ name?: string;
+ clientId?: string;
+ issuer?: string;
+ displayName?: string;
+ enabled?: boolean;
+ clientSecret?: string;
+ responseType?: OAuthResponseType;
+}
+
+/** The server side email configuration request interface. */
+export interface EmailSignInConfigServerRequest {
+ allowPasswordSignup?: boolean;
+ enableEmailLinkSignin?: boolean;
+}
+
+/** Identifies the server side second factor type. */
+type AuthFactorServerType = 'PHONE_SMS';
+
+/** Client Auth factor type to server auth factor type mapping. */
+const AUTH_FACTOR_CLIENT_TO_SERVER_TYPE: {[key: string]: AuthFactorServerType} = {
+ phone: 'PHONE_SMS',
+};
+
+/** Server Auth factor type to client auth factor type mapping. */
+const AUTH_FACTOR_SERVER_TO_CLIENT_TYPE: {[key: string]: AuthFactorType} =
+ Object.keys(AUTH_FACTOR_CLIENT_TO_SERVER_TYPE)
+ .reduce((res: {[key: string]: AuthFactorType}, key) => {
+ res[AUTH_FACTOR_CLIENT_TO_SERVER_TYPE[key]] = key as AuthFactorType;
+ return res;
+ }, {});
+
+/** Server side multi-factor configuration. */
+export interface MultiFactorAuthServerConfig {
+ state?: MultiFactorConfigState;
+ enabledProviders?: AuthFactorServerType[];
+ providerConfigs?: MultiFactorProviderConfig[];
+}
+
+/**
+ * Identifies a second factor type.
+ */
+export type AuthFactorType = 'phone';
+
+/**
+ * Identifies a multi-factor configuration state.
+ */
+export type MultiFactorConfigState = 'ENABLED' | 'DISABLED';
+
+/**
+ * Interface representing a multi-factor configuration.
+ * This can be used to define whether multi-factor authentication is enabled
+ * or disabled and the list of second factor challenges that are supported.
+ */
+export interface MultiFactorConfig {
+ /**
+ * The multi-factor config state.
+ */
+ state: MultiFactorConfigState;
+
+ /**
+ * The list of identifiers for enabled second factors.
+ * Currently only ‘phone’ is supported.
+ */
+ factorIds?: AuthFactorType[];
+
+ /**
+ * A list of multi-factor provider configurations.
+ * MFA providers (except phone) indicate whether they're enabled through this field. */
+ providerConfigs?: MultiFactorProviderConfig[];
+}
+
+/**
+ * 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.
+ */export interface MultiFactorProviderConfig {
+ /**
+ * Indicates whether this multi-factor provider is enabled or disabled. */
+ state: MultiFactorConfigState;
+ /**
+ * TOTP multi-factor provider config. */
+ totpProviderConfig?: TotpMultiFactorProviderConfig;
+}
+
+/**
+ * Interface representing configuration settings for TOTP second factor auth.
+ */
+export interface TotpMultiFactorProviderConfig {
+ /**
+ * The allowed number of adjacent intervals that will be used for verification
+ * to compensate for clock skew. */
+ adjacentIntervals?: number;
+}
+
+/**
+ * Defines the multi-factor config class used to convert client side MultiFactorConfig
+ * to a format that is understood by the Auth server.
+ *
+ * @internal
+ */
+export class MultiFactorAuthConfig implements MultiFactorConfig {
+
+ /**
+ * The multi-factor config state.
+ */
+ public readonly state: MultiFactorConfigState;
+ /**
+ * The list of identifiers for enabled second factors.
+ * Currently only ‘phone’ is supported.
+ */
+ public readonly factorIds: AuthFactorType[];
+ /**
+ * A list of multi-factor provider specific config.
+ * New MFA providers (except phone) will indicate enablement/disablement through this field.
+ */
+ public readonly providerConfigs: MultiFactorProviderConfig[];
+
+ /**
+ * Static method to convert a client side request to a MultiFactorAuthServerConfig.
+ * Throws an error if validation fails.
+ *
+ * @param options - The options object to convert to a server request.
+ * @returns The resulting server request.
+ * @internal
+ */
+ public static buildServerRequest(options: MultiFactorConfig): MultiFactorAuthServerConfig {
+ const request: MultiFactorAuthServerConfig = {};
+ MultiFactorAuthConfig.validate(options);
+ if (Object.prototype.hasOwnProperty.call(options, 'state')) {
+ request.state = options.state;
+ }
+ if (Object.prototype.hasOwnProperty.call(options, 'factorIds')) {
+ (options.factorIds || []).forEach((factorId) => {
+ if (typeof request.enabledProviders === 'undefined') {
+ request.enabledProviders = [];
+ }
+ request.enabledProviders.push(AUTH_FACTOR_CLIENT_TO_SERVER_TYPE[factorId]);
+ });
+ // In case an empty array is passed. Ensure it gets populated so the array is cleared.
+ if (options.factorIds && options.factorIds.length === 0) {
+ request.enabledProviders = [];
+ }
+ }
+ if (Object.prototype.hasOwnProperty.call(options, 'providerConfigs')) {
+ request.providerConfigs = options.providerConfigs;
+ }
+ return request;
+ }
+
+ /**
+ * Validates the MultiFactorConfig options object. Throws an error on failure.
+ *
+ * @param options - The options object to validate.
+ */
+ public static validate(options: MultiFactorConfig): void {
+ const validKeys = {
+ state: true,
+ factorIds: true,
+ providerConfigs: true,
+ };
+ if (!validator.isNonNullObject(options)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"MultiFactorConfig" must be a non-null object.',
+ );
+ }
+ // Check for unsupported top level attributes.
+ for (const key in options) {
+ if (!(key in validKeys)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${key}" is not a valid MultiFactorConfig parameter.`,
+ );
+ }
+ }
+ // Validate content.
+ if (typeof options.state !== 'undefined' &&
+ options.state !== 'ENABLED' &&
+ options.state !== 'DISABLED') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"MultiFactorConfig.state" must be either "ENABLED" or "DISABLED".',
+ );
+ }
+
+ if (typeof options.factorIds !== 'undefined') {
+ if (!validator.isArray(options.factorIds)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"MultiFactorConfig.factorIds" must be an array of valid "AuthFactorTypes".',
+ );
+ }
+
+ // Validate content of array.
+ options.factorIds.forEach((factorId) => {
+ if (typeof AUTH_FACTOR_CLIENT_TO_SERVER_TYPE[factorId] === 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${factorId}" is not a valid "AuthFactorType".`,
+ );
+ }
+ });
+ }
+
+ if (typeof options.providerConfigs !== 'undefined') {
+ if (!validator.isArray(options.providerConfigs)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"MultiFactorConfig.providerConfigs" must be an array of valid "MultiFactorProviderConfig."',
+ );
+ }
+ //Validate content of array.
+ options.providerConfigs.forEach((multiFactorProviderConfig) => {
+ if (typeof multiFactorProviderConfig === 'undefined' || !validator.isObject(multiFactorProviderConfig)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${multiFactorProviderConfig}" is not a valid "MultiFactorProviderConfig" type.`
+ )
+ }
+ const validProviderConfigKeys = {
+ state: true,
+ totpProviderConfig: true,
+ };
+ for (const key in multiFactorProviderConfig) {
+ if (!(key in validProviderConfigKeys)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${key}" is not a valid ProviderConfig parameter.`,
+ );
+ }
+ }
+ if (typeof multiFactorProviderConfig.state === 'undefined' ||
+ (multiFactorProviderConfig.state !== 'ENABLED' &&
+ multiFactorProviderConfig.state !== 'DISABLED')) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"MultiFactorConfig.providerConfigs.state" must be either "ENABLED" or "DISABLED".',
+ )
+ }
+ // Since TOTP is the only provider config available right now, not defining it will lead into an error
+ if (typeof multiFactorProviderConfig.totpProviderConfig === 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"MultiFactorConfig.providerConfigs.totpProviderConfig" must be defined.'
+ )
+ }
+ const validTotpProviderConfigKeys = {
+ adjacentIntervals: true,
+ };
+ for (const key in multiFactorProviderConfig.totpProviderConfig) {
+ if (!(key in validTotpProviderConfigKeys)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${key}" is not a valid TotpProviderConfig parameter.`,
+ );
+ }
+ }
+ const adjIntervals = multiFactorProviderConfig.totpProviderConfig.adjacentIntervals
+ if (typeof adjIntervals !== 'undefined' &&
+ (!Number.isInteger(adjIntervals) || adjIntervals < 0 || adjIntervals > 10)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"MultiFactorConfig.providerConfigs.totpProviderConfig.adjacentIntervals" must' +
+ ' be a valid number between 0 and 10 (both inclusive).'
+ )
+ }
+ });
+ }
+ }
+
+ /**
+ * The MultiFactorAuthConfig constructor.
+ *
+ * @param response - The server side response used to initialize the
+ * MultiFactorAuthConfig object.
+ * @constructor
+ * @internal
+ */
+ constructor(response: MultiFactorAuthServerConfig) {
+ if (typeof response.state === 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Invalid multi-factor configuration response');
+ }
+ this.state = response.state;
+ this.factorIds = [];
+ (response.enabledProviders || []).forEach((enabledProvider) => {
+ // Ignore unsupported types. It is possible the current admin SDK version is
+ // not up to date and newer backend types are supported.
+ if (typeof AUTH_FACTOR_SERVER_TO_CLIENT_TYPE[enabledProvider] !== 'undefined') {
+ this.factorIds.push(AUTH_FACTOR_SERVER_TO_CLIENT_TYPE[enabledProvider]);
+ }
+ })
+ this.providerConfigs = [];
+ (response.providerConfigs || []).forEach((providerConfig) => {
+ if (typeof providerConfig !== 'undefined') {
+ if (typeof providerConfig.state === 'undefined' ||
+ typeof providerConfig.totpProviderConfig === 'undefined' ||
+ (typeof providerConfig.totpProviderConfig.adjacentIntervals !== 'undefined' &&
+ typeof providerConfig.totpProviderConfig.adjacentIntervals !== 'number')) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Invalid multi-factor configuration response');
+ }
+ this.providerConfigs.push(providerConfig);
+ }
+ })
+ }
+
+ /** Converts MultiFactorConfig to JSON object
+ * @returns The plain object representation of the multi-factor config instance. */
+ public toJSON(): object {
+ return {
+ state: this.state,
+ factorIds: this.factorIds,
+ providerConfigs: this.providerConfigs,
+ };
+ }
+}
+
+
+/**
+ * Validates the provided map of test phone number / code pairs.
+ * @param testPhoneNumbers - The phone number / code pairs to validate.
+ */
+export function validateTestPhoneNumbers(
+ testPhoneNumbers: {[phoneNumber: string]: string},
+): void {
+ if (!validator.isObject(testPhoneNumbers)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"testPhoneNumbers" must be a map of phone number / code pairs.',
+ );
+ }
+ if (Object.keys(testPhoneNumbers).length > MAXIMUM_TEST_PHONE_NUMBERS) {
+ throw new FirebaseAuthError(AuthClientErrorCode.MAXIMUM_TEST_PHONE_NUMBER_EXCEEDED);
+ }
+ for (const phoneNumber in testPhoneNumbers) {
+ // Validate phone number.
+ if (!validator.isPhoneNumber(phoneNumber)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_TESTING_PHONE_NUMBER,
+ `"${phoneNumber}" is not a valid E.164 standard compliant phone number.`
+ );
+ }
+
+ // Validate code.
+ if (!validator.isString(testPhoneNumbers[phoneNumber]) ||
+ !/^[\d]{6}$/.test(testPhoneNumbers[phoneNumber])) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_TESTING_PHONE_NUMBER,
+ `"${testPhoneNumbers[phoneNumber]}" is not a valid 6 digit code string.`
+ );
+ }
+ }
+}
+
+/**
+ * The email sign in provider configuration.
+ */
+export interface EmailSignInProviderConfig {
+ /**
+ * Whether email provider is enabled.
+ */
+ enabled: boolean;
+
+ /**
+ * 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.
+ */
+ passwordRequired?: boolean; // In the backend API, default is true if not provided
+}
+
+
+/**
+ * Defines the email sign-in config class used to convert client side EmailSignInConfig
+ * to a format that is understood by the Auth server.
+ *
+ * @internal
+ */
+export class EmailSignInConfig implements EmailSignInProviderConfig {
+ public readonly enabled: boolean;
+ public readonly passwordRequired?: boolean;
+
+ /**
+ * Static method to convert a client side request to a EmailSignInConfigServerRequest.
+ * Throws an error if validation fails.
+ *
+ * @param options - The options object to convert to a server request.
+ * @returns The resulting server request.
+ * @internal
+ */
+ public static buildServerRequest(options: EmailSignInProviderConfig): EmailSignInConfigServerRequest {
+ const request: EmailSignInConfigServerRequest = {};
+ EmailSignInConfig.validate(options);
+ if (Object.prototype.hasOwnProperty.call(options, 'enabled')) {
+ request.allowPasswordSignup = options.enabled;
+ }
+ if (Object.prototype.hasOwnProperty.call(options, 'passwordRequired')) {
+ request.enableEmailLinkSignin = !options.passwordRequired;
+ }
+ return request;
+ }
+
+ /**
+ * Validates the EmailSignInConfig options object. Throws an error on failure.
+ *
+ * @param options - The options object to validate.
+ */
+ private static validate(options: EmailSignInProviderConfig): void {
+ // TODO: Validate the request.
+ const validKeys = {
+ enabled: true,
+ passwordRequired: true,
+ };
+ if (!validator.isNonNullObject(options)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"EmailSignInConfig" must be a non-null object.',
+ );
+ }
+ // Check for unsupported top level attributes.
+ for (const key in options) {
+ if (!(key in validKeys)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ `"${key}" is not a valid EmailSignInConfig parameter.`,
+ );
+ }
+ }
+ // Validate content.
+ if (typeof options.enabled !== 'undefined' &&
+ !validator.isBoolean(options.enabled)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"EmailSignInConfig.enabled" must be a boolean.',
+ );
+ }
+ if (typeof options.passwordRequired !== 'undefined' &&
+ !validator.isBoolean(options.passwordRequired)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"EmailSignInConfig.passwordRequired" must be a boolean.',
+ );
+ }
+ }
+
+ /**
+ * The EmailSignInConfig constructor.
+ *
+ * @param response - The server side response used to initialize the
+ * EmailSignInConfig object.
+ * @constructor
+ */
+ constructor(response: {[key: string]: any}) {
+ if (typeof response.allowPasswordSignup === 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Invalid email sign-in configuration response');
+ }
+ this.enabled = response.allowPasswordSignup;
+ this.passwordRequired = !response.enableEmailLinkSignin;
+ }
+
+ /** @returns The plain object representation of the email sign-in config. */
+ public toJSON(): object {
+ return {
+ enabled: this.enabled,
+ passwordRequired: this.passwordRequired,
+ };
+ }
+}
+
+/**
+ * The base Auth provider configuration interface.
+ */
+export interface BaseAuthProviderConfig {
+
+ /**
+ * The provider ID defined by the developer.
+ * For a SAML provider, this is always prefixed by `saml.`.
+ * For an OIDC provider, this is always prefixed by `oidc.`.
+ */
+ providerId: string;
+
+ /**
+ * The user-friendly display name to the current configuration. This name is
+ * also used as the provider label in the Cloud Console.
+ */
+ displayName?: string;
+
+ /**
+ * Whether the provider configuration is enabled or disabled. A user
+ * cannot sign in using a disabled provider.
+ */
+ enabled: boolean;
+}
+
+/**
+ * The
+ * [SAML](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html)
+ * Auth provider configuration interface. A SAML provider can be created via
+ * {@link BaseAuth.createProviderConfig}.
+ */
+export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig {
+
+ /**
+ * The SAML IdP entity identifier.
+ */
+ idpEntityId: string;
+
+ /**
+ * The SAML IdP SSO URL. This must be a valid URL.
+ */
+ ssoURL: string;
+
+ /**
+ * The list of SAML IdP X.509 certificates issued by CA for this provider.
+ * Multiple certificates are accepted to prevent outages during
+ * IdP key rotation (for example ADFS rotates every 10 days). When the Auth
+ * server receives a SAML response, it will match the SAML response with the
+ * certificate on record. Otherwise the response is rejected.
+ * Developers are expected to manage the certificate updates as keys are
+ * rotated.
+ */
+ x509Certificates: string[];
+
+ /**
+ * The SAML relying party (service provider) entity ID.
+ * This is defined by the developer but needs to be provided to the SAML IdP.
+ */
+ rpEntityId: string;
+
+ /**
+ * This is fixed and must always be the same as the OAuth redirect URL
+ * provisioned by Firebase Auth,
+ * `https://project-id.firebaseapp.com/__/auth/handler` unless a custom
+ * `authDomain` is used.
+ * The callback URL should also be provided to the SAML IdP during
+ * configuration.
+ */
+ callbackURL?: 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.
+ *
+ */
+export interface OAuthResponseType {
+ /**
+ * Whether ID token is returned from IdP's authorization endpoint.
+ */
+ idToken?: boolean;
+
+ /**
+ * Whether authorization code is returned from IdP's authorization endpoint.
+ */
+ code?: boolean;
+}
+
+/**
+ * The [OIDC](https://openid.net/specs/openid-connect-core-1_0-final.html) Auth
+ * provider configuration interface. An OIDC provider can be created via
+ * {@link BaseAuth.createProviderConfig}.
+ */
+export interface OIDCAuthProviderConfig extends BaseAuthProviderConfig {
+
+ /**
+ * 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).
+ */
+ clientId: string;
+
+ /**
+ * 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).
+ */
+ issuer: string;
+
+ /**
+ * The OIDC provider's client secret to enable OIDC code flow.
+ */
+ clientSecret?: string;
+
+ /**
+ * The OIDC provider's response object for OAuth authorization flow.
+ */
+ responseType?: OAuthResponseType;
+}
+
+/**
+ * The Auth provider configuration type.
+ * {@link BaseAuth.createProviderConfig}.
+ */
+export type AuthProviderConfig = SAMLAuthProviderConfig | OIDCAuthProviderConfig;
+
+/**
+ * Defines the SAMLConfig class used to convert a client side configuration to its
+ * server side representation.
+ *
+ * @internal
+ */
+export class SAMLConfig implements SAMLAuthProviderConfig {
+ public readonly enabled: boolean;
+ public readonly displayName?: string;
+ public readonly providerId: string;
+ public readonly idpEntityId: string;
+ public readonly ssoURL: string;
+ public readonly x509Certificates: string[];
+ public readonly rpEntityId: string;
+ public readonly callbackURL?: string;
+ public readonly enableRequestSigning?: boolean;
+
+ /**
+ * Converts a client side request to a SAMLConfigServerRequest which is the format
+ * accepted by the backend server.
+ * Throws an error if validation fails. If the request is not a SAMLConfig request,
+ * returns null.
+ *
+ * @param options - The options object to convert to a server request.
+ * @param ignoreMissingFields - Whether to ignore missing fields.
+ * @returns The resulting server request or null if not valid.
+ */
+ public static buildServerRequest(
+ options: Partial,
+ ignoreMissingFields = false): SAMLConfigServerRequest | null {
+ const makeRequest = validator.isNonNullObject(options) &&
+ (options.providerId || ignoreMissingFields);
+ if (!makeRequest) {
+ return null;
+ }
+ const request: SAMLConfigServerRequest = {};
+ // Validate options.
+ SAMLConfig.validate(options, ignoreMissingFields);
+ request.enabled = options.enabled;
+ request.displayName = options.displayName;
+ // IdP config.
+ if (options.idpEntityId || options.ssoURL || options.x509Certificates) {
+ request.idpConfig = {
+ idpEntityId: options.idpEntityId,
+ ssoUrl: options.ssoURL,
+ signRequest: (options as any).enableRequestSigning,
+ idpCertificates: typeof options.x509Certificates === 'undefined' ? undefined : [],
+ };
+ if (options.x509Certificates) {
+ for (const cert of (options.x509Certificates || [])) {
+ request.idpConfig!.idpCertificates!.push({ x509Certificate: cert });
+ }
+ }
+ }
+ // RP config.
+ if (options.callbackURL || options.rpEntityId) {
+ request.spConfig = {
+ spEntityId: options.rpEntityId,
+ callbackUri: options.callbackURL,
+ };
+ }
+ return request;
+ }
+
+ /**
+ * Returns the provider ID corresponding to the resource name if available.
+ *
+ * @param resourceName - The server side resource name.
+ * @returns The provider ID corresponding to the resource, null otherwise.
+ */
+ public static getProviderIdFromResourceName(resourceName: string): string | null {
+ // name is of form projects/project1/inboundSamlConfigs/providerId1
+ const matchProviderRes = resourceName.match(/\/inboundSamlConfigs\/(saml\..*)$/);
+ if (!matchProviderRes || matchProviderRes.length < 2) {
+ return null;
+ }
+ return matchProviderRes[1];
+ }
+
+ /**
+ * @param providerId - The provider ID to check.
+ * @returns Whether the provider ID corresponds to a SAML provider.
+ */
+ public static isProviderId(providerId: any): providerId is string {
+ return validator.isNonEmptyString(providerId) && providerId.indexOf('saml.') === 0;
+ }
+
+ /**
+ * Validates the SAMLConfig options object. Throws an error on failure.
+ *
+ * @param options - The options object to validate.
+ * @param ignoreMissingFields - Whether to ignore missing fields.
+ */
+ public static validate(options: Partial, ignoreMissingFields = false): void {
+ const validKeys = {
+ enabled: true,
+ displayName: true,
+ providerId: true,
+ idpEntityId: true,
+ ssoURL: true,
+ x509Certificates: true,
+ rpEntityId: true,
+ callbackURL: true,
+ enableRequestSigning: true,
+ };
+ if (!validator.isNonNullObject(options)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"SAMLAuthProviderConfig" must be a valid non-null object.',
+ );
+ }
+ // Check for unsupported top level attributes.
+ for (const key in options) {
+ if (!(key in validKeys)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${key}" is not a valid SAML config parameter.`,
+ );
+ }
+ }
+ // Required fields.
+ if (validator.isNonEmptyString(options.providerId)) {
+ if (options.providerId.indexOf('saml.') !== 0) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_PROVIDER_ID,
+ '"SAMLAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "saml.".',
+ );
+ }
+ } else if (!ignoreMissingFields) {
+ // providerId is required and not provided correctly.
+ throw new FirebaseAuthError(
+ !options.providerId ? AuthClientErrorCode.MISSING_PROVIDER_ID : AuthClientErrorCode.INVALID_PROVIDER_ID,
+ '"SAMLAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "saml.".',
+ );
+ }
+ if (!(ignoreMissingFields && typeof options.idpEntityId === 'undefined') &&
+ !validator.isNonEmptyString(options.idpEntityId)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"SAMLAuthProviderConfig.idpEntityId" must be a valid non-empty string.',
+ );
+ }
+ if (!(ignoreMissingFields && typeof options.ssoURL === 'undefined') &&
+ !validator.isURL(options.ssoURL)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"SAMLAuthProviderConfig.ssoURL" must be a valid URL string.',
+ );
+ }
+ if (!(ignoreMissingFields && typeof options.rpEntityId === 'undefined') &&
+ !validator.isNonEmptyString(options.rpEntityId)) {
+ throw new FirebaseAuthError(
+ !options.rpEntityId ? AuthClientErrorCode.MISSING_SAML_RELYING_PARTY_CONFIG :
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"SAMLAuthProviderConfig.rpEntityId" must be a valid non-empty string.',
+ );
+ }
+ if (!(ignoreMissingFields && typeof options.callbackURL === 'undefined') &&
+ !validator.isURL(options.callbackURL)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"SAMLAuthProviderConfig.callbackURL" must be a valid URL string.',
+ );
+ }
+ if (!(ignoreMissingFields && typeof options.x509Certificates === 'undefined') &&
+ !validator.isArray(options.x509Certificates)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"SAMLAuthProviderConfig.x509Certificates" must be a valid array of X509 certificate strings.',
+ );
+ }
+ (options.x509Certificates || []).forEach((cert: string) => {
+ if (!validator.isNonEmptyString(cert)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"SAMLAuthProviderConfig.x509Certificates" must be a valid array of X509 certificate strings.',
+ );
+ }
+ });
+ if (typeof (options as any).enableRequestSigning !== 'undefined' &&
+ !validator.isBoolean((options as any).enableRequestSigning)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"SAMLAuthProviderConfig.enableRequestSigning" must be a boolean.',
+ );
+ }
+ if (typeof options.enabled !== 'undefined' &&
+ !validator.isBoolean(options.enabled)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"SAMLAuthProviderConfig.enabled" must be a boolean.',
+ );
+ }
+ if (typeof options.displayName !== 'undefined' &&
+ !validator.isString(options.displayName)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"SAMLAuthProviderConfig.displayName" must be a valid string.',
+ );
+ }
+ }
+
+ /**
+ * The SAMLConfig constructor.
+ *
+ * @param response - The server side response used to initialize the SAMLConfig object.
+ * @constructor
+ */
+ constructor(response: SAMLConfigServerResponse) {
+ if (!response ||
+ !response.idpConfig ||
+ !response.idpConfig.idpEntityId ||
+ !response.idpConfig.ssoUrl ||
+ !response.spConfig ||
+ !response.spConfig.spEntityId ||
+ !response.name ||
+ !(validator.isString(response.name) &&
+ SAMLConfig.getProviderIdFromResourceName(response.name))) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Invalid SAML configuration response');
+ }
+
+ const providerId = SAMLConfig.getProviderIdFromResourceName(response.name);
+ if (!providerId) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Invalid SAML configuration response');
+ }
+ this.providerId = providerId;
+
+ // RP config.
+ this.rpEntityId = response.spConfig.spEntityId;
+ this.callbackURL = response.spConfig.callbackUri;
+ // IdP config.
+ this.idpEntityId = response.idpConfig.idpEntityId;
+ this.ssoURL = response.idpConfig.ssoUrl;
+ this.enableRequestSigning = !!response.idpConfig.signRequest;
+ const x509Certificates: string[] = [];
+ for (const cert of (response.idpConfig.idpCertificates || [])) {
+ if (cert.x509Certificate) {
+ x509Certificates.push(cert.x509Certificate);
+ }
+ }
+ this.x509Certificates = x509Certificates;
+ // When enabled is undefined, it takes its default value of false.
+ this.enabled = !!response.enabled;
+ this.displayName = response.displayName;
+ }
+
+ /** @returns The plain object representation of the SAMLConfig. */
+ public toJSON(): object {
+ return {
+ enabled: this.enabled,
+ displayName: this.displayName,
+ providerId: this.providerId,
+ idpEntityId: this.idpEntityId,
+ ssoURL: this.ssoURL,
+ x509Certificates: deepCopy(this.x509Certificates),
+ rpEntityId: this.rpEntityId,
+ callbackURL: this.callbackURL,
+ enableRequestSigning: this.enableRequestSigning,
+ };
+ }
+}
+
+/**
+ * Defines the OIDCConfig class used to convert a client side configuration to its
+ * server side representation.
+ *
+ * @internal
+ */
+export class OIDCConfig implements OIDCAuthProviderConfig {
+ public readonly enabled: boolean;
+ public readonly displayName?: string;
+ public readonly providerId: string;
+ public readonly issuer: string;
+ public readonly clientId: string;
+ public readonly clientSecret?: string;
+ public readonly responseType: OAuthResponseType;
+
+ /**
+ * Converts a client side request to a OIDCConfigServerRequest which is the format
+ * accepted by the backend server.
+ * Throws an error if validation fails. If the request is not a OIDCConfig request,
+ * returns null.
+ *
+ * @param options - The options object to convert to a server request.
+ * @param ignoreMissingFields - Whether to ignore missing fields.
+ * @returns The resulting server request or null if not valid.
+ */
+ public static buildServerRequest(
+ options: Partial,
+ ignoreMissingFields = false): OIDCConfigServerRequest | null {
+ const makeRequest = validator.isNonNullObject(options) &&
+ (options.providerId || ignoreMissingFields);
+ if (!makeRequest) {
+ return null;
+ }
+ const request: OIDCConfigServerRequest = {};
+ // Validate options.
+ OIDCConfig.validate(options, ignoreMissingFields);
+ request.enabled = options.enabled;
+ request.displayName = options.displayName;
+ request.issuer = options.issuer;
+ request.clientId = options.clientId;
+ if (typeof options.clientSecret !== 'undefined') {
+ request.clientSecret = options.clientSecret;
+ }
+ if (typeof options.responseType !== 'undefined') {
+ request.responseType = options.responseType;
+ }
+ return request;
+ }
+
+ /**
+ * Returns the provider ID corresponding to the resource name if available.
+ *
+ * @param resourceName - The server side resource name
+ * @returns The provider ID corresponding to the resource, null otherwise.
+ */
+ public static getProviderIdFromResourceName(resourceName: string): string | null {
+ // name is of form projects/project1/oauthIdpConfigs/providerId1
+ const matchProviderRes = resourceName.match(/\/oauthIdpConfigs\/(oidc\..*)$/);
+ if (!matchProviderRes || matchProviderRes.length < 2) {
+ return null;
+ }
+ return matchProviderRes[1];
+ }
+
+ /**
+ * @param providerId - The provider ID to check.
+ * @returns Whether the provider ID corresponds to an OIDC provider.
+ */
+ public static isProviderId(providerId: any): providerId is string {
+ return validator.isNonEmptyString(providerId) && providerId.indexOf('oidc.') === 0;
+ }
+
+ /**
+ * Validates the OIDCConfig options object. Throws an error on failure.
+ *
+ * @param options - The options object to validate.
+ * @param ignoreMissingFields - Whether to ignore missing fields.
+ */
+ public static validate(options: Partial, ignoreMissingFields = false): void {
+ const validKeys = {
+ enabled: true,
+ displayName: true,
+ providerId: true,
+ clientId: true,
+ issuer: true,
+ clientSecret: true,
+ responseType: true,
+ };
+ const validResponseTypes = {
+ idToken: true,
+ code: true,
+ };
+ if (!validator.isNonNullObject(options)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"OIDCAuthProviderConfig" must be a valid non-null object.',
+ );
+ }
+ // Check for unsupported top level attributes.
+ for (const key in options) {
+ if (!(key in validKeys)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${key}" is not a valid OIDC config parameter.`,
+ );
+ }
+ }
+ // Required fields.
+ if (validator.isNonEmptyString(options.providerId)) {
+ if (options.providerId.indexOf('oidc.') !== 0) {
+ throw new FirebaseAuthError(
+ !options.providerId ? AuthClientErrorCode.MISSING_PROVIDER_ID : AuthClientErrorCode.INVALID_PROVIDER_ID,
+ '"OIDCAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "oidc.".',
+ );
+ }
+ } else if (!ignoreMissingFields) {
+ throw new FirebaseAuthError(
+ !options.providerId ? AuthClientErrorCode.MISSING_PROVIDER_ID : AuthClientErrorCode.INVALID_PROVIDER_ID,
+ '"OIDCAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "oidc.".',
+ );
+ }
+ if (!(ignoreMissingFields && typeof options.clientId === 'undefined') &&
+ !validator.isNonEmptyString(options.clientId)) {
+ throw new FirebaseAuthError(
+ !options.clientId ? AuthClientErrorCode.MISSING_OAUTH_CLIENT_ID : AuthClientErrorCode.INVALID_OAUTH_CLIENT_ID,
+ '"OIDCAuthProviderConfig.clientId" must be a valid non-empty string.',
+ );
+ }
+ if (!(ignoreMissingFields && typeof options.issuer === 'undefined') &&
+ !validator.isURL(options.issuer)) {
+ throw new FirebaseAuthError(
+ !options.issuer ? AuthClientErrorCode.MISSING_ISSUER : AuthClientErrorCode.INVALID_CONFIG,
+ '"OIDCAuthProviderConfig.issuer" must be a valid URL string.',
+ );
+ }
+ if (typeof options.enabled !== 'undefined' &&
+ !validator.isBoolean(options.enabled)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"OIDCAuthProviderConfig.enabled" must be a boolean.',
+ );
+ }
+ if (typeof options.displayName !== 'undefined' &&
+ !validator.isString(options.displayName)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"OIDCAuthProviderConfig.displayName" must be a valid string.',
+ );
+ }
+ if (typeof options.clientSecret !== 'undefined' &&
+ !validator.isNonEmptyString(options.clientSecret)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"OIDCAuthProviderConfig.clientSecret" must be a valid string.',
+ );
+ }
+ if (validator.isNonNullObject(options.responseType) && typeof options.responseType !== 'undefined') {
+ Object.keys(options.responseType).forEach((key) => {
+ if (!(key in validResponseTypes)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${key}" is not a valid OAuthResponseType parameter.`,
+ );
+ }
+ });
+
+ const idToken = options.responseType.idToken;
+ if (typeof idToken !== 'undefined' && !validator.isBoolean(idToken)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"OIDCAuthProviderConfig.responseType.idToken" must be a boolean.',
+ );
+ }
+ const code = options.responseType.code;
+ if (typeof code !== 'undefined') {
+ if (!validator.isBoolean(code)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"OIDCAuthProviderConfig.responseType.code" must be a boolean.',
+ );
+ }
+ // If code flow is enabled, client secret must be provided.
+ if (code && typeof options.clientSecret === 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.MISSING_OAUTH_CLIENT_SECRET,
+ 'The OAuth configuration client secret is required to enable OIDC code flow.',
+ );
+ }
+ }
+
+ const allKeys = Object.keys(options.responseType).length;
+ const enabledCount = Object.values(options.responseType).filter(Boolean).length;
+ // Only one of OAuth response types can be set to true.
+ if (allKeys > 1 && enabledCount !== 1) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_OAUTH_RESPONSETYPE,
+ 'Only exactly one OAuth responseType should be set to true.',
+ );
+ }
+ }
+ }
+
+ /**
+ * The OIDCConfig constructor.
+ *
+ * @param response - The server side response used to initialize the OIDCConfig object.
+ * @constructor
+ */
+ constructor(response: OIDCConfigServerResponse) {
+ if (!response ||
+ !response.issuer ||
+ !response.clientId ||
+ !response.name ||
+ !(validator.isString(response.name) &&
+ OIDCConfig.getProviderIdFromResourceName(response.name))) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Invalid OIDC configuration response');
+ }
+
+ const providerId = OIDCConfig.getProviderIdFromResourceName(response.name);
+ if (!providerId) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Invalid SAML configuration response');
+ }
+ this.providerId = providerId;
+
+ this.clientId = response.clientId;
+ this.issuer = response.issuer;
+ // When enabled is undefined, it takes its default value of false.
+ this.enabled = !!response.enabled;
+ this.displayName = response.displayName;
+
+ if (typeof response.clientSecret !== 'undefined') {
+ this.clientSecret = response.clientSecret;
+ }
+ if (typeof response.responseType !== 'undefined') {
+ this.responseType = response.responseType;
+ }
+ }
+
+ /** @returns The plain object representation of the OIDCConfig. */
+ public toJSON(): OIDCAuthProviderConfig {
+ return {
+ enabled: this.enabled,
+ displayName: this.displayName,
+ providerId: this.providerId,
+ issuer: this.issuer,
+ clientId: this.clientId,
+ clientSecret: deepCopy(this.clientSecret),
+ responseType: deepCopy(this.responseType),
+ };
+ }
+}
+
+/**
+ * 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.
+ */
+export type SmsRegionConfig = AllowByDefaultWrap | AllowlistOnlyWrap;
+
+/**
+ * Mutual exclusive SMS Region Config of AllowByDefault interface
+ */
+export interface AllowByDefaultWrap {
+ /**
+ * Allow every region by default.
+ */
+ allowByDefault: AllowByDefault;
+ /** @alpha */
+ allowlistOnly?: never;
+}
+
+/**
+ * Mutually exclusive SMS Region Config of AllowlistOnly interface
+ */
+export interface AllowlistOnlyWrap {
+ /**
+ * Only allowing regions by explicitly adding them to an
+ * allowlist.
+ */
+ allowlistOnly: AllowlistOnly;
+ /** @alpha */
+ allowByDefault?: never;
+}
+
+/**
+ * Defines a policy of allowing every region by default and adding disallowed
+ * regions to a disallow list.
+ */
+export interface AllowByDefault {
+ /**
+ * Two letter unicode region codes to disallow as defined by
+ * https://cldr.unicode.org/
+ * The full list of these region codes is here:
+ * https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json
+ */
+ disallowedRegions: string[];
+}
+
+/**
+ * Defines a policy of only allowing regions by explicitly adding them to an
+ * allowlist.
+ */
+export interface AllowlistOnly {
+ /**
+ * Two letter unicode region codes to allow as defined by
+ * https://cldr.unicode.org/
+ * The full list of these region codes is here:
+ * https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json
+ */
+ allowedRegions: string[];
+}
+
+/**
+ * Defines the SMSRegionConfig class used for validation.
+ *
+ * @internal
+ */
+export class SmsRegionsAuthConfig {
+ public static validate(options: SmsRegionConfig): void {
+ if (!validator.isNonNullObject(options)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"SmsRegionConfig" must be a non-null object.',
+ );
+ }
+
+ const validKeys = {
+ allowlistOnly: true,
+ allowByDefault: true,
+ };
+
+ for (const key in options) {
+ if (!(key in validKeys)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${key}" is not a valid SmsRegionConfig parameter.`,
+ );
+ }
+ }
+
+ // validate mutual exclusiveness of allowByDefault and allowlistOnly
+ if (typeof options.allowByDefault !== 'undefined' && typeof options.allowlistOnly !== 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ 'SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.',
+ );
+ }
+ // validation for allowByDefault type
+ if (typeof options.allowByDefault !== 'undefined') {
+ const allowByDefaultValidKeys = {
+ disallowedRegions: true,
+ }
+ for (const key in options.allowByDefault) {
+ if (!(key in allowByDefaultValidKeys)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${key}" is not a valid SmsRegionConfig.allowByDefault parameter.`,
+ );
+ }
+ }
+ // disallowedRegion can be empty.
+ if (typeof options.allowByDefault.disallowedRegions !== 'undefined'
+ && !validator.isArray(options.allowByDefault.disallowedRegions)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.',
+ );
+ }
+ }
+
+ if (typeof options.allowlistOnly !== 'undefined') {
+ const allowListOnlyValidKeys = {
+ allowedRegions: true,
+ }
+ for (const key in options.allowlistOnly) {
+ if (!(key in allowListOnlyValidKeys)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${key}" is not a valid SmsRegionConfig.allowlistOnly parameter.`,
+ );
+ }
+ }
+
+ // allowedRegions can be empty
+ if (typeof options.allowlistOnly.allowedRegions !== 'undefined'
+ && !validator.isArray(options.allowlistOnly.allowedRegions)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.',
+ );
+ }
+ }
+ }
+}
+/**
+* Enforcement state of reCAPTCHA protection.
+* - 'OFF': Unenforced.
+* - 'AUDIT': Create assessment but don't enforce the result.
+* - 'ENFORCE': Create assessment and enforce the result.
+*/
+export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE';
+
+/**
+* The actions to take for reCAPTCHA-protected requests.
+* - 'BLOCK': The reCAPTCHA-protected request will be blocked.
+*/
+export type RecaptchaAction = 'BLOCK';
+
+/**
+ * The config for a reCAPTCHA action rule.
+ */
+export interface RecaptchaManagedRule {
+ /**
+ * The action will be enforced if the reCAPTCHA score of a request is larger than endScore.
+ */
+ endScore: number;
+ /**
+ * The action for reCAPTCHA-protected requests.
+ */
+ action?: RecaptchaAction;
+}
+
+/**
+ * The key's platform type.
+ */
+export type RecaptchaKeyClientType = 'WEB' | 'IOS' | 'ANDROID';
+
+/**
+ * The reCAPTCHA key config.
+ */
+export interface RecaptchaKey {
+ /**
+ * The key's client platform type.
+ */
+ type?: RecaptchaKeyClientType;
+
+ /**
+ * The reCAPTCHA site key.
+ */
+ key: string;
+}
+
+/**
+ * The request interface for updating a reCAPTCHA Config.
+ * By enabling reCAPTCHA Enterprise Integration you are
+ * agreeing to reCAPTCHA Enterprise
+ * {@link https://cloud.google.com/terms/service-terms | Term of Service}.
+ */
+export interface RecaptchaConfig {
+ /**
+ * The enforcement state of the email password provider.
+ */
+ emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
+ /**
+ * The reCAPTCHA managed rules.
+ */
+ managedRules?: RecaptchaManagedRule[];
+
+ /**
+ * The reCAPTCHA keys.
+ */
+ recaptchaKeys?: RecaptchaKey[];
+
+ /**
+ * Whether to use account defender for reCAPTCHA assessment.
+ * The default value is false.
+ */
+ useAccountDefender?: boolean;
+}
+
+export class RecaptchaAuthConfig implements RecaptchaConfig {
+ public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
+ public readonly managedRules?: RecaptchaManagedRule[];
+ public readonly recaptchaKeys?: RecaptchaKey[];
+ public readonly useAccountDefender?: boolean;
+
+ constructor(recaptchaConfig: RecaptchaConfig) {
+ this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState;
+ this.managedRules = recaptchaConfig.managedRules;
+ this.recaptchaKeys = recaptchaConfig.recaptchaKeys;
+ this.useAccountDefender = recaptchaConfig.useAccountDefender;
+ }
+
+ /**
+ * Validates the RecaptchaConfig options object. Throws an error on failure.
+ * @param options - The options object to validate.
+ */
+ public static validate(options: RecaptchaConfig): void {
+ const validKeys = {
+ emailPasswordEnforcementState: true,
+ managedRules: true,
+ recaptchaKeys: true,
+ useAccountDefender: true,
+ };
+
+ if (!validator.isNonNullObject(options)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"RecaptchaConfig" must be a non-null object.',
+ );
+ }
+
+ for (const key in options) {
+ if (!(key in validKeys)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${key}" is not a valid RecaptchaConfig parameter.`,
+ );
+ }
+ }
+
+ // Validation
+ if (typeof options.emailPasswordEnforcementState !== undefined) {
+ if (!validator.isNonEmptyString(options.emailPasswordEnforcementState)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.',
+ );
+ }
+
+ if (options.emailPasswordEnforcementState !== 'OFF' &&
+ options.emailPasswordEnforcementState !== 'AUDIT' &&
+ options.emailPasswordEnforcementState !== 'ENFORCE') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".',
+ );
+ }
+ }
+
+ if (typeof options.managedRules !== 'undefined') {
+ // Validate array
+ if (!validator.isArray(options.managedRules)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".',
+ );
+ }
+ // Validate each rule of the array
+ options.managedRules.forEach((managedRule) => {
+ RecaptchaAuthConfig.validateManagedRule(managedRule);
+ });
+ }
+
+ if (typeof options.useAccountDefender !== 'undefined') {
+ if (!validator.isBoolean(options.useAccountDefender)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"RecaptchaConfig.useAccountDefender" must be a boolean value".',
+ );
+ }
+ }
+ }
+
+ /**
+ * Validate each element in ManagedRule array
+ * @param options - The options object to validate.
+ */
+ private static validateManagedRule(options: RecaptchaManagedRule): void {
+ const validKeys = {
+ endScore: true,
+ action: true,
+ }
+ if (!validator.isNonNullObject(options)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"RecaptchaManagedRule" must be a non-null object.',
+ );
+ }
+ // Check for unsupported top level attributes.
+ for (const key in options) {
+ if (!(key in validKeys)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${key}" is not a valid RecaptchaManagedRule parameter.`,
+ );
+ }
+ }
+
+ // Validate content.
+ if (typeof options.action !== 'undefined' &&
+ options.action !== 'BLOCK') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"RecaptchaManagedRule.action" must be "BLOCK".',
+ );
+ }
+ }
+
+ /**
+ * Returns a JSON-serializable representation of this object.
+ * @returns The JSON-serializable object representation of the ReCaptcha config instance
+ */
+ public toJSON(): object {
+ const json: any = {
+ emailPasswordEnforcementState: this.emailPasswordEnforcementState,
+ managedRules: deepCopy(this.managedRules),
+ recaptchaKeys: deepCopy(this.recaptchaKeys),
+ useAccountDefender: this.useAccountDefender,
+ }
+
+ if (typeof json.emailPasswordEnforcementState === 'undefined') {
+ delete json.emailPasswordEnforcementState;
+ }
+ if (typeof json.managedRules === 'undefined') {
+ delete json.managedRules;
+ }
+ if (typeof json.recaptchaKeys === 'undefined') {
+ delete json.recaptchaKeys;
+ }
+
+ if (typeof json.useAccountDefender === 'undefined') {
+ delete json.useAccountDefender;
+ }
+
+ return json;
+ }
+}
+
+/**
+ * A password policy configuration for a project or tenant
+*/
+export interface PasswordPolicyConfig {
+ /**
+ * Enforcement state of the password policy
+ */
+ enforcementState?: PasswordPolicyEnforcementState;
+ /**
+ * Require users to have a policy-compliant password to sign in
+ */
+ forceUpgradeOnSignin?: boolean;
+ /**
+ * The constraints that make up the password strength policy
+ */
+ constraints?: CustomStrengthOptionsConfig;
+}
+
+/**
+ * A password policy's enforcement state.
+ */
+export type PasswordPolicyEnforcementState = 'ENFORCE' | 'OFF';
+
+/**
+ * Constraints to be enforced on the password policy
+ */
+export interface CustomStrengthOptionsConfig {
+ /**
+ * The password must contain an upper case character
+ */
+ requireUppercase?: boolean;
+ /**
+ * The password must contain a lower case character
+ */
+ requireLowercase?: boolean;
+ /**
+ * The password must contain a non-alphanumeric character
+ */
+ requireNonAlphanumeric?: boolean;
+ /**
+ * The password must contain a number
+ */
+ requireNumeric?: boolean;
+ /**
+ * Minimum password length. Valid values are from 6 to 30
+ */
+ minLength?: number;
+ /**
+ * Maximum password length. No default max length
+ */
+ maxLength?: number;
+}
+
+/**
+ * Defines the password policy config class used to convert client side PasswordPolicyConfig
+ * to a format that is understood by the Auth server.
+ *
+ * @internal
+ */
+export class PasswordPolicyAuthConfig implements PasswordPolicyConfig {
+
+ /**
+ * Identifies a password policy configuration state.
+ */
+ public readonly enforcementState: PasswordPolicyEnforcementState;
+ /**
+ * Users must have a password compliant with the password policy to sign-in
+ */
+ public readonly forceUpgradeOnSignin: boolean;
+ /**
+ * Must be of length 1. Contains the strength attributes for the password policy
+ */
+ public readonly constraints?: CustomStrengthOptionsConfig;
+
+ /**
+ * Static method to convert a client side request to a PasswordPolicyAuthServerConfig.
+ * Throws an error if validation fails.
+ *
+ * @param options - The options object to convert to a server request.
+ * @returns The resulting server request.
+ * @internal
+ */
+ public static buildServerRequest(options: PasswordPolicyConfig): PasswordPolicyAuthServerConfig {
+ const request: PasswordPolicyAuthServerConfig = {};
+ PasswordPolicyAuthConfig.validate(options);
+ if (Object.prototype.hasOwnProperty.call(options, 'enforcementState')) {
+ request.passwordPolicyEnforcementState = options.enforcementState;
+ }
+ request.forceUpgradeOnSignin = false;
+ if (Object.prototype.hasOwnProperty.call(options, 'forceUpgradeOnSignin')) {
+ request.forceUpgradeOnSignin = options.forceUpgradeOnSignin;
+ }
+ const constraintsRequest: CustomStrengthOptionsAuthServerConfig = {
+ containsUppercaseCharacter: false,
+ containsLowercaseCharacter: false,
+ containsNonAlphanumericCharacter: false,
+ containsNumericCharacter: false,
+ minPasswordLength: 6,
+ maxPasswordLength: 4096,
+ };
+ request.passwordPolicyVersions = [];
+ if (Object.prototype.hasOwnProperty.call(options, 'constraints')) {
+ if (options) {
+ if (options.constraints?.requireUppercase !== undefined) {
+ constraintsRequest.containsUppercaseCharacter = options.constraints.requireUppercase;
+ }
+ if (options.constraints?.requireLowercase !== undefined) {
+ constraintsRequest.containsLowercaseCharacter = options.constraints.requireLowercase;
+ }
+ if (options.constraints?.requireNonAlphanumeric !== undefined) {
+ constraintsRequest.containsNonAlphanumericCharacter = options.constraints.requireNonAlphanumeric;
+ }
+ if (options.constraints?.requireNumeric !== undefined) {
+ constraintsRequest.containsNumericCharacter = options.constraints.requireNumeric;
+ }
+ if (options.constraints?.minLength !== undefined) {
+ constraintsRequest.minPasswordLength = options.constraints.minLength;
+ }
+ if (options.constraints?.maxLength !== undefined) {
+ constraintsRequest.maxPasswordLength = options.constraints.maxLength;
+ }
+ }
+ }
+ request.passwordPolicyVersions.push({ customStrengthOptions: constraintsRequest });
+ return request;
+ }
+
+ /**
+ * Validates the PasswordPolicyConfig options object. Throws an error on failure.
+ *
+ * @param options - The options object to validate.
+ * @internal
+ */
+ public static validate(options: PasswordPolicyConfig): void {
+ const validKeys = {
+ enforcementState: true,
+ forceUpgradeOnSignin: true,
+ constraints: true,
+ };
+ if (!validator.isNonNullObject(options)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"PasswordPolicyConfig" must be a non-null object.',
+ );
+ }
+ // Check for unsupported top level attributes.
+ for (const key in options) {
+ if (!(key in validKeys)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${key}" is not a valid PasswordPolicyConfig parameter.`,
+ );
+ }
+ }
+ // Validate content.
+ if (typeof options.enforcementState === 'undefined' ||
+ !(options.enforcementState === 'ENFORCE' ||
+ options.enforcementState === 'OFF')) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"PasswordPolicyConfig.enforcementState" must be either "ENFORCE" or "OFF".',
+ );
+ }
+
+ if (typeof options.forceUpgradeOnSignin !== 'undefined') {
+ if (!validator.isBoolean(options.forceUpgradeOnSignin)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"PasswordPolicyConfig.forceUpgradeOnSignin" must be a boolean.',
+ );
+ }
+ }
+
+ if (typeof options.constraints !== 'undefined') {
+ if (options.enforcementState === 'ENFORCE' && !validator.isNonNullObject(options.constraints)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"PasswordPolicyConfig.constraints" must be a non-empty object.',
+ );
+ }
+
+ const validCharKeys = {
+ requireUppercase: true,
+ requireLowercase: true,
+ requireNumeric: true,
+ requireNonAlphanumeric: true,
+ minLength: true,
+ maxLength: true,
+ };
+
+ // Check for unsupported attributes.
+ for (const key in options.constraints) {
+ if (!(key in validCharKeys)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${key}" is not a valid PasswordPolicyConfig.constraints parameter.`,
+ );
+ }
+ }
+ if (typeof options.constraints.requireUppercase !== 'undefined' &&
+ !validator.isBoolean(options.constraints.requireUppercase)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"PasswordPolicyConfig.constraints.requireUppercase" must be a boolean.',
+ );
+ }
+ if (typeof options.constraints.requireLowercase !== 'undefined' &&
+ !validator.isBoolean(options.constraints.requireLowercase)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"PasswordPolicyConfig.constraints.requireLowercase" must be a boolean.',
+ );
+ }
+ if (typeof options.constraints.requireNonAlphanumeric !== 'undefined' &&
+ !validator.isBoolean(options.constraints.requireNonAlphanumeric)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"PasswordPolicyConfig.constraints.requireNonAlphanumeric"' +
+ ' must be a boolean.',
+ );
+ }
+ if (typeof options.constraints.requireNumeric !== 'undefined' &&
+ !validator.isBoolean(options.constraints.requireNumeric)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"PasswordPolicyConfig.constraints.requireNumeric" must be a boolean.',
+ );
+ }
+ if (typeof options.constraints.minLength === 'undefined') {
+ options.constraints.minLength = 6;
+ } else if (!validator.isNumber(options.constraints.minLength)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"PasswordPolicyConfig.constraints.minLength" must be a number.',
+ );
+ } else {
+ if (!(options.constraints.minLength >= 6
+ && options.constraints.minLength <= 30)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"PasswordPolicyConfig.constraints.minLength"' +
+ ' must be an integer between 6 and 30, inclusive.',
+ );
+ }
+ }
+ if (typeof options.constraints.maxLength === 'undefined') {
+ options.constraints.maxLength = 4096;
+ } else if (!validator.isNumber(options.constraints.maxLength)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"PasswordPolicyConfig.constraints.maxLength" must be a number.',
+ );
+ } else {
+ if (!(options.constraints.maxLength >= options.constraints.minLength &&
+ options.constraints.maxLength <= 4096)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"PasswordPolicyConfig.constraints.maxLength"' +
+ ' must be greater than or equal to minLength and at max 4096.',
+ );
+ }
+ }
+ } else {
+ if (options.enforcementState === 'ENFORCE') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"PasswordPolicyConfig.constraints" must be defined.',
+ );
+ }
+ }
+ }
+
+ /**
+ * The PasswordPolicyAuthConfig constructor.
+ *
+ * @param response - The server side response used to initialize the
+ * PasswordPolicyAuthConfig object.
+ * @constructor
+ * @internal
+ */
+ constructor(response: PasswordPolicyAuthServerConfig) {
+ if (typeof response.passwordPolicyEnforcementState === 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'INTERNAL ASSERT FAILED: Invalid password policy configuration response');
+ }
+ this.enforcementState = response.passwordPolicyEnforcementState;
+ let constraintsResponse: CustomStrengthOptionsConfig = {};
+ if (typeof response.passwordPolicyVersions !== 'undefined') {
+ (response.passwordPolicyVersions || []).forEach((policyVersion) => {
+ constraintsResponse = {
+ requireLowercase: policyVersion.customStrengthOptions?.containsLowercaseCharacter,
+ requireUppercase: policyVersion.customStrengthOptions?.containsUppercaseCharacter,
+ requireNonAlphanumeric: policyVersion.customStrengthOptions?.containsNonAlphanumericCharacter,
+ requireNumeric: policyVersion.customStrengthOptions?.containsNumericCharacter,
+ minLength: policyVersion.customStrengthOptions?.minPasswordLength,
+ maxLength: policyVersion.customStrengthOptions?.maxPasswordLength,
+ };
+ });
+ }
+ this.constraints = constraintsResponse;
+ this.forceUpgradeOnSignin = response.forceUpgradeOnSignin?true:false;
+ }
+}
+
+/**
+ * Server side password policy configuration.
+ */
+export interface PasswordPolicyAuthServerConfig {
+ passwordPolicyEnforcementState?: PasswordPolicyEnforcementState;
+ passwordPolicyVersions?: PasswordPolicyVersionsAuthServerConfig[];
+ forceUpgradeOnSignin?: boolean;
+}
+
+/**
+ * Server side password policy versions configuration.
+ */
+export interface PasswordPolicyVersionsAuthServerConfig {
+ customStrengthOptions?: CustomStrengthOptionsAuthServerConfig;
+}
+
+/**
+ * Server side password policy constraints configuration.
+ */
+export interface CustomStrengthOptionsAuthServerConfig {
+ containsLowercaseCharacter?: boolean;
+ containsUppercaseCharacter?: boolean;
+ containsNumericCharacter?: boolean;
+ containsNonAlphanumericCharacter?: boolean;
+ minPasswordLength?: number;
+ maxPasswordLength?: number;
+}
\ No newline at end of file
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 0dba898..bd54a2a 100644
--- a/packages/dart_firebase_admin/lib/src/auth/auth_exception.dart
+++ b/packages/dart_firebase_admin/lib/src/auth/auth_exception.dart
@@ -1,27 +1,23 @@
-part of '../../dart_firebase_admin.dart';
+part of '../dart_firebase_admin.dart';
class FirebaseAuthAdminException extends FirebaseAdminException {
- FirebaseAuthAdminException._(String code, [String? message])
- : super('auth', code, message);
+ FirebaseAuthAdminException(
+ this.errorCode, [
+ String? message,
+ ]) : super('auth', errorCode.name, errorCode.message ?? message);
factory FirebaseAuthAdminException.fromServerError(
firebase_auth_v1.DetailedApiRequestError error,
) {
final code =
_authServerToClientCode(error.message) ?? AuthClientErrorCode.unknown;
- return FirebaseAuthAdminException._(code.name, code.message);
+ return FirebaseAuthAdminException(code);
}
- factory FirebaseAuthAdminException.fromAuthClientErrorCode(
- AuthClientErrorCode code,
- ) {
- return FirebaseAuthAdminException._(code.name, code.message);
- }
+ final AuthClientErrorCode errorCode;
@override
- String toString() {
- return 'FirebaseAuthAdminException: $code: $message';
- }
+ String toString() => 'FirebaseAuthAdminException: $code: $message';
}
extension AuthClientErrorCodeExtension on AuthClientErrorCode {
@@ -118,7 +114,7 @@ enum AuthClientErrorCode {
userNotFound,
notFound,
userDisabled,
- userNotDisabled,
+ userNotDisabled;
}
String? _authClientCodeMessage(AuthClientErrorCode code) {
@@ -391,7 +387,6 @@ String? _authClientCodeMessage(AuthClientErrorCode code) {
return 'The user must be disabled in order to bulk delete it (or you must pass force=true).';
case AuthClientErrorCode.unknown:
- default:
return null;
}
}
@@ -634,3 +629,30 @@ AuthClientErrorCode? _authServerToClientCode(String? serverCode) {
return null;
}
+
+/// A generic guard wrapper for API calls to handle exceptions.
+R authGuard(R Function() cb) {
+ try {
+ final value = cb();
+
+ if (value is Future) {
+ return value.catchError(_handleException) as R;
+ }
+
+ return value;
+ } catch (error, stackTrace) {
+ _handleException(error, stackTrace);
+ }
+}
+
+/// Converts a Exception to a FirebaseAdminException.
+Never _handleException(Object exception, StackTrace stackTrace) {
+ if (exception is firebase_auth_v1.DetailedApiRequestError) {
+ Error.throwWithStackTrace(
+ FirebaseAuthAdminException.fromServerError(exception),
+ stackTrace,
+ );
+ }
+
+ Error.throwWithStackTrace(exception, stackTrace);
+}
diff --git a/packages/dart_firebase_admin/lib/src/auth/base_auth.dart b/packages/dart_firebase_admin/lib/src/auth/base_auth.dart
new file mode 100644
index 0000000..1c8e91d
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/base_auth.dart
@@ -0,0 +1,442 @@
+import 'package:collection/collection.dart';
+import 'package:firebaseapis/identitytoolkit/v1.dart' as auth1;
+import 'package:meta/meta.dart';
+
+import '../dart_firebase_admin.dart';
+import '../app/core.dart';
+import '../utils/validator.dart';
+import 'auth_api_request.dart';
+import 'auth_config.dart';
+import 'identifier.dart';
+import 'token_verifier.dart';
+import 'user.dart';
+import 'user_import_builder.dart';
+
+abstract class BaseAuth {
+ FirebaseAdminApp get app;
+ @visibleForOverriding
+ AbstractAuthRequestHandler get authRequestHandler;
+ FirebaseTokenVerifier get _sessionCookieVerifier;
+
+ // TODO createCustomToken
+ // TODO verifyIdToken
+ // TODO setCustomUserClaims
+ // TODO revokeRefreshTokens
+ // TODO createSessionCookie
+ // TODO verifySessionCookie
+ // TODO generatePasswordResetLink
+ // TODO generateEmailVerificationLink
+ // TODO generateVerifyAndChangeEmailLink
+ // TODO generateSignInWithEmailLink
+ // TODO listProviderConfigs
+ // TODO getProviderConfig
+ // TODO deleteProviderConfig
+ // TODO updateProviderConfig
+ // TODO createProviderConfig
+
+ Future verifySessionCookie(
+ String sessionCookie, {
+ bool checkRevoked = false,
+ }) async {
+ final isEmulator = app.isUsingEmulator;
+ throw UnimplementedError();
+ }
+
+ /// Imports the provided list of users into Firebase Auth.
+ /// A maximum of 1000 users are allowed to be imported one at a time.
+ /// When importing users with passwords,
+ /// [UserImportOptions] are required to be
+ /// specified.
+ /// This operation is optimized for bulk imports and will ignore checks on `uid`,
+ /// `email` and other identifier uniqueness which could result in duplications.
+ ///
+ /// - users - The list of user records to import to Firebase Auth.
+ /// - options - The user import options, required when the users provided include
+ /// password credentials.
+ ///
+ /// Returns a Future that resolves when
+ /// the operation completes with the result of the import. This includes the
+ /// number of successful imports, the number of failed imports and their
+ /// corresponding errors.
+ Future importUsers(
+ List users, [
+ UserImportOptions? options,
+ ]) async {
+ return authRequestHandler.uploadAccount(users, options);
+ }
+
+ /// Retrieves a list of users (single batch only) with a size of `maxResults`
+ /// starting from the offset as specified by `pageToken`. This is used to
+ /// retrieve all the users of a specified project in batches.
+ ///
+ /// See https://firebase.google.com/docs/auth/admin/manage-users#list_all_users
+ /// for code samples and detailed documentation.
+ ///
+ /// - maxResults - The page size, 1000 if undefined. This is also
+ /// the maximum allowed limit.
+ /// - pageToken - The next page token. If not specified, returns
+ /// users starting without any offset.
+ ///
+ /// Returns a promise that resolves with
+ /// the current batch of downloaded users and the next page token.
+ Future listUsers({
+ int? maxResults,
+ String? pageToken,
+ }) async {
+ final response = await authRequestHandler.downloadAccount(
+ maxResults: maxResults,
+ pageToken: pageToken,
+ );
+
+ final users =
+ response.users?.map(UserRecord.fromResponse).toList() ?? [];
+
+ return ListUsersResult._(
+ users: users,
+ pageToken: response.nextPageToken,
+ );
+ }
+
+ /// Deletes an existing user.
+ ///
+ /// See https://firebase.google.com/docs/auth/admin/manage-users#delete_a_user
+ /// for code samples and detailed documentation.
+ ///
+ /// Returns an empty promise fulfilled once the user has been
+ /// deleted.
+ Future deleteUser(String uid) async {
+ await authRequestHandler.deleteAccount(uid);
+ }
+
+ /// Deletes the users specified by the given uids.
+ ///
+ /// Deleting a non-existing user won't generate an error (i.e. this method
+ /// is idempotent.) Non-existing users are considered to be successfully
+ /// deleted, and are therefore counted in the
+ /// `DeleteUsersResult.successCount` value.
+ ///
+ /// Only a maximum of 1000 identifiers may be supplied. If more than 1000
+ /// identifiers are supplied, this method throws a FirebaseAuthError.
+ ///
+ /// This API is currently rate limited at the server to 1 QPS. If you exceed
+ /// this, you may get a quota exceeded error. Therefore, if you want to
+ /// delete more than 1000 users, you may need to add a delay to ensure you
+ /// don't go over this limit.
+ ///
+ /// Returns a Futrue that resolves to the total number of successful/failed
+ /// deletions, as well as the array of errors that corresponds to the
+ /// failed deletions.
+ Future deleteUsers(List uids) async {
+ uids.forEach(assertIsUid);
+
+ final response = await authRequestHandler.deleteAccounts(uids, force: true);
+ final errors = response.errors ??
+ [];
+
+ return DeleteUsersResult._(
+ successCount: uids.length - errors.length,
+ failureCount: errors.length,
+ errors: errors.map((batchDeleteErrorInfo) {
+ final index = batchDeleteErrorInfo.index;
+ if (index == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'Corrupt BatchDeleteAccountsResponse detected',
+ );
+ }
+
+ FirebaseAuthAdminException errMsgToError(String? msg) {
+ // We unconditionally set force=true, so the 'NOT_DISABLED' error
+ // should not be possible.
+ final code = msg != null && msg.startsWith('NOT_DISABLED')
+ ? AuthClientErrorCode.userNotDisabled
+ : AuthClientErrorCode.internalError;
+
+ return FirebaseAuthAdminException(code, batchDeleteErrorInfo.message);
+ }
+
+ return FirebaseArrayIndexError(
+ index: index,
+ error: errMsgToError(batchDeleteErrorInfo.message),
+ );
+ }).toList(),
+ );
+ }
+
+ /// Gets the user data for the user corresponding to a given `uid`.
+ ///
+ /// See https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data
+ /// for code samples and detailed documentation.
+ ///
+ /// Returns a Future fulfilled with the use data corresponding to the provided `uid`.
+ Future getUser(String uid) async {
+ final response = await authRequestHandler.getAccountInfoByUid(uid);
+ // Returns the user record populated with server response.
+ return UserRecord.fromResponse(response);
+ }
+
+ /// Gets the user data for the user corresponding to a given phone number. The
+ /// phone number has to conform to the E.164 specification.
+ ///
+ /// See https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data
+ /// for code samples and detailed documentation.
+ ///
+ /// Takes the phone number corresponding to the user whose
+ /// data to fetch.
+ ///
+ /// Returns a Future fulfilled with the user
+ /// data corresponding to the provided phone number.
+ Future getUserByPhoneNumber(String phoneNumber) async {
+ final response =
+ await authRequestHandler.getAccountInfoByPhoneNumber(phoneNumber);
+ // Returns the user record populated with server response.
+ return UserRecord.fromResponse(response);
+ }
+
+ /// Gets the user data for the user corresponding to a given email.
+ ///
+ /// See https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data
+ /// for code samples and detailed documentation.
+ ///
+ /// Receives the email corresponding to the user whose data to fetch.
+ ///
+ /// Returns a promise fulfilled with the user
+ /// data corresponding to the provided email.
+ Future getUserByEmail(String email) async {
+ final response = await authRequestHandler.getAccountInfoByEmail(email);
+ // Returns the user record populated with server response.
+ return UserRecord.fromResponse(response);
+ }
+
+ /// Gets the user data for the user corresponding to a given provider id.
+ ///
+ /// See https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data
+ /// for code samples and detailed documentation.
+ ///
+ /// - `providerId`: The provider ID, for example, "google.com" for the
+ /// Google provider.
+ /// - `uid`: The user identifier for the given provider.
+ ///
+ /// Returns a Future fulfilled with the user data corresponding to the
+ /// given provider id.
+ Future getUserByProviderUid({
+ required String providerId,
+ required String uid,
+ }) async {
+ // Although we don't really advertise it, we want to also handle
+ // non-federated idps with this call. So if we detect one of them, we'll
+ // reroute this request appropriately.
+ if (providerId == 'phone') {
+ return getUserByPhoneNumber(uid);
+ } else if (providerId == 'email') {
+ return getUserByEmail(uid);
+ }
+
+ final response = await authRequestHandler.getAccountInfoByFederatedUid(
+ providerId: providerId,
+ rawId: uid,
+ );
+
+ // Returns the user record populated with server response.
+ return UserRecord.fromResponse(response);
+ }
+
+ /// Gets the user data corresponding to the specified identifiers.
+ ///
+ /// There are no ordering guarantees; in particular, the nth entry in the result list is not
+ /// guaranteed to correspond to the nth entry in the input parameters list.
+ ///
+ /// Only a maximum of 100 identifiers may be supplied. If more than 100 identifiers are supplied,
+ /// this method throws a FirebaseAuthError.
+ ///
+ /// Takes a list of [UserIdentifier] used to indicate which user records should be returned.
+ /// Must not have more than 100 entries.
+ ///
+ /// Returns a Future that resolves to the corresponding user records.
+ /// Throws [FirebaseAdminException] if any of the identifiers are invalid or if more than 100
+ /// identifiers are specified.
+ Future getUsers(List identifiers) async {
+ final response =
+ await authRequestHandler.getAccountInfoByIdentifiers(identifiers);
+
+ final userRecords = response.users?.map(UserRecord.fromResponse).toList() ??
+ const [];
+
+ // Checks if the specified identifier is within the list of UserRecords.
+ bool isUserFound(UserIdentifier id) {
+ return userRecords.any((userRecord) {
+ switch (id) {
+ case UidIdentifier():
+ return id.uid == userRecord.uid;
+ case EmailIdentifier():
+ return id.email == userRecord.email;
+ case PhoneIdentifier():
+ return id.phoneNumber == userRecord.phoneNumber;
+ case ProviderIdentifier():
+ final matchingUserInfo = userRecord.providerData
+ .firstWhereOrNull((userInfo) => userInfo.phoneNumber != null);
+ return matchingUserInfo != null &&
+ id.providerUid == matchingUserInfo.uid;
+ }
+ });
+ }
+
+ final notFound = identifiers.where((id) => !isUserFound(id)).toList();
+
+ return GetUsersResult._(users: userRecords, notFound: notFound);
+ }
+
+ /// Creates a new user.
+ ///
+ /// See https://firebase.google.com/docs/auth/admin/manage-users#create_a_user
+ /// for code samples and detailed documentation.
+ ///
+ /// Returns A Future fulfilled with the user
+ /// data corresponding to the newly created user.
+ Future createUser(CreateRequest properties) async {
+ return authRequestHandler
+ .createNewAccount(properties)
+ // Return the corresponding user record.
+ .then(getUser)
+ .onError((error, _) {
+ if (error.errorCode == AuthClientErrorCode.userNotFound) {
+ // Something must have happened after creating the user and then retrieving it.
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'Unable to create the user record provided.',
+ );
+ }
+ throw error;
+ });
+ }
+
+ /// Updates an existing user.
+ ///
+ /// See https://firebase.google.com/docs/auth/admin/manage-users#update_a_user
+ /// for code samples and detailed documentation.
+ ///
+ /// - uid - The `uid` corresponding to the user to update.
+ /// - properties - The properties to update on the provided user.
+ ///
+ /// Returns a [Future] fulfilled with the updated user data.
+ Future updateuser(String uid, UpdateRequest properties) async {
+ // Although we don't really advertise it, we want to also handle linking of
+ // non-federated idps with this call. So if we detect one of them, we'll
+ // adjust the properties parameter appropriately. This *does* imply that a
+ // conflict could arise, e.g. if the user provides a phoneNumber property,
+ // but also provides a providerToLink with a 'phone' provider id. In that
+ // case, we'll throw an error.
+ var request = properties;
+ final providerToLink = properties.providerToLink;
+ switch (providerToLink) {
+ case UserProvider(providerId: 'email'):
+ if (properties.email != null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ "Both UpdateRequest.email and UpdateRequest.providerToLink.providerId='email' were set. To "
+ 'link to the email/password provider, only specify the UpdateRequest.email field.',
+ );
+ }
+ request = properties.copyWith(email: providerToLink.uid);
+ case UserProvider(providerId: 'phone'):
+ if (properties.phoneNumber != null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ "Both UpdateRequest.phoneNumber and UpdateRequest.providerToLink.providerId='phone' were set. To "
+ 'link to a phone provider, only specify the UpdateRequest.phoneNumber field.',
+ );
+ }
+ request = properties.copyWith(phoneNumber: providerToLink.uid);
+ }
+ final providersToUnlink = properties.providersToUnlink;
+ if (providersToUnlink != null && providersToUnlink.contains('phone')) {
+ // If we've been told to unlink the phone provider both via setting
+ // phoneNumber to null *and* by setting providersToUnlink to include
+ // 'phone', then we'll reject that. Though it might also be reasonable
+ // to relax this restriction and just unlink it.
+ if (properties.phoneNumber == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ "Both UpdateRequest.phoneNumber and UpdateRequest.providersToUnlink=['phone'] were set. To "
+ 'unlink the phone provider, only specify the UpdateRequest.providersToUnlink field.',
+ );
+ }
+ }
+
+ final existingUid =
+ await authRequestHandler.updateExistingAccount(uid, request);
+ return getUser(existingUid);
+ }
+}
+
+/// Interface representing the object returned from a
+/// [BaseAuth.listUsers] operation. Contains the list
+/// of users for the current batch and the next page token if available.
+class ListUsersResult {
+ ListUsersResult._({required this.users, required this.pageToken});
+
+ /// The list of {@link UserRecord} objects for the
+ /// current downloaded batch.
+ final List users;
+
+ /// The next page token if available. This is needed for the next batch download.
+ final String? pageToken;
+}
+
+/// Represents the result of the {@link BaseAuth.getUsers} API.
+class GetUsersResult {
+ GetUsersResult._({required this.users, required this.notFound});
+
+ /// Set of user records, corresponding to the set of users that were
+ /// requested. Only users that were found are listed here. The result set is
+ /// unordered.
+ final List users;
+
+ /// Set of identifiers that were requested, but not found.
+ final List notFound;
+}
+
+/// Represents the result of the {@link BaseAuth.deleteUsers}.
+/// API.
+class DeleteUsersResult {
+ DeleteUsersResult._({
+ required this.failureCount,
+ required this.successCount,
+ required this.errors,
+ });
+
+ /// The number of user records that failed to be deleted (possibly zero).
+ final int failureCount;
+
+ /// The number of users that were deleted successfully (possibly zero).
+ /// Users that did not exist prior to calling `deleteUsers()` are
+ /// considered to be successfully deleted.
+ final int successCount;
+
+ /// A list of `FirebaseArrayIndexError` instances describing the errors that
+ /// were encountered during the deletion. Length of this list is equal to
+ /// the return value of {@link DeleteUsersResult.failureCount}.
+ final List errors;
+}
+
+/// Interface representing the response from the
+/// [BaseAuth.importUsers] method for batch
+/// importing users to Firebase Auth.
+class UserImportResult {
+ @internal
+ UserImportResult({
+ required this.failureCount,
+ required this.successCount,
+ required this.errors,
+ });
+
+ /// The number of user records that failed to import to Firebase Auth.
+ final int failureCount;
+
+ /// The number of user records that successfully imported to Firebase Auth.
+ final int successCount;
+
+ /// An array of errors corresponding to the provided users to import. The
+ /// length of this array is equal to [failureCount].
+ final List errors;
+}
diff --git a/packages/dart_firebase_admin/lib/src/auth/base_auth.ts b/packages/dart_firebase_admin/lib/src/auth/base_auth.ts
new file mode 100644
index 0000000..229cbc9
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/base_auth.ts
@@ -0,0 +1,1144 @@
+/*!
+ * Copyright 2021 Google Inc.
+ *
+ * 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.
+ */
+
+import { App, FirebaseArrayIndexError } from '../app';
+import { AuthClientErrorCode, ErrorInfo, FirebaseAuthError } from '../utils/error';
+import { deepCopy } from '../utils/deep-copy';
+import * as validator from '../utils/validator';
+
+import { AbstractAuthRequestHandler, useEmulator } from './auth-api-request';
+import { FirebaseTokenGenerator, EmulatedSigner, handleCryptoSignerError } from './token-generator';
+import {
+ FirebaseTokenVerifier,
+ createSessionCookieVerifier,
+ createIdTokenVerifier,
+ createAuthBlockingTokenVerifier,
+ DecodedIdToken,
+ DecodedAuthBlockingToken,
+} from './token-verifier';
+import {
+ AuthProviderConfig, SAMLAuthProviderConfig, AuthProviderConfigFilter, ListProviderConfigResults,
+ SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse,
+ UpdateAuthProviderRequest, OIDCAuthProviderConfig, CreateRequest, UpdateRequest,
+} from './auth-config';
+import { UserRecord } from './user-record';
+import {
+ UserIdentifier, isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier,
+} from './identifier';
+import { UserImportOptions, UserImportRecord, UserImportResult } from './user-import-builder';
+import { ActionCodeSettings } from './action-code-settings-builder';
+import { cryptoSignerFromApp } from '../utils/crypto-signer';
+
+/** Interface representing a phone number. */
+
+/** Represents the result of the {@link BaseAuth.getUsers} API. */
+export interface GetUsersResult {
+ /**
+ * Set of user records, corresponding to the set of users that were
+ * requested. Only users that were found are listed here. The result set is
+ * unordered.
+ */
+ users: UserRecord[];
+
+ /** Set of identifiers that were requested, but not found. */
+ notFound: UserIdentifier[];
+}
+
+/**
+ * Interface representing the object returned from a
+ * {@link BaseAuth.listUsers} operation. Contains the list
+ * of users for the current batch and the next page token if available.
+ */
+export interface ListUsersResult {
+
+ /**
+ * The list of {@link UserRecord} objects for the
+ * current downloaded batch.
+ */
+ users: UserRecord[];
+
+ /**
+ * The next page token if available. This is needed for the next batch download.
+ */
+ pageToken?: string;
+}
+
+/**
+ * Represents the result of the {@link BaseAuth.deleteUsers}.
+ * API.
+ */
+export interface DeleteUsersResult {
+ /**
+ * The number of user records that failed to be deleted (possibly zero).
+ */
+ failureCount: number;
+
+ /**
+ * The number of users that were deleted successfully (possibly zero).
+ * Users that did not exist prior to calling `deleteUsers()` are
+ * considered to be successfully deleted.
+ */
+ successCount: number;
+
+ /**
+ * A list of `FirebaseArrayIndexError` instances describing the errors that
+ * were encountered during the deletion. Length of this list is equal to
+ * the return value of {@link DeleteUsersResult.failureCount}.
+ */
+ errors: FirebaseArrayIndexError[];
+}
+
+/**
+ * Interface representing the session cookie options needed for the
+ * {@link BaseAuth.createSessionCookie} method.
+ */
+export interface SessionCookieOptions {
+
+ /**
+ * The session cookie custom expiration in milliseconds. The minimum allowed is
+ * 5 minutes and the maxium allowed is 2 weeks.
+ */
+ expiresIn: number;
+}
+
+/**
+ * @internal
+ */
+export function createFirebaseTokenGenerator(app: App,
+ tenantId?: string): FirebaseTokenGenerator {
+ try {
+ const signer = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app);
+ return new FirebaseTokenGenerator(signer, tenantId);
+ } catch (err) {
+ throw handleCryptoSignerError(err);
+ }
+}
+
+/**
+ * Common parent interface for both `Auth` and `TenantAwareAuth` APIs.
+ */
+export abstract class BaseAuth {
+
+ /** @internal */
+ protected readonly tokenGenerator: FirebaseTokenGenerator;
+ /** @internal */
+ protected readonly idTokenVerifier: FirebaseTokenVerifier;
+ /** @internal */
+ protected readonly authBlockingTokenVerifier: FirebaseTokenVerifier;
+ /** @internal */
+ protected readonly sessionCookieVerifier: FirebaseTokenVerifier;
+
+ /**
+ * The BaseAuth class constructor.
+ *
+ * @param app - The FirebaseApp to associate with this Auth instance.
+ * @param authRequestHandler - The RPC request handler for this instance.
+ * @param tokenGenerator - Optional token generator. If not specified, a
+ * (non-tenant-aware) instance will be created. Use this paramter to
+ * specify a tenant-aware tokenGenerator.
+ * @constructor
+ * @internal
+ */
+ constructor(
+ app: App,
+ /** @internal */ protected readonly authRequestHandler: AbstractAuthRequestHandler,
+ tokenGenerator?: FirebaseTokenGenerator) {
+ if (tokenGenerator) {
+ this.tokenGenerator = tokenGenerator;
+ } else {
+ this.tokenGenerator = createFirebaseTokenGenerator(app);
+ }
+
+ this.sessionCookieVerifier = createSessionCookieVerifier(app);
+ this.idTokenVerifier = createIdTokenVerifier(app);
+ this.authBlockingTokenVerifier = createAuthBlockingTokenVerifier(app);
+ }
+
+ /**
+ * Creates a new Firebase custom token (JWT) that can be sent back to a client
+ * device to use to sign in with the client SDKs' `signInWithCustomToken()`
+ * methods. (Tenant-aware instances will also embed the tenant ID in the
+ * token.)
+ *
+ * See {@link https://firebase.google.com/docs/auth/admin/create-custom-tokens | Create Custom Tokens}
+ * for code samples and detailed documentation.
+ *
+ * @param uid - The `uid` to use as the custom token's subject.
+ * @param developerClaims - Optional additional claims to include
+ * in the custom token's payload.
+ *
+ * @returns A promise fulfilled with a custom token for the
+ * provided `uid` and payload.
+ */
+ public createCustomToken(uid: string, developerClaims?: object): Promise {
+ return this.tokenGenerator.createCustomToken(uid, developerClaims);
+ }
+
+ /**
+ * Verifies a Firebase ID token (JWT). If the token is valid, the promise is
+ * fulfilled with the token's decoded claims; otherwise, the promise is
+ * rejected.
+ *
+ * If `checkRevoked` is set to true, first verifies whether the corresponding
+ * user is disabled. If yes, an `auth/user-disabled` error is thrown. If no,
+ * verifies if the session corresponding to the ID token was revoked. If the
+ * corresponding user's session was invalidated, an `auth/id-token-revoked`
+ * error is thrown. If not specified the check is not applied.
+ *
+ * See {@link https://firebase.google.com/docs/auth/admin/verify-id-tokens | Verify ID Tokens}
+ * for code samples and detailed documentation.
+ *
+ * @param idToken - The ID token to verify.
+ * @param checkRevoked - Whether to check if the ID token was revoked.
+ * This requires an extra request to the Firebase Auth backend to check
+ * the `tokensValidAfterTime` time for the corresponding user.
+ * When not specified, this additional check is not applied.
+ *
+ * @returns A promise fulfilled with the
+ * token's decoded claims if the ID token is valid; otherwise, a rejected
+ * promise.
+ */
+ public verifyIdToken(idToken: string, checkRevoked = false): Promise {
+ const isEmulator = useEmulator();
+ return this.idTokenVerifier.verifyJWT(idToken, isEmulator)
+ .then((decodedIdToken: DecodedIdToken) => {
+ // Whether to check if the token was revoked.
+ if (checkRevoked || isEmulator) {
+ return this.verifyDecodedJWTNotRevokedOrDisabled(
+ decodedIdToken,
+ AuthClientErrorCode.ID_TOKEN_REVOKED);
+ }
+ return decodedIdToken;
+ });
+ }
+
+ /**
+ * Gets the user data for the user corresponding to a given `uid`.
+ *
+ * See {@link https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data | Retrieve user data}
+ * for code samples and detailed documentation.
+ *
+ * @param uid - The `uid` corresponding to the user whose data to fetch.
+ *
+ * @returns A promise fulfilled with the user
+ * data corresponding to the provided `uid`.
+ */
+ public getUser(uid: string): Promise {
+ return this.authRequestHandler.getAccountInfoByUid(uid)
+ .then((response: any) => {
+ // Returns the user record populated with server response.
+ return new UserRecord(response.users[0]);
+ });
+ }
+
+ /**
+ * Gets the user data for the user corresponding to a given email.
+ *
+ * See {@link https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data | Retrieve user data}
+ * for code samples and detailed documentation.
+ *
+ * @param email - The email corresponding to the user whose data to
+ * fetch.
+ *
+ * @returns A promise fulfilled with the user
+ * data corresponding to the provided email.
+ */
+ public getUserByEmail(email: string): Promise {
+ return this.authRequestHandler.getAccountInfoByEmail(email)
+ .then((response: any) => {
+ // Returns the user record populated with server response.
+ return new UserRecord(response.users[0]);
+ });
+ }
+
+ /**
+ * Gets the user data for the user corresponding to a given phone number. The
+ * phone number has to conform to the E.164 specification.
+ *
+ * See {@link https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data | Retrieve user data}
+ * for code samples and detailed documentation.
+ *
+ * @param phoneNumber - The phone number corresponding to the user whose
+ * data to fetch.
+ *
+ * @returns A promise fulfilled with the user
+ * data corresponding to the provided phone number.
+ */
+ public getUserByPhoneNumber(phoneNumber: string): Promise {
+ return this.authRequestHandler.getAccountInfoByPhoneNumber(phoneNumber)
+ .then((response: any) => {
+ // Returns the user record populated with server response.
+ return new UserRecord(response.users[0]);
+ });
+ }
+
+ /**
+ * Gets the user data for the user corresponding to a given provider id.
+ *
+ * See {@link https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data | Retrieve user data}
+ * for code samples and detailed documentation.
+ *
+ * @param providerId - The provider ID, for example, "google.com" for the
+ * Google provider.
+ * @param uid - The user identifier for the given provider.
+ *
+ * @returns A promise fulfilled with the user data corresponding to the
+ * given provider id.
+ */
+ public getUserByProviderUid(providerId: string, uid: string): Promise {
+ // Although we don't really advertise it, we want to also handle
+ // non-federated idps with this call. So if we detect one of them, we'll
+ // reroute this request appropriately.
+ if (providerId === 'phone') {
+ return this.getUserByPhoneNumber(uid);
+ } else if (providerId === 'email') {
+ return this.getUserByEmail(uid);
+ }
+
+ return this.authRequestHandler.getAccountInfoByFederatedUid(providerId, uid)
+ .then((response: any) => {
+ // Returns the user record populated with server response.
+ return new UserRecord(response.users[0]);
+ });
+ }
+
+ /**
+ * Gets the user data corresponding to the specified identifiers.
+ *
+ * There are no ordering guarantees; in particular, the nth entry in the result list is not
+ * guaranteed to correspond to the nth entry in the input parameters list.
+ *
+ * Only a maximum of 100 identifiers may be supplied. If more than 100 identifiers are supplied,
+ * this method throws a FirebaseAuthError.
+ *
+ * @param identifiers - The identifiers used to indicate which user records should be returned.
+ * Must not have more than 100 entries.
+ * @returns A promise that resolves to the corresponding user records.
+ * @throws FirebaseAuthError If any of the identifiers are invalid or if more than 100
+ * identifiers are specified.
+ */
+ public getUsers(identifiers: UserIdentifier[]): Promise {
+ if (!validator.isArray(identifiers)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT, '`identifiers` parameter must be an array');
+ }
+ return this.authRequestHandler
+ .getAccountInfoByIdentifiers(identifiers)
+ .then((response: any) => {
+ /**
+ * Checks if the specified identifier is within the list of
+ * UserRecords.
+ */
+ const isUserFound = ((id: UserIdentifier, userRecords: UserRecord[]): boolean => {
+ return !!userRecords.find((userRecord) => {
+ if (isUidIdentifier(id)) {
+ return id.uid === userRecord.uid;
+ } else if (isEmailIdentifier(id)) {
+ return id.email === userRecord.email;
+ } else if (isPhoneIdentifier(id)) {
+ return id.phoneNumber === userRecord.phoneNumber;
+ } else if (isProviderIdentifier(id)) {
+ const matchingUserInfo = userRecord.providerData.find((userInfo) => {
+ return id.providerId === userInfo.providerId;
+ });
+ return !!matchingUserInfo && id.providerUid === matchingUserInfo.uid;
+ } else {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'Unhandled identifier type');
+ }
+ });
+ });
+
+ const users = response.users ? response.users.map((user: any) => new UserRecord(user)) : [];
+ const notFound = identifiers.filter((id) => !isUserFound(id, users));
+
+ return { users, notFound };
+ });
+ }
+
+ /**
+ * Retrieves a list of users (single batch only) with a size of `maxResults`
+ * starting from the offset as specified by `pageToken`. This is used to
+ * retrieve all the users of a specified project in batches.
+ *
+ * See {@link https://firebase.google.com/docs/auth/admin/manage-users#list_all_users | List all users}
+ * for code samples and detailed documentation.
+ *
+ * @param maxResults - The page size, 1000 if undefined. This is also
+ * the maximum allowed limit.
+ * @param pageToken - The next page token. If not specified, returns
+ * users starting without any offset.
+ * @returns A promise that resolves with
+ * the current batch of downloaded users and the next page token.
+ */
+ public listUsers(maxResults?: number, pageToken?: string): Promise {
+ return this.authRequestHandler.downloadAccount(maxResults, pageToken)
+ .then((response: any) => {
+ // List of users to return.
+ const users: UserRecord[] = [];
+ // Convert each user response to a UserRecord.
+ response.users.forEach((userResponse: any) => {
+ users.push(new UserRecord(userResponse));
+ });
+ // Return list of user records and the next page token if available.
+ const result = {
+ users,
+ pageToken: response.nextPageToken,
+ };
+ // Delete result.pageToken if undefined.
+ if (typeof result.pageToken === 'undefined') {
+ delete result.pageToken;
+ }
+ return result;
+ });
+ }
+
+ /**
+ * Creates a new user.
+ *
+ * See {@link https://firebase.google.com/docs/auth/admin/manage-users#create_a_user | Create a user}
+ * for code samples and detailed documentation.
+ *
+ * @param properties - The properties to set on the
+ * new user record to be created.
+ *
+ * @returns A promise fulfilled with the user
+ * data corresponding to the newly created user.
+ */
+ public createUser(properties: CreateRequest): Promise {
+ return this.authRequestHandler.createNewAccount(properties)
+ .then((uid) => {
+ // Return the corresponding user record.
+ return this.getUser(uid);
+ })
+ .catch((error) => {
+ if (error.code === 'auth/user-not-found') {
+ // Something must have happened after creating the user and then retrieving it.
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'Unable to create the user record provided.');
+ }
+ throw error;
+ });
+ }
+
+ /**
+ * Deletes an existing user.
+ *
+ * See {@link https://firebase.google.com/docs/auth/admin/manage-users#delete_a_user | Delete a user}
+ * for code samples and detailed documentation.
+ *
+ * @param uid - The `uid` corresponding to the user to delete.
+ *
+ * @returns An empty promise fulfilled once the user has been
+ * deleted.
+ */
+ public deleteUser(uid: string): Promise {
+ return this.authRequestHandler.deleteAccount(uid)
+ .then(() => {
+ // Return nothing on success.
+ });
+ }
+
+ /**
+ * Deletes the users specified by the given uids.
+ *
+ * Deleting a non-existing user won't generate an error (i.e. this method
+ * is idempotent.) Non-existing users are considered to be successfully
+ * deleted, and are therefore counted in the
+ * `DeleteUsersResult.successCount` value.
+ *
+ * Only a maximum of 1000 identifiers may be supplied. If more than 1000
+ * identifiers are supplied, this method throws a FirebaseAuthError.
+ *
+ * This API is currently rate limited at the server to 1 QPS. If you exceed
+ * this, you may get a quota exceeded error. Therefore, if you want to
+ * delete more than 1000 users, you may need to add a delay to ensure you
+ * don't go over this limit.
+ *
+ * @param uids - The `uids` corresponding to the users to delete.
+ *
+ * @returns A Promise that resolves to the total number of successful/failed
+ * deletions, as well as the array of errors that corresponds to the
+ * failed deletions.
+ */
+ public deleteUsers(uids: string[]): Promise {
+ if (!validator.isArray(uids)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT, '`uids` parameter must be an array');
+ }
+ return this.authRequestHandler.deleteAccounts(uids, /*force=*/true)
+ .then((batchDeleteAccountsResponse) => {
+ const result: DeleteUsersResult = {
+ failureCount: 0,
+ successCount: uids.length,
+ errors: [],
+ };
+
+ if (!validator.isNonEmptyArray(batchDeleteAccountsResponse.errors)) {
+ return result;
+ }
+
+ result.failureCount = batchDeleteAccountsResponse.errors.length;
+ result.successCount = uids.length - batchDeleteAccountsResponse.errors.length;
+ result.errors = batchDeleteAccountsResponse.errors.map((batchDeleteErrorInfo) => {
+ if (batchDeleteErrorInfo.index === undefined) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INTERNAL_ERROR,
+ 'Corrupt BatchDeleteAccountsResponse detected');
+ }
+
+ const errMsgToError = (msg?: string): FirebaseAuthError => {
+ // We unconditionally set force=true, so the 'NOT_DISABLED' error
+ // should not be possible.
+ const code = msg && msg.startsWith('NOT_DISABLED') ?
+ AuthClientErrorCode.USER_NOT_DISABLED : AuthClientErrorCode.INTERNAL_ERROR;
+ return new FirebaseAuthError(code, batchDeleteErrorInfo.message);
+ };
+
+ return {
+ index: batchDeleteErrorInfo.index,
+ error: errMsgToError(batchDeleteErrorInfo.message),
+ };
+ });
+
+ return result;
+ });
+ }
+
+ /**
+ * Updates an existing user.
+ *
+ * See {@link https://firebase.google.com/docs/auth/admin/manage-users#update_a_user | Update a user}
+ * for code samples and detailed documentation.
+ *
+ * @param uid - The `uid` corresponding to the user to update.
+ * @param properties - The properties to update on
+ * the provided user.
+ *
+ * @returns A promise fulfilled with the
+ * updated user data.
+ */
+ public updateUser(uid: string, properties: UpdateRequest): Promise {
+ // Although we don't really advertise it, we want to also handle linking of
+ // non-federated idps with this call. So if we detect one of them, we'll
+ // adjust the properties parameter appropriately. This *does* imply that a
+ // conflict could arise, e.g. if the user provides a phoneNumber property,
+ // but also provides a providerToLink with a 'phone' provider id. In that
+ // case, we'll throw an error.
+ properties = deepCopy(properties);
+
+ if (properties?.providerToLink) {
+ if (properties.providerToLink.providerId === 'email') {
+ if (typeof properties.email !== 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ "Both UpdateRequest.email and UpdateRequest.providerToLink.providerId='email' were set. To "
+ + 'link to the email/password provider, only specify the UpdateRequest.email field.');
+ }
+ properties.email = properties.providerToLink.uid;
+ delete properties.providerToLink;
+ } else if (properties.providerToLink.providerId === 'phone') {
+ if (typeof properties.phoneNumber !== 'undefined') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ "Both UpdateRequest.phoneNumber and UpdateRequest.providerToLink.providerId='phone' were set. To "
+ + 'link to a phone provider, only specify the UpdateRequest.phoneNumber field.');
+ }
+ properties.phoneNumber = properties.providerToLink.uid;
+ delete properties.providerToLink;
+ }
+ }
+ if (properties?.providersToUnlink) {
+ if (properties.providersToUnlink.indexOf('phone') !== -1) {
+ // If we've been told to unlink the phone provider both via setting
+ // phoneNumber to null *and* by setting providersToUnlink to include
+ // 'phone', then we'll reject that. Though it might also be reasonable
+ // to relax this restriction and just unlink it.
+ if (properties.phoneNumber === null) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ "Both UpdateRequest.phoneNumber=null and UpdateRequest.providersToUnlink=['phone'] were set. To "
+ + 'unlink from a phone provider, only specify the UpdateRequest.phoneNumber=null field.');
+ }
+ }
+ }
+
+ return this.authRequestHandler.updateExistingAccount(uid, properties)
+ .then((existingUid) => {
+ // Return the corresponding user record.
+ return this.getUser(existingUid);
+ });
+ }
+
+ /**
+ * Sets additional developer claims on an existing user identified by the
+ * provided `uid`, typically used to define user roles and levels of
+ * access. These claims should propagate to all devices where the user is
+ * already signed in (after token expiration or when token refresh is forced)
+ * and the next time the user signs in. If a reserved OIDC claim name
+ * is used (sub, iat, iss, etc), an error is thrown. They are set on the
+ * authenticated user's ID token JWT.
+ *
+ * See {@link https://firebase.google.com/docs/auth/admin/custom-claims |
+ * Defining user roles and access levels}
+ * for code samples and detailed documentation.
+ *
+ * @param uid - The `uid` of the user to edit.
+ * @param customUserClaims - The developer claims to set. If null is
+ * passed, existing custom claims are deleted. Passing a custom claims payload
+ * larger than 1000 bytes will throw an error. Custom claims are added to the
+ * user's ID token which is transmitted on every authenticated request.
+ * For profile non-access related user attributes, use database or other
+ * separate storage systems.
+ * @returns A promise that resolves when the operation completes
+ * successfully.
+ */
+ public setCustomUserClaims(uid: string, customUserClaims: object | null): Promise {
+ return this.authRequestHandler.setCustomUserClaims(uid, customUserClaims)
+ .then(() => {
+ // Return nothing on success.
+ });
+ }
+
+ /**
+ * Revokes all refresh tokens for an existing user.
+ *
+ * This API will update the user's {@link UserRecord.tokensValidAfterTime} to
+ * the current UTC. It is important that the server on which this is called has
+ * its clock set correctly and synchronized.
+ *
+ * While this will revoke all sessions for a specified user and disable any
+ * new ID tokens for existing sessions from getting minted, existing ID tokens
+ * may remain active until their natural expiration (one hour). To verify that
+ * ID tokens are revoked, use {@link BaseAuth.verifyIdToken}
+ * where `checkRevoked` is set to true.
+ *
+ * @param uid - The `uid` corresponding to the user whose refresh tokens
+ * are to be revoked.
+ *
+ * @returns An empty promise fulfilled once the user's refresh
+ * tokens have been revoked.
+ */
+ public revokeRefreshTokens(uid: string): Promise {
+ return this.authRequestHandler.revokeRefreshTokens(uid)
+ .then(() => {
+ // Return nothing on success.
+ });
+ }
+
+ /**
+ * Imports the provided list of users into Firebase Auth.
+ * A maximum of 1000 users are allowed to be imported one at a time.
+ * When importing users with passwords,
+ * {@link UserImportOptions} are required to be
+ * specified.
+ * This operation is optimized for bulk imports and will ignore checks on `uid`,
+ * `email` and other identifier uniqueness which could result in duplications.
+ *
+ * @param users - The list of user records to import to Firebase Auth.
+ * @param options - The user import options, required when the users provided include
+ * password credentials.
+ * @returns A promise that resolves when
+ * the operation completes with the result of the import. This includes the
+ * number of successful imports, the number of failed imports and their
+ * corresponding errors.
+ */
+ public importUsers(
+ users: UserImportRecord[], options?: UserImportOptions): Promise {
+ return this.authRequestHandler.uploadAccount(users, options);
+ }
+
+ /**
+ * Creates a new Firebase session cookie with the specified options. The created
+ * JWT string can be set as a server-side session cookie with a custom cookie
+ * policy, and be used for session management. The session cookie JWT will have
+ * the same payload claims as the provided ID token.
+ *
+ * See {@link https://firebase.google.com/docs/auth/admin/manage-cookies | Manage Session Cookies}
+ * for code samples and detailed documentation.
+ *
+ * @param idToken - The Firebase ID token to exchange for a session
+ * cookie.
+ * @param sessionCookieOptions - The session
+ * cookie options which includes custom session duration.
+ *
+ * @returns A promise that resolves on success with the
+ * created session cookie.
+ */
+ public createSessionCookie(
+ idToken: string, sessionCookieOptions: SessionCookieOptions): Promise {
+ // Return rejected promise if expiresIn is not available.
+ if (!validator.isNonNullObject(sessionCookieOptions) ||
+ !validator.isNumber(sessionCookieOptions.expiresIn)) {
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION));
+ }
+ return this.authRequestHandler.createSessionCookie(
+ idToken, sessionCookieOptions.expiresIn);
+ }
+
+ /**
+ * Verifies a Firebase session cookie. Returns a Promise with the cookie claims.
+ * Rejects the promise if the cookie could not be verified.
+ *
+ * If `checkRevoked` is set to true, first verifies whether the corresponding
+ * user is disabled: If yes, an `auth/user-disabled` error is thrown. If no,
+ * verifies if the session corresponding to the session cookie was revoked.
+ * If the corresponding user's session was invalidated, an
+ * `auth/session-cookie-revoked` error is thrown. If not specified the check
+ * is not performed.
+ *
+ * See {@link https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookie_and_check_permissions |
+ * Verify Session Cookies}
+ * for code samples and detailed documentation
+ *
+ * @param sessionCookie - The session cookie to verify.
+ * @param checkForRevocation - Whether to check if the session cookie was
+ * revoked. This requires an extra request to the Firebase Auth backend to
+ * check the `tokensValidAfterTime` time for the corresponding user.
+ * When not specified, this additional check is not performed.
+ *
+ * @returns A promise fulfilled with the
+ * session cookie's decoded claims if the session cookie is valid; otherwise,
+ * a rejected promise.
+ */
+ public verifySessionCookie(
+ sessionCookie: string, checkRevoked = false): Promise {
+ const isEmulator = useEmulator();
+ return this.sessionCookieVerifier.verifyJWT(sessionCookie, isEmulator)
+ .then((decodedIdToken: DecodedIdToken) => {
+ // Whether to check if the token was revoked.
+ if (checkRevoked || isEmulator) {
+ return this.verifyDecodedJWTNotRevokedOrDisabled(
+ decodedIdToken,
+ AuthClientErrorCode.SESSION_COOKIE_REVOKED);
+ }
+ return decodedIdToken;
+ });
+ }
+
+ /**
+ * Generates the out of band email action link to reset a user's password.
+ * The link is generated for the user with the specified email address. The
+ * optional {@link ActionCodeSettings} object
+ * defines whether the link is to be handled by a mobile app or browser and the
+ * additional state information to be passed in the deep link, etc.
+ *
+ * @example
+ * ```javascript
+ * var actionCodeSettings = {
+ * url: 'https://www.example.com/?email=user@example.com',
+ * iOS: {
+ * bundleId: 'com.example.ios'
+ * },
+ * android: {
+ * packageName: 'com.example.android',
+ * installApp: true,
+ * minimumVersion: '12'
+ * },
+ * handleCodeInApp: true,
+ * dynamicLinkDomain: 'custom.page.link'
+ * };
+ * admin.auth()
+ * .generatePasswordResetLink('user@example.com', actionCodeSettings)
+ * .then(function(link) {
+ * // The link was successfully generated.
+ * })
+ * .catch(function(error) {
+ * // Some error occurred, you can inspect the code: error.code
+ * });
+ * ```
+ *
+ * @param email - The email address of the user whose password is to be
+ * reset.
+ * @param actionCodeSettings - The action
+ * code settings. If specified, the state/continue URL is set as the
+ * "continueUrl" parameter in the password reset link. The default password
+ * reset landing page will use this to display a link to go back to the app
+ * if it is installed.
+ * If the actionCodeSettings is not specified, no URL is appended to the
+ * action URL.
+ * The state URL provided must belong to a domain that is whitelisted by the
+ * developer in the console. Otherwise an error is thrown.
+ * Mobile app redirects are only applicable if the developer configures
+ * and accepts the Firebase Dynamic Links terms of service.
+ * The Android package name and iOS bundle ID are respected only if they
+ * are configured in the same Firebase Auth project.
+ * @returns A promise that resolves with the generated link.
+ */
+ public generatePasswordResetLink(email: string, actionCodeSettings?: ActionCodeSettings): Promise {
+ return this.authRequestHandler.getEmailActionLink('PASSWORD_RESET', email, actionCodeSettings);
+ }
+
+ /**
+ * Generates the out of band email action link to verify the user's ownership
+ * of the specified email. The {@link ActionCodeSettings} object provided
+ * as an argument to this method defines whether the link is to be handled by a
+ * mobile app or browser along with additional state information to be passed in
+ * the deep link, etc.
+ *
+ * @example
+ * ```javascript
+ * var actionCodeSettings = {
+ * url: 'https://www.example.com/cart?email=user@example.com&cartId=123',
+ * iOS: {
+ * bundleId: 'com.example.ios'
+ * },
+ * android: {
+ * packageName: 'com.example.android',
+ * installApp: true,
+ * minimumVersion: '12'
+ * },
+ * handleCodeInApp: true,
+ * dynamicLinkDomain: 'custom.page.link'
+ * };
+ * admin.auth()
+ * .generateEmailVerificationLink('user@example.com', actionCodeSettings)
+ * .then(function(link) {
+ * // The link was successfully generated.
+ * })
+ * .catch(function(error) {
+ * // Some error occurred, you can inspect the code: error.code
+ * });
+ * ```
+ *
+ * @param email - The email account to verify.
+ * @param actionCodeSettings - The action
+ * code settings. If specified, the state/continue URL is set as the
+ * "continueUrl" parameter in the email verification link. The default email
+ * verification landing page will use this to display a link to go back to
+ * the app if it is installed.
+ * If the actionCodeSettings is not specified, no URL is appended to the
+ * action URL.
+ * The state URL provided must belong to a domain that is whitelisted by the
+ * developer in the console. Otherwise an error is thrown.
+ * Mobile app redirects are only applicable if the developer configures
+ * and accepts the Firebase Dynamic Links terms of service.
+ * The Android package name and iOS bundle ID are respected only if they
+ * are configured in the same Firebase Auth project.
+ * @returns A promise that resolves with the generated link.
+ */
+ public generateEmailVerificationLink(email: string, actionCodeSettings?: ActionCodeSettings): Promise {
+ return this.authRequestHandler.getEmailActionLink('VERIFY_EMAIL', email, actionCodeSettings);
+ }
+
+ /**
+ * Generates an out-of-band email action link to verify the user's ownership
+ * of the specified email. The {@link ActionCodeSettings} object provided
+ * as an argument to this method defines whether the link is to be handled by a
+ * mobile app or browser along with additional state information to be passed in
+ * the deep link, etc.
+ *
+ * @param email - The current email account.
+ * @param newEmail - The email address the account is being updated to.
+ * @param actionCodeSettings - The action
+ * code settings. If specified, the state/continue URL is set as the
+ * "continueUrl" parameter in the email verification link. The default email
+ * verification landing page will use this to display a link to go back to
+ * the app if it is installed.
+ * If the actionCodeSettings is not specified, no URL is appended to the
+ * action URL.
+ * The state URL provided must belong to a domain that is authorized
+ * in the console, or an error will be thrown.
+ * Mobile app redirects are only applicable if the developer configures
+ * and accepts the Firebase Dynamic Links terms of service.
+ * The Android package name and iOS bundle ID are respected only if they
+ * are configured in the same Firebase Auth project.
+ * @returns A promise that resolves with the generated link.
+ */
+ public generateVerifyAndChangeEmailLink(email: string, newEmail: string,
+ actionCodeSettings?: ActionCodeSettings): Promise {
+ return this.authRequestHandler.getEmailActionLink('VERIFY_AND_CHANGE_EMAIL', email, actionCodeSettings, newEmail);
+ }
+
+ /**
+ * Generates the out of band email action link to verify the user's ownership
+ * of the specified email. The {@link ActionCodeSettings} object provided
+ * as an argument to this method defines whether the link is to be handled by a
+ * mobile app or browser along with additional state information to be passed in
+ * the deep link, etc.
+ *
+ * @example
+ * ```javascript
+ * var actionCodeSettings = {
+ * url: 'https://www.example.com/cart?email=user@example.com&cartId=123',
+ * iOS: {
+ * bundleId: 'com.example.ios'
+ * },
+ * android: {
+ * packageName: 'com.example.android',
+ * installApp: true,
+ * minimumVersion: '12'
+ * },
+ * handleCodeInApp: true,
+ * dynamicLinkDomain: 'custom.page.link'
+ * };
+ * admin.auth()
+ * .generateEmailVerificationLink('user@example.com', actionCodeSettings)
+ * .then(function(link) {
+ * // The link was successfully generated.
+ * })
+ * .catch(function(error) {
+ * // Some error occurred, you can inspect the code: error.code
+ * });
+ * ```
+ *
+ * @param email - The email account to verify.
+ * @param actionCodeSettings - The action
+ * code settings. If specified, the state/continue URL is set as the
+ * "continueUrl" parameter in the email verification link. The default email
+ * verification landing page will use this to display a link to go back to
+ * the app if it is installed.
+ * If the actionCodeSettings is not specified, no URL is appended to the
+ * action URL.
+ * The state URL provided must belong to a domain that is whitelisted by the
+ * developer in the console. Otherwise an error is thrown.
+ * Mobile app redirects are only applicable if the developer configures
+ * and accepts the Firebase Dynamic Links terms of service.
+ * The Android package name and iOS bundle ID are respected only if they
+ * are configured in the same Firebase Auth project.
+ * @returns A promise that resolves with the generated link.
+ */
+ public generateSignInWithEmailLink(email: string, actionCodeSettings: ActionCodeSettings): Promise {
+ return this.authRequestHandler.getEmailActionLink('EMAIL_SIGNIN', email, actionCodeSettings);
+ }
+
+ /**
+ * Returns the list of existing provider configurations matching the filter
+ * provided. At most, 100 provider configs can be listed at a time.
+ *
+ * SAML and OIDC provider support requires Google Cloud's Identity Platform
+ * (GCIP). To learn more about GCIP, including pricing and features,
+ * see the {@link https://cloud.google.com/identity-platform | GCIP documentation}.
+ *
+ * @param options - The provider config filter to apply.
+ * @returns A promise that resolves with the list of provider configs meeting the
+ * filter requirements.
+ */
+ public listProviderConfigs(options: AuthProviderConfigFilter): Promise {
+ const processResponse = (response: any, providerConfigs: AuthProviderConfig[]): ListProviderConfigResults => {
+ // Return list of provider configuration and the next page token if available.
+ const result: ListProviderConfigResults = {
+ providerConfigs,
+ };
+ // Delete result.pageToken if undefined.
+ if (Object.prototype.hasOwnProperty.call(response, 'nextPageToken')) {
+ result.pageToken = response.nextPageToken;
+ }
+ return result;
+ };
+ if (options && options.type === 'oidc') {
+ return this.authRequestHandler.listOAuthIdpConfigs(options.maxResults, options.pageToken)
+ .then((response: any) => {
+ // List of provider configurations to return.
+ const providerConfigs: OIDCConfig[] = [];
+ // Convert each provider config response to a OIDCConfig.
+ response.oauthIdpConfigs.forEach((configResponse: any) => {
+ providerConfigs.push(new OIDCConfig(configResponse));
+ });
+ // Return list of provider configuration and the next page token if available.
+ return processResponse(response, providerConfigs);
+ });
+ } else if (options && options.type === 'saml') {
+ return this.authRequestHandler.listInboundSamlConfigs(options.maxResults, options.pageToken)
+ .then((response: any) => {
+ // List of provider configurations to return.
+ const providerConfigs: SAMLConfig[] = [];
+ // Convert each provider config response to a SAMLConfig.
+ response.inboundSamlConfigs.forEach((configResponse: any) => {
+ providerConfigs.push(new SAMLConfig(configResponse));
+ });
+ // Return list of provider configuration and the next page token if available.
+ return processResponse(response, providerConfigs);
+ });
+ }
+ return Promise.reject(
+ new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"AuthProviderConfigFilter.type" must be either "saml" or "oidc"'));
+ }
+
+ /**
+ * Looks up an Auth provider configuration by the provided ID.
+ * Returns a promise that resolves with the provider configuration
+ * corresponding to the provider ID specified. If the specified ID does not
+ * exist, an `auth/configuration-not-found` error is thrown.
+ *
+ * SAML and OIDC provider support requires Google Cloud's Identity Platform
+ * (GCIP). To learn more about GCIP, including pricing and features,
+ * see the {@link https://cloud.google.com/identity-platform | GCIP documentation}.
+ *
+ * @param providerId - The provider ID corresponding to the provider
+ * config to return.
+ * @returns A promise that resolves
+ * with the configuration corresponding to the provided ID.
+ */
+ public getProviderConfig(providerId: string): Promise {
+ if (OIDCConfig.isProviderId(providerId)) {
+ return this.authRequestHandler.getOAuthIdpConfig(providerId)
+ .then((response: OIDCConfigServerResponse) => {
+ return new OIDCConfig(response);
+ });
+ } else if (SAMLConfig.isProviderId(providerId)) {
+ return this.authRequestHandler.getInboundSamlConfig(providerId)
+ .then((response: SAMLConfigServerResponse) => {
+ return new SAMLConfig(response);
+ });
+ }
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID));
+ }
+
+ /**
+ * Deletes the provider configuration corresponding to the provider ID passed.
+ * If the specified ID does not exist, an `auth/configuration-not-found` error
+ * is thrown.
+ *
+ * SAML and OIDC provider support requires Google Cloud's Identity Platform
+ * (GCIP). To learn more about GCIP, including pricing and features,
+ * see the {@link https://cloud.google.com/identity-platform | GCIP documentation}.
+ *
+ * @param providerId - The provider ID corresponding to the provider
+ * config to delete.
+ * @returns A promise that resolves on completion.
+ */
+ public deleteProviderConfig(providerId: string): Promise {
+ if (OIDCConfig.isProviderId(providerId)) {
+ return this.authRequestHandler.deleteOAuthIdpConfig(providerId);
+ } else if (SAMLConfig.isProviderId(providerId)) {
+ return this.authRequestHandler.deleteInboundSamlConfig(providerId);
+ }
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID));
+ }
+
+ /**
+ * Returns a promise that resolves with the updated `AuthProviderConfig`
+ * corresponding to the provider ID specified.
+ * If the specified ID does not exist, an `auth/configuration-not-found` error
+ * is thrown.
+ *
+ * SAML and OIDC provider support requires Google Cloud's Identity Platform
+ * (GCIP). To learn more about GCIP, including pricing and features,
+ * see the {@link https://cloud.google.com/identity-platform | GCIP documentation}.
+ *
+ * @param providerId - The provider ID corresponding to the provider
+ * config to update.
+ * @param updatedConfig - The updated configuration.
+ * @returns A promise that resolves with the updated provider configuration.
+ */
+ public updateProviderConfig(
+ providerId: string, updatedConfig: UpdateAuthProviderRequest): Promise {
+ if (!validator.isNonNullObject(updatedConfig)) {
+ return Promise.reject(new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ 'Request is missing "UpdateAuthProviderRequest" configuration.',
+ ));
+ }
+ if (OIDCConfig.isProviderId(providerId)) {
+ return this.authRequestHandler.updateOAuthIdpConfig(providerId, updatedConfig)
+ .then((response) => {
+ return new OIDCConfig(response);
+ });
+ } else if (SAMLConfig.isProviderId(providerId)) {
+ return this.authRequestHandler.updateInboundSamlConfig(providerId, updatedConfig)
+ .then((response) => {
+ return new SAMLConfig(response);
+ });
+ }
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID));
+ }
+
+ /**
+ * Returns a promise that resolves with the newly created `AuthProviderConfig`
+ * when the new provider configuration is created.
+ *
+ * SAML and OIDC provider support requires Google Cloud's Identity Platform
+ * (GCIP). To learn more about GCIP, including pricing and features,
+ * see the {@link https://cloud.google.com/identity-platform | GCIP documentation}.
+ *
+ * @param config - The provider configuration to create.
+ * @returns A promise that resolves with the created provider configuration.
+ */
+ public createProviderConfig(config: AuthProviderConfig): Promise {
+ if (!validator.isNonNullObject(config)) {
+ return Promise.reject(new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ 'Request is missing "AuthProviderConfig" configuration.',
+ ));
+ }
+ if (OIDCConfig.isProviderId(config.providerId)) {
+ return this.authRequestHandler.createOAuthIdpConfig(config as OIDCAuthProviderConfig)
+ .then((response) => {
+ return new OIDCConfig(response);
+ });
+ } else if (SAMLConfig.isProviderId(config.providerId)) {
+ return this.authRequestHandler.createInboundSamlConfig(config as SAMLAuthProviderConfig)
+ .then((response) => {
+ return new SAMLConfig(response);
+ });
+ }
+ return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID));
+ }
+
+ /** @alpha */
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ public _verifyAuthBlockingToken(
+ token: string,
+ audience?: string
+ ): Promise {
+ const isEmulator = useEmulator();
+ return this.authBlockingTokenVerifier._verifyAuthBlockingToken(token, isEmulator, audience)
+ .then((decodedAuthBlockingToken: DecodedAuthBlockingToken) => {
+ return decodedAuthBlockingToken;
+ });
+ }
+
+ /**
+ * Verifies the decoded Firebase issued JWT is not revoked or disabled. Returns a promise that
+ * resolves with the decoded claims on success. Rejects the promise with revocation error if revoked
+ * or user disabled.
+ *
+ * @param decodedIdToken - The JWT's decoded claims.
+ * @param revocationErrorInfo - The revocation error info to throw on revocation
+ * detection.
+ * @returns A promise that will be fulfilled after a successful verification.
+ */
+ private verifyDecodedJWTNotRevokedOrDisabled(
+ decodedIdToken: DecodedIdToken, revocationErrorInfo: ErrorInfo): Promise {
+ // Get tokens valid after time for the corresponding user.
+ return this.getUser(decodedIdToken.sub)
+ .then((user: UserRecord) => {
+ if (user.disabled) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.USER_DISABLED,
+ 'The user record is disabled.');
+ }
+ // If no tokens valid after time available, token is not revoked.
+ if (user.tokensValidAfterTime) {
+ // Get the ID token authentication time and convert to milliseconds UTC.
+ const authTimeUtc = decodedIdToken.auth_time * 1000;
+ // Get user tokens valid after time in milliseconds UTC.
+ const validSinceUtc = new Date(user.tokensValidAfterTime).getTime();
+ // Check if authentication time is older than valid since time.
+ if (authTimeUtc < validSinceUtc) {
+ throw new FirebaseAuthError(revocationErrorInfo);
+ }
+ }
+ // All checks above passed. Return the decoded token.
+ return decodedIdToken;
+ });
+ }
+}
\ No newline at end of file
diff --git a/packages/dart_firebase_admin/lib/src/auth/create_request.dart b/packages/dart_firebase_admin/lib/src/auth/create_request.dart
deleted file mode 100644
index 5618ab5..0000000
--- a/packages/dart_firebase_admin/lib/src/auth/create_request.dart
+++ /dev/null
@@ -1,28 +0,0 @@
-part of '../../dart_firebase_admin.dart';
-
-class CreateRequest extends UpdateRequest {
- CreateRequest({
- bool? disabled,
- String? displayName,
- String? email,
- bool? emailVerified,
- String? password,
- String? photoURL,
- List? providersToUnlink,
- String? providerToLink,
- this.uid,
- }) : super(
- disabled: disabled,
- displayName: displayName,
- email: email,
- emailVerified: emailVerified,
- password: password,
- photoURL: photoURL,
- providersToUnlink: providersToUnlink,
- providerToLink: providerToLink,
- );
-
- // TODO multiFactor
-
- final String? uid;
-}
diff --git a/packages/dart_firebase_admin/lib/src/auth/delete_users_result.dart b/packages/dart_firebase_admin/lib/src/auth/delete_users_result.dart
deleted file mode 100644
index ce38cab..0000000
--- a/packages/dart_firebase_admin/lib/src/auth/delete_users_result.dart
+++ /dev/null
@@ -1,27 +0,0 @@
-part of '../../dart_firebase_admin.dart';
-
-class DeleteUsersResult {
- DeleteUsersResult._(this.localIds, this._delegate);
-
- final List localIds;
-
- final firebase_auth_v1.GoogleCloudIdentitytoolkitV1BatchDeleteAccountsResponse
- _delegate;
-
- /// The number of user records that failed to be deleted (possibly zero).
- int get failureCount => _delegate.errors?.length ?? 0;
-
- /// The number of users that were deleted successfully (possibly zero).
- ///
- /// Users that did not exist prior to calling [FirebaseAdminAuth.deleteUsers]
- /// are considered to be successfully deleted.
- int get successCount => localIds.length - failureCount;
-
- /// A list of FirebaseArrayIndexError instances describing the errors that
- /// were encountered during the deletion
- List get errors =>
- _delegate.errors?.map((e) {
- return FirebaseArrayIndexException(e.index!, e.message ?? 'Unknown');
- }).toList() ??
- [];
-}
diff --git a/packages/dart_firebase_admin/lib/src/auth/firebase_admin_auth.dart b/packages/dart_firebase_admin/lib/src/auth/firebase_admin_auth.dart
deleted file mode 100644
index 1474bee..0000000
--- a/packages/dart_firebase_admin/lib/src/auth/firebase_admin_auth.dart
+++ /dev/null
@@ -1,254 +0,0 @@
-part of '../../dart_firebase_admin.dart';
-
-class FirebaseAdminAuth {
- FirebaseAdminAuth(this.app);
- final FirebaseAdminApp app;
-
- auth.AuthClient? _client;
-
- Object tenantManager() {
- return {};
- }
-
- Future _getClient() async {
- return _client ??= await app._credential._getAuthClient([
- firebase_auth_v3.IdentityToolkitApi.cloudPlatformScope,
- firebase_auth_v3.IdentityToolkitApi.firebaseScope,
- ]);
- }
-
- Future _v1() async {
- return firebase_auth_v1.IdentityToolkitApi(await _getClient());
- }
-
- Future _v2() async {
- return firebase_auth_v2.IdentityToolkitApi(await _getClient());
- }
-
- Future _v3() async {
- return firebase_auth_v3.IdentityToolkitApi(await _getClient());
- }
-
- Future createCustomToken(
- String uid, [
- Map? developerClaims,
- ]) async {
- throw UnimplementedError();
- }
-
- Future createProviderConfig(Object config) async {
- throw UnimplementedError();
- }
-
- Future createSessionCookie(String idToken, {int? expiresIn}) async {
- return guard(
- () async {
- final request = firebase_auth_v1
- .GoogleCloudIdentitytoolkitV1CreateSessionCookieRequest(
- idToken: idToken,
- validDuration: expiresIn?.toString(),
- );
-
- final response = await (await _v1()).projects.createSessionCookie(
- request,
- app._projectId,
- );
-
- return response.sessionCookie ?? '';
- },
- );
- }
-
- Future createUser(
- CreateRequest properties,
- ) async {
- throw UnimplementedError();
- }
-
- Future deleteProviderConfig(
- String providerId,
- ) async {
- return;
- }
-
- Future deleteUser(
- String uid,
- ) async {
- return guard(() async {
- final request =
- firebase_auth_v3.IdentitytoolkitRelyingpartyDeleteAccountRequest(
- localId: uid,
- );
-
- await (await _v3()).relyingparty.deleteAccount(request);
- });
- }
-
- Future deleteUsers(
- List uids,
- ) async {
- return guard(
- () async {
- final localIds = uids.where((element) => element.isUid).toList();
-
- final request = firebase_auth_v1
- .GoogleCloudIdentitytoolkitV1BatchDeleteAccountsRequest(
- localIds: localIds,
- );
-
- final response = await (await _v1()).projects.accounts_1.batchDelete(
- request,
- app._projectId,
- );
-
- return DeleteUsersResult._(localIds, response);
- },
- );
- }
-
- Future generateEmailVerificationLink(
- String email, [
- Object? actionCodeSettings,
- ]) async {
- throw UnimplementedError();
- }
-
- Future generatePasswordResetLink(
- String email, [
- Object? actionCodeSettings,
- ]) async {
- throw UnimplementedError();
- }
-
- Future generateSignInWithEmailLink(
- String email,
- Object actionCodeSettings,
- ) async {
- throw UnimplementedError();
- }
-
- Future getProviderConfig(String providerId) async {
- throw UnimplementedError();
- }
-
- Future _getUserRecord(
- firebase_auth_v1.GoogleCloudIdentitytoolkitV1GetAccountInfoRequest request,
- ) async {
- final response = await (await _v1()).projects.accounts_1.lookup(
- request,
- app._projectId,
- );
-
- if (response.users == null || response.users!.isEmpty) {
- throw FirebaseAuthAdminException.fromAuthClientErrorCode(
- AuthClientErrorCode.userNotFound,
- );
- }
-
- return UserRecord._(response.users!.first);
- }
-
- Future getUser(String uid) async {
- return guard(
- () async {
- if (!uid.isUid) {
- throw FirebaseAuthAdminException.fromAuthClientErrorCode(
- AuthClientErrorCode.invalidUid,
- );
- }
-
- return _getUserRecord(
- firebase_auth_v1.GoogleCloudIdentitytoolkitV1GetAccountInfoRequest(
- localId: [uid],
- ),
- );
- },
- );
- }
-
- Future getUserByEmail(String email) async {
- return guard(
- () async {
- if (!email.isEmail) {
- throw FirebaseAuthAdminException.fromAuthClientErrorCode(
- AuthClientErrorCode.invalidEmail,
- );
- }
-
- return _getUserRecord(
- firebase_auth_v1.GoogleCloudIdentitytoolkitV1GetAccountInfoRequest(
- email: [email],
- ),
- );
- },
- );
- }
-
- Future getUserByPhoneNumber(String phoneNumber) async {
- return guard(
- () async {
- if (!phoneNumber.isPhoneNumber) {
- throw FirebaseAuthAdminException.fromAuthClientErrorCode(
- AuthClientErrorCode.invalidPhoneNumber,
- );
- }
-
- return _getUserRecord(
- firebase_auth_v1.GoogleCloudIdentitytoolkitV1GetAccountInfoRequest(
- phoneNumber: [phoneNumber],
- ),
- );
- },
- );
- }
-
- Future getUserByProviderUid(String providerId, String uid) async {
- throw UnimplementedError();
- }
-
- Future getUsers(List identifiers) async {
- throw UnimplementedError();
- }
-
- Future importUsers(List users, [Object? options]) async {
- throw UnimplementedError();
- }
-
- Future listProviderConfigs(Object options) async {
- throw UnimplementedError();
- }
-
- Future listUsers({int? maxResults = 1000, String? pageToken}) async {
- throw UnimplementedError();
- }
-
- Future revokeRefreshTokens(String uid) async {
- throw UnimplementedError();
- }
-
- Future setCustomUserClaims(String uid, Object? customUserClaims) async {
- throw UnimplementedError();
- }
-
- Future updateProviderConfig(
- String providerId,
- Object updatedConfig,
- ) async {
- throw UnimplementedError();
- }
-
- Future updateUser(String uid, Object properties) async {
- throw UnimplementedError();
- }
-
- Future verifyIdToken(String idToken, {bool? checkRevoked}) async {
- throw UnimplementedError();
- }
-
- Future verifySessionCookie(
- String sessionCookie, {
- bool? checkRevoked,
- }) async {
- throw UnimplementedError();
- }
-}
diff --git a/packages/dart_firebase_admin/lib/src/auth/identifier.dart b/packages/dart_firebase_admin/lib/src/auth/identifier.dart
new file mode 100644
index 0000000..5b1d556
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/identifier.dart
@@ -0,0 +1,62 @@
+import '../dart_firebase_admin.dart';
+import '../utils/validator.dart';
+import 'base_auth.dart';
+
+/// Identifies a user to be looked up.
+///
+/// See also:
+/// - [ProviderIdentifier]
+/// - [PhoneIdentifier]
+/// - [EmailIdentifier]
+/// - [UidIdentifier]
+sealed class UserIdentifier {}
+
+/// Used for looking up an account by federated provider.
+///
+/// See [BaseAuth.getUsers].
+class ProviderIdentifier extends UserIdentifier {
+ ProviderIdentifier({required this.providerId, required this.providerUid}) {
+ if (providerId.isEmpty) {
+ throw FirebaseAuthAdminException(AuthClientErrorCode.invalidProviderId);
+ }
+ if (providerUid.isEmpty) {
+ throw FirebaseAuthAdminException(AuthClientErrorCode.invalidProviderUid);
+ }
+ }
+
+ final String providerId;
+ final String providerUid;
+}
+
+/// Used for looking up an account by phone number.
+///
+/// See [BaseAuth.getUsers].
+class PhoneIdentifier extends UserIdentifier {
+ PhoneIdentifier({required this.phoneNumber}) {
+ assertIsPhoneNumber(phoneNumber);
+ }
+
+ final String phoneNumber;
+}
+
+/// Used for looking up an account by email.
+///
+/// See [BaseAuth.getUsers].
+class EmailIdentifier extends UserIdentifier {
+ EmailIdentifier({required this.email}) {
+ assertIsEmail(email);
+ }
+
+ final String email;
+}
+
+/// Used for looking up an account by uid.
+///
+/// See [BaseAuth.getUsers].
+class UidIdentifier extends UserIdentifier {
+ UidIdentifier({required this.uid}) {
+ assertIsUid(uid);
+ }
+
+ final String uid;
+}
diff --git a/packages/dart_firebase_admin/lib/src/auth/identifier.ts b/packages/dart_firebase_admin/lib/src/auth/identifier.ts
new file mode 100644
index 0000000..fcd978d
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/identifier.ts
@@ -0,0 +1,80 @@
+/*!
+ * Copyright 2020 Google Inc.
+ *
+ * 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.
+ */
+
+/**
+ * Used for looking up an account by uid.
+ *
+ * See {@link BaseAuth.getUsers}.
+ */
+export interface UidIdentifier {
+ uid: string;
+}
+
+/**
+ * Used for looking up an account by email.
+ *
+ * See {@link BaseAuth.getUsers}.
+ */
+export interface EmailIdentifier {
+ email: string;
+}
+
+/**
+ * Used for looking up an account by phone number.
+ *
+ * See {@link BaseAuth.getUsers}.
+ */
+export interface PhoneIdentifier {
+ phoneNumber: string;
+}
+
+/**
+ * Used for looking up an account by federated provider.
+ *
+ * See {@link BaseAuth.getUsers}.
+ */
+export interface ProviderIdentifier {
+ providerId: string;
+ providerUid: string;
+}
+
+/**
+ * Identifies a user to be looked up.
+ */
+export type UserIdentifier =
+ UidIdentifier | EmailIdentifier | PhoneIdentifier | ProviderIdentifier;
+
+/*
+ * User defined type guards. See
+ * https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards
+ */
+
+export function isUidIdentifier(id: UserIdentifier): id is UidIdentifier {
+ return (id as UidIdentifier).uid !== undefined;
+}
+
+export function isEmailIdentifier(id: UserIdentifier): id is EmailIdentifier {
+ return (id as EmailIdentifier).email !== undefined;
+}
+
+export function isPhoneIdentifier(id: UserIdentifier): id is PhoneIdentifier {
+ return (id as PhoneIdentifier).phoneNumber !== undefined;
+}
+
+export function isProviderIdentifier(id: ProviderIdentifier): id is ProviderIdentifier {
+ const pid = id as ProviderIdentifier;
+ return pid.providerId !== undefined && pid.providerUid !== undefined;
+}
\ No newline at end of file
diff --git a/packages/dart_firebase_admin/lib/src/auth/token_verifier.dart b/packages/dart_firebase_admin/lib/src/auth/token_verifier.dart
new file mode 100644
index 0000000..a9cfe4d
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/token_verifier.dart
@@ -0,0 +1,209 @@
+import '../dart_firebase_admin.dart';
+import '../utils/error.dart';
+import '../utils/jwt.dart';
+
+class FirebaseTokenInfo {
+ FirebaseTokenInfo({
+ required this.url,
+ required this.verifyApiName,
+ required this.jwtName,
+ required this.shortName,
+ required this.expiredErrorCode,
+ }) {
+ if (verifyApiName.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ 'The JWT verify API name must be a non-empty string.',
+ );
+ }
+ if (jwtName.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ 'The JWT public full name must be a non-empty string.',
+ );
+ }
+ if (shortName.isEmpty) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ 'The JWT public full name must be a non-empty string.',
+ );
+ }
+ }
+
+ /// Documentation URL.
+ final Uri url;
+
+ /// verify API name.
+ final String verifyApiName;
+
+ /// The JWT full name.
+ final String jwtName;
+
+ /// The JWT short name.
+ final String shortName;
+
+ /// JWT Expiration error code.
+ final ErrorInfo expiredErrorCode;
+}
+
+class FirebaseTokenVerifier {
+ FirebaseTokenVerifier({
+ required Uri clientCertUrl,
+ required this.issuer,
+ required this.tokenInfo,
+ required this.app,
+ }) : _shortNameArticle = RegExp('[aeiou]', caseSensitive: false)
+ .hasMatch(tokenInfo.shortName[0])
+ ? 'an'
+ : 'a',
+ _signatureVerifier =
+ PublicKeySignatureVerifier.withCertificateUrl(clientCertUrl);
+
+ final String _shortNameArticle;
+ final Uri issuer;
+ final FirebaseAdminApp app;
+ final FirebaseTokenInfo tokenInfo;
+ final SignatureVerifier _signatureVerifier;
+}
+
+class TokenProvider {
+ // TODO optional parameters
+ TokenProvider({
+ required this.identities,
+ required this.signInProvider,
+ required this.signInSecondFactor,
+ required this.secondFactorIdentifier,
+ required this.tenant,
+ });
+
+ /// Provider-specific identity details corresponding
+ /// to the provider used to sign in the user.
+ final Map identities;
+
+ /// The ID of the provider used to sign in the user.
+ /// One of `"anonymous"`, `"password"`, `"facebook.com"`, `"github.com"`,
+ /// `"google.com"`, `"twitter.com"`, `"apple.com"`, `"microsoft.com"`,
+ /// `"yahoo.com"`, `"phone"`, `"playgames.google.com"`, `"gc.apple.com"`,
+ /// or `"custom"`.
+ ///
+ /// Additional Identity Platform provider IDs include `"linkedin.com"`,
+ /// OIDC and SAML identity providers prefixed with `"saml."` and `"oidc."`
+ /// respectively.
+ final String signInProvider;
+
+ /// The type identifier or `factorId` of the second factor, provided the
+ /// ID token was obtained from a multi-factor authenticated user.
+ /// For phone, this is `"phone"`.
+ final String? signInSecondFactor;
+
+ /// The `uid` of the second factor used to sign in, provided the
+ /// ID token was obtained from a multi-factor authenticated user.
+ final String? secondFactorIdentifier;
+
+ /// The ID of the tenant the user belongs to, if available.
+ final String? tenant;
+ // TODO allow any key
+ // [key: string]: any;
+}
+
+/// Interface representing a decoded Firebase ID token, returned from the
+/// {@link BaseAuth.verifyIdToken} method.
+///
+/// Firebase ID tokens are OpenID Connect spec-compliant JSON Web Tokens (JWTs).
+/// See the
+/// [ID Token section of the OpenID Connect spec](http://openid.net/specs/openid-connect-core-1_0.html#IDToken)
+/// for more information about the specific properties below.
+class DecodedIdToken {
+ // TODO optional parameters
+ DecodedIdToken({
+ required this.aud,
+ required this.authTime,
+ required this.email,
+ required this.emailVerified,
+ required this.exp,
+ required this.firebase,
+ required this.iat,
+ required this.iss,
+ required this.phoneNumber,
+ required this.picture,
+ required this.sub,
+ required this.uid,
+ });
+
+ /// The audience for which this token is intended.
+ ///
+ /// This value is a string equal to your Firebase project ID, the unique
+ /// identifier for your Firebase project, which can be found in [your project's
+ /// settings](https://console.firebase.google.com/project/_/settings/general/android:com.random.android).
+ final String aud;
+
+ /// Time, in seconds since the Unix epoch, when the end-user authentication
+ /// occurred.
+ ///
+ /// This value is not set when this particular ID token was created, but when the
+ /// user initially logged in to this session. In a single session, the Firebase
+ /// SDKs will refresh a user's ID tokens every hour. Each ID token will have a
+ /// different [`iat`](#iat) value, but the same `auth_time` value.
+ final DateTime authTime;
+
+ /// The email of the user to whom the ID token belongs, if available.
+ final String? email;
+
+ /// Whether or not the email of the user to whom the ID token belongs is
+ /// verified, provided the user has an email.
+ final bool? emailVerified;
+
+ /// The ID token's expiration time, in seconds since the Unix epoch. That is, the
+ /// time at which this ID token expires and should no longer be considered valid.
+ ///
+ /// The Firebase SDKs transparently refresh ID tokens every hour, issuing a new
+ /// ID token with up to a one hour expiration.
+ final int exp;
+
+ /// Information about the sign in event, including which sign in provider was
+ /// used and provider-specific identity details.
+ ///
+ /// This data is provided by the Firebase Authentication service and is a
+ /// reserved claim in the ID token.
+ final TokenProvider firebase;
+
+ /// The ID token's issued-at time, in seconds since the Unix epoch. That is, the
+ /// time at which this ID token was issued and should start to be considered
+ /// valid.
+ ///
+ /// The Firebase SDKs transparently refresh ID tokens every hour, issuing a new
+ /// ID token with a new issued-at time. If you want to get the time at which the
+ /// user session corresponding to the ID token initially occurred, see the
+ /// [`auth_time`](#auth_time) property.
+ final int iat;
+
+ /// The issuer identifier for the issuer of the response.
+ ///
+ /// This value is a URL with the format
+ /// `https://securetoken.google.com/`, where `` is the
+ /// same project ID specified in the [`aud`](#aud) property.
+ final String iss;
+
+ /// The phone number of the user to whom the ID token belongs, if available.
+ final String? phoneNumber;
+
+ /// The photo URL for the user to whom the ID token belongs, if available.
+ final String? picture;
+
+ /// The `uid` corresponding to the user who the ID token belonged to.
+ ///
+ /// As a convenience, this value is copied over to the [`uid`](#uid) property.
+ final String sub;
+
+ /// The `uid` corresponding to the user who the ID token belonged to.
+ ///
+ /// This value is not actually in the JWT token claims itself. It is added as a
+ /// convenience, and is set as the value of the [`sub`](#sub) property.
+ final String uid;
+
+ /**
+ * Other arbitrary claims included in the ID token.
+ */
+ // TODO allow any key
+ // [key: string]: any;
+}
diff --git a/packages/dart_firebase_admin/lib/src/auth/token_verifier.ts b/packages/dart_firebase_admin/lib/src/auth/token_verifier.ts
new file mode 100644
index 0000000..5788982
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/token_verifier.ts
@@ -0,0 +1,627 @@
+/*!
+ * Copyright 2018 Google Inc.
+ *
+ * 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.
+ */
+
+import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error';
+import * as util from '../utils/index';
+import * as validator from '../utils/validator';
+import {
+ DecodedToken, decodeJwt, JwtError, JwtErrorCode, EmulatorSignatureVerifier,
+ PublicKeySignatureVerifier, ALGORITHM_RS256, SignatureVerifier,
+} from '../utils/jwt';
+import { App } from '../app/index';
+
+/**
+ * Interface representing a decoded Firebase ID token, returned from the
+ * {@link BaseAuth.verifyIdToken} method.
+ *
+ * Firebase ID tokens are OpenID Connect spec-compliant JSON Web Tokens (JWTs).
+ * See the
+ * [ID Token section of the OpenID Connect spec](http://openid.net/specs/openid-connect-core-1_0.html#IDToken)
+ * for more information about the specific properties below.
+ */
+export interface DecodedIdToken {
+
+ /**
+ * The audience for which this token is intended.
+ *
+ * This value is a string equal to your Firebase project ID, the unique
+ * identifier for your Firebase project, which can be found in [your project's
+ * settings](https://console.firebase.google.com/project/_/settings/general/android:com.random.android).
+ */
+ aud: string;
+
+ /**
+ * Time, in seconds since the Unix epoch, when the end-user authentication
+ * occurred.
+ *
+ * This value is not set when this particular ID token was created, but when the
+ * user initially logged in to this session. In a single session, the Firebase
+ * SDKs will refresh a user's ID tokens every hour. Each ID token will have a
+ * different [`iat`](#iat) value, but the same `auth_time` value.
+ */
+ auth_time: number;
+
+ /**
+ * The email of the user to whom the ID token belongs, if available.
+ */
+ email?: string;
+
+ /**
+ * Whether or not the email of the user to whom the ID token belongs is
+ * verified, provided the user has an email.
+ */
+ email_verified?: boolean;
+
+ /**
+ * The ID token's expiration time, in seconds since the Unix epoch. That is, the
+ * time at which this ID token expires and should no longer be considered valid.
+ *
+ * The Firebase SDKs transparently refresh ID tokens every hour, issuing a new
+ * ID token with up to a one hour expiration.
+ */
+ exp: number;
+
+ /**
+ * Information about the sign in event, including which sign in provider was
+ * used and provider-specific identity details.
+ *
+ * This data is provided by the Firebase Authentication service and is a
+ * reserved claim in the ID token.
+ */
+ firebase: {
+
+ /**
+ * Provider-specific identity details corresponding
+ * to the provider used to sign in the user.
+ */
+ identities: {
+ [key: string]: any;
+ };
+
+ /**
+ * The ID of the provider used to sign in the user.
+ * One of `"anonymous"`, `"password"`, `"facebook.com"`, `"github.com"`,
+ * `"google.com"`, `"twitter.com"`, `"apple.com"`, `"microsoft.com"`,
+ * `"yahoo.com"`, `"phone"`, `"playgames.google.com"`, `"gc.apple.com"`,
+ * or `"custom"`.
+ *
+ * Additional Identity Platform provider IDs include `"linkedin.com"`,
+ * OIDC and SAML identity providers prefixed with `"saml."` and `"oidc."`
+ * respectively.
+ */
+ sign_in_provider: string;
+
+ /**
+ * The type identifier or `factorId` of the second factor, provided the
+ * ID token was obtained from a multi-factor authenticated user.
+ * For phone, this is `"phone"`.
+ */
+ sign_in_second_factor?: string;
+
+ /**
+ * The `uid` of the second factor used to sign in, provided the
+ * ID token was obtained from a multi-factor authenticated user.
+ */
+ second_factor_identifier?: string;
+
+ /**
+ * The ID of the tenant the user belongs to, if available.
+ */
+ tenant?: string;
+ [key: string]: any;
+ };
+
+ /**
+ * The ID token's issued-at time, in seconds since the Unix epoch. That is, the
+ * time at which this ID token was issued and should start to be considered
+ * valid.
+ *
+ * The Firebase SDKs transparently refresh ID tokens every hour, issuing a new
+ * ID token with a new issued-at time. If you want to get the time at which the
+ * user session corresponding to the ID token initially occurred, see the
+ * [`auth_time`](#auth_time) property.
+ */
+ iat: number;
+
+ /**
+ * The issuer identifier for the issuer of the response.
+ *
+ * This value is a URL with the format
+ * `https://securetoken.google.com/`, where `` is the
+ * same project ID specified in the [`aud`](#aud) property.
+ */
+ iss: string;
+
+ /**
+ * The phone number of the user to whom the ID token belongs, if available.
+ */
+ phone_number?: string;
+
+ /**
+ * The photo URL for the user to whom the ID token belongs, if available.
+ */
+ picture?: string;
+
+ /**
+ * The `uid` corresponding to the user who the ID token belonged to.
+ *
+ * As a convenience, this value is copied over to the [`uid`](#uid) property.
+ */
+ sub: string;
+
+ /**
+ * The `uid` corresponding to the user who the ID token belonged to.
+ *
+ * This value is not actually in the JWT token claims itself. It is added as a
+ * convenience, and is set as the value of the [`sub`](#sub) property.
+ */
+ uid: string;
+
+ /**
+ * Other arbitrary claims included in the ID token.
+ */
+ [key: string]: any;
+}
+
+/** @alpha */
+export interface DecodedAuthBlockingSharedUserInfo {
+ uid: string;
+ display_name?: string;
+ email?: string;
+ photo_url?: string;
+ phone_number?: string;
+}
+
+/** @alpha */
+export interface DecodedAuthBlockingMetadata {
+ creation_time?: number;
+ last_sign_in_time?: number;
+}
+
+/** @alpha */
+export interface DecodedAuthBlockingUserInfo extends DecodedAuthBlockingSharedUserInfo {
+ provider_id: string;
+}
+
+/** @alpha */
+export interface DecodedAuthBlockingMfaInfo {
+ uid: string;
+ display_name?: string;
+ phone_number?: string;
+ enrollment_time?: string;
+ factor_id?: string;
+}
+
+/** @alpha */
+export interface DecodedAuthBlockingEnrolledFactors {
+ enrolled_factors?: DecodedAuthBlockingMfaInfo[];
+}
+
+/** @alpha */
+export interface DecodedAuthBlockingUserRecord extends DecodedAuthBlockingSharedUserInfo {
+ email_verified?: boolean;
+ disabled?: boolean;
+ metadata?: DecodedAuthBlockingMetadata;
+ password_hash?: string;
+ password_salt?: string;
+ provider_data?: DecodedAuthBlockingUserInfo[];
+ multi_factor?: DecodedAuthBlockingEnrolledFactors;
+ custom_claims?: any;
+ tokens_valid_after_time?: number;
+ tenant_id?: string;
+ [key: string]: any;
+}
+
+/** @alpha */
+export interface DecodedAuthBlockingToken {
+ aud: string;
+ exp: number;
+ iat: number;
+ iss: string;
+ sub: string;
+ event_id: string;
+ event_type: string;
+ ip_address: string;
+ user_agent?: string;
+ locale?: string;
+ sign_in_method?: string;
+ user_record?: DecodedAuthBlockingUserRecord;
+ tenant_id?: string;
+ raw_user_info?: string;
+ sign_in_attributes?: {
+ [key: string]: any;
+ };
+ oauth_id_token?: string;
+ oauth_access_token?: string;
+ oauth_refresh_token?: string;
+ oauth_token_secret?: string;
+ oauth_expires_in?: number;
+ [key: string]: any;
+}
+
+// Audience to use for Firebase Auth Custom tokens
+const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit';
+
+// URL containing the public keys for the Google certs (whose private keys are used to sign Firebase
+// Auth ID tokens)
+const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com';
+
+// URL containing the public keys for Firebase session cookies. This will be updated to a different URL soon.
+const SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys';
+
+const EMULATOR_VERIFIER = new EmulatorSignatureVerifier();
+
+/**
+ * User facing token information related to the Firebase ID token.
+ *
+ * @internal
+ */
+export const ID_TOKEN_INFO: FirebaseTokenInfo = {
+ url: 'https://firebase.google.com/docs/auth/admin/verify-id-tokens',
+ verifyApiName: 'verifyIdToken()',
+ jwtName: 'Firebase ID token',
+ shortName: 'ID token',
+ expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED,
+};
+
+/**
+ * User facing token information related to the Firebase Auth Blocking token.
+ *
+ * @internal
+ */
+export const AUTH_BLOCKING_TOKEN_INFO: FirebaseTokenInfo = {
+ url: 'https://cloud.google.com/identity-platform/docs/blocking-functions',
+ verifyApiName: '_verifyAuthBlockingToken()',
+ jwtName: 'Firebase Auth Blocking token',
+ shortName: 'Auth Blocking token',
+ expiredErrorCode: AuthClientErrorCode.AUTH_BLOCKING_TOKEN_EXPIRED,
+};
+
+/**
+ * User facing token information related to the Firebase session cookie.
+ *
+ * @internal
+ */
+export const SESSION_COOKIE_INFO: FirebaseTokenInfo = {
+ url: 'https://firebase.google.com/docs/auth/admin/manage-cookies',
+ verifyApiName: 'verifySessionCookie()',
+ jwtName: 'Firebase session cookie',
+ shortName: 'session cookie',
+ expiredErrorCode: AuthClientErrorCode.SESSION_COOKIE_EXPIRED,
+};
+
+/**
+ * Interface that defines token related user facing information.
+ *
+ * @internal
+ */
+export interface FirebaseTokenInfo {
+ /** Documentation URL. */
+ url: string;
+ /** verify API name. */
+ verifyApiName: string;
+ /** The JWT full name. */
+ jwtName: string;
+ /** The JWT short name. */
+ shortName: string;
+ /** JWT Expiration error code. */
+ expiredErrorCode: ErrorInfo;
+}
+
+/**
+ * Class for verifying general purpose Firebase JWTs. This verifies ID tokens and session cookies.
+ *
+ * @internal
+ */
+export class FirebaseTokenVerifier {
+
+ private readonly shortNameArticle: string;
+ private readonly signatureVerifier: SignatureVerifier;
+
+ constructor(clientCertUrl: string, private issuer: string, private tokenInfo: FirebaseTokenInfo,
+ private readonly app: App) {
+
+ if (!validator.isURL(clientCertUrl)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'The provided public client certificate URL is an invalid URL.',
+ );
+ } else if (!validator.isURL(issuer)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'The provided JWT issuer is an invalid URL.',
+ );
+ } else if (!validator.isNonNullObject(tokenInfo)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'The provided JWT information is not an object or null.',
+ );
+ } else if (!validator.isURL(tokenInfo.url)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'The provided JWT verification documentation URL is invalid.',
+ );
+ } else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'The JWT verify API name must be a non-empty string.',
+ );
+ } else if (!validator.isNonEmptyString(tokenInfo.jwtName)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'The JWT public full name must be a non-empty string.',
+ );
+ } else if (!validator.isNonEmptyString(tokenInfo.shortName)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'The JWT public short name must be a non-empty string.',
+ );
+ } else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || !('code' in tokenInfo.expiredErrorCode)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ 'The JWT expiration error code must be a non-null ErrorInfo object.',
+ );
+ }
+ this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a';
+
+ this.signatureVerifier =
+ PublicKeySignatureVerifier.withCertificateUrl(clientCertUrl, app.options.httpAgent);
+
+ // For backward compatibility, the project ID is validated in the verification call.
+ }
+
+ /**
+ * Verifies the format and signature of a Firebase Auth JWT token.
+ *
+ * @param jwtToken - The Firebase Auth JWT token to verify.
+ * @param isEmulator - Whether to accept Auth Emulator tokens.
+ * @returns A promise fulfilled with the decoded claims of the Firebase Auth ID token.
+ */
+ public verifyJWT(jwtToken: string, isEmulator = false): Promise {
+ if (!validator.isString(jwtToken)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ `First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`,
+ );
+ }
+
+ return this.ensureProjectId()
+ .then((projectId) => {
+ return this.decodeAndVerify(jwtToken, projectId, isEmulator);
+ })
+ .then((decoded) => {
+ const decodedIdToken = decoded.payload as DecodedIdToken;
+ decodedIdToken.uid = decodedIdToken.sub;
+ return decodedIdToken;
+ });
+ }
+
+ /** @alpha */
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ public _verifyAuthBlockingToken(
+ jwtToken: string,
+ isEmulator: boolean,
+ audience: string | undefined): Promise {
+ if (!validator.isString(jwtToken)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ `First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`,
+ );
+ }
+
+ return this.ensureProjectId()
+ .then((projectId) => {
+ if (typeof audience === 'undefined') {
+ audience = `${projectId}.cloudfunctions.net/`;
+ }
+ return this.decodeAndVerify(jwtToken, projectId, isEmulator, audience);
+ })
+ .then((decoded) => {
+ const decodedAuthBlockingToken = decoded.payload as DecodedAuthBlockingToken;
+ decodedAuthBlockingToken.uid = decodedAuthBlockingToken.sub;
+ return decodedAuthBlockingToken;
+ });
+ }
+
+ private ensureProjectId(): Promise {
+ return util.findProjectId(this.app)
+ .then((projectId) => {
+ if (!validator.isNonEmptyString(projectId)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CREDENTIAL,
+ 'Must initialize app with a cert credential or set your Firebase project ID as the ' +
+ `GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`,
+ );
+ }
+ return Promise.resolve(projectId);
+ })
+ }
+
+ private decodeAndVerify(
+ token: string,
+ projectId: string,
+ isEmulator: boolean,
+ audience?: string): Promise {
+ return this.safeDecode(token)
+ .then((decodedToken) => {
+ this.verifyContent(decodedToken, projectId, isEmulator, audience);
+ return this.verifySignature(token, isEmulator)
+ .then(() => decodedToken);
+ });
+ }
+
+ private safeDecode(jwtToken: string): Promise {
+ return decodeJwt(jwtToken)
+ .catch((err: JwtError) => {
+ if (err.code === JwtErrorCode.INVALID_ARGUMENT) {
+ const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` +
+ `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`;
+ const errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed ` +
+ `the entire string JWT which represents ${this.shortNameArticle} ` +
+ `${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage;
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT,
+ errorMessage);
+ }
+ throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, err.message);
+ });
+ }
+
+ /**
+ * Verifies the content of a Firebase Auth JWT.
+ *
+ * @param fullDecodedToken - The decoded JWT.
+ * @param projectId - The Firebase Project Id.
+ * @param isEmulator - Whether the token is an Emulator token.
+ */
+ private verifyContent(
+ fullDecodedToken: DecodedToken,
+ projectId: string | null,
+ isEmulator: boolean,
+ audience: string | undefined): void {
+ const header = fullDecodedToken && fullDecodedToken.header;
+ const payload = fullDecodedToken && fullDecodedToken.payload;
+
+ const projectIdMatchMessage = ` Make sure the ${this.tokenInfo.shortName} comes from the same ` +
+ 'Firebase project as the service account used to authenticate this SDK.';
+ const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` +
+ `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`;
+
+ let errorMessage: string | undefined;
+ if (!isEmulator && typeof header.kid === 'undefined') {
+ const isCustomToken = (payload.aud === FIREBASE_AUDIENCE);
+ const isLegacyCustomToken = (header.alg === 'HS256' && payload.v === 0 && 'd' in payload && 'uid' in payload.d);
+
+ if (isCustomToken) {
+ errorMessage = `${this.tokenInfo.verifyApiName} expects ${this.shortNameArticle} ` +
+ `${this.tokenInfo.shortName}, but was given a custom token.`;
+ } else if (isLegacyCustomToken) {
+ errorMessage = `${this.tokenInfo.verifyApiName} expects ${this.shortNameArticle} ` +
+ `${this.tokenInfo.shortName}, but was given a legacy custom token.`;
+ } else {
+ errorMessage = `${this.tokenInfo.jwtName} has no "kid" claim.`;
+ }
+
+ errorMessage += verifyJwtTokenDocsMessage;
+ } else if (!isEmulator && header.alg !== ALGORITHM_RS256) {
+ errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + ALGORITHM_RS256 + '" but got ' +
+ '"' + header.alg + '".' + verifyJwtTokenDocsMessage;
+ } else if (typeof audience !== 'undefined' && !(payload.aud as string).includes(audience)) {
+ errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` +
+ audience + '" but got "' + payload.aud + '".' + verifyJwtTokenDocsMessage;
+ } else if (typeof audience === 'undefined' && payload.aud !== projectId) {
+ errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` +
+ projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage +
+ verifyJwtTokenDocsMessage;
+ } else if (payload.iss !== this.issuer + projectId) {
+ errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` +
+ `"${this.issuer}` + projectId + '" but got "' +
+ payload.iss + '".' + projectIdMatchMessage + verifyJwtTokenDocsMessage;
+ } else if (typeof payload.sub !== 'string') {
+ errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim.` + verifyJwtTokenDocsMessage;
+ } else if (payload.sub === '') {
+ errorMessage = `${this.tokenInfo.jwtName} has an empty string "sub" (subject) claim.` + verifyJwtTokenDocsMessage;
+ } else if (payload.sub.length > 128) {
+ errorMessage = `${this.tokenInfo.jwtName} has "sub" (subject) claim longer than 128 characters.` +
+ verifyJwtTokenDocsMessage;
+ }
+ if (errorMessage) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage);
+ }
+ }
+
+ private verifySignature(jwtToken: string, isEmulator: boolean):
+ Promise {
+ const verifier = isEmulator ? EMULATOR_VERIFIER : this.signatureVerifier;
+ return verifier.verify(jwtToken)
+ .catch((error) => {
+ throw this.mapJwtErrorToAuthError(error);
+ });
+ }
+
+ /**
+ * Maps JwtError to FirebaseAuthError
+ *
+ * @param error - JwtError to be mapped.
+ * @returns FirebaseAuthError or Error instance.
+ */
+ private mapJwtErrorToAuthError(error: JwtError): Error {
+ const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` +
+ `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`;
+ if (error.code === JwtErrorCode.TOKEN_EXPIRED) {
+ const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` +
+ ` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` +
+ verifyJwtTokenDocsMessage;
+ return new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage);
+ } else if (error.code === JwtErrorCode.INVALID_SIGNATURE) {
+ const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage;
+ return new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage);
+ } else if (error.code === JwtErrorCode.NO_MATCHING_KID) {
+ const errorMessage = `${this.tokenInfo.jwtName} has "kid" claim which does not ` +
+ `correspond to a known public key. Most likely the ${this.tokenInfo.shortName} ` +
+ 'is expired, so get a fresh token from your client app and try again.';
+ return new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage);
+ }
+ return new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message);
+ }
+}
+
+/**
+ * Creates a new FirebaseTokenVerifier to verify Firebase ID tokens.
+ *
+ * @internal
+ * @param app - Firebase app instance.
+ * @returns FirebaseTokenVerifier
+ */
+export function createIdTokenVerifier(app: App): FirebaseTokenVerifier {
+ return new FirebaseTokenVerifier(
+ CLIENT_CERT_URL,
+ 'https://securetoken.google.com/',
+ ID_TOKEN_INFO,
+ app
+ );
+}
+
+/**
+ * Creates a new FirebaseTokenVerifier to verify Firebase Auth Blocking tokens.
+ *
+ * @internal
+ * @param app - Firebase app instance.
+ * @returns FirebaseTokenVerifier
+ */
+export function createAuthBlockingTokenVerifier(app: App): FirebaseTokenVerifier {
+ return new FirebaseTokenVerifier(
+ CLIENT_CERT_URL,
+ 'https://securetoken.google.com/',
+ AUTH_BLOCKING_TOKEN_INFO,
+ app
+ );
+}
+
+/**
+ * Creates a new FirebaseTokenVerifier to verify Firebase session cookies.
+ *
+ * @internal
+ * @param app - Firebase app instance.
+ * @returns FirebaseTokenVerifier
+ */
+export function createSessionCookieVerifier(app: App): FirebaseTokenVerifier {
+ return new FirebaseTokenVerifier(
+ SESSION_COOKIE_CERT_URL,
+ 'https://session.firebase.google.com/',
+ SESSION_COOKIE_INFO,
+ app
+ );
+}
\ No newline at end of file
diff --git a/packages/dart_firebase_admin/lib/src/auth/update_request.dart b/packages/dart_firebase_admin/lib/src/auth/update_request.dart
deleted file mode 100644
index 1074d94..0000000
--- a/packages/dart_firebase_admin/lib/src/auth/update_request.dart
+++ /dev/null
@@ -1,25 +0,0 @@
-part of '../../dart_firebase_admin.dart';
-
-class UpdateRequest {
- UpdateRequest({
- this.disabled,
- this.displayName,
- this.email,
- this.emailVerified,
- this.password,
- this.photoURL,
- this.providersToUnlink,
- this.providerToLink,
- });
-
- final bool? disabled;
- final String? displayName;
- final String? email;
- final bool? emailVerified;
- // TODO multifactor
- final String? password;
- final String? photoURL;
- final List? providersToUnlink;
- // TODO UserProvider
- final Object? providerToLink;
-}
diff --git a/packages/dart_firebase_admin/lib/src/auth/user.dart b/packages/dart_firebase_admin/lib/src/auth/user.dart
new file mode 100644
index 0000000..320008f
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/user.dart
@@ -0,0 +1,417 @@
+import 'dart:convert';
+
+import 'package:collection/collection.dart';
+import 'package:firebaseapis/identitytoolkit/v1.dart' as firebase_auth_v1;
+import 'package:meta/meta.dart';
+
+import '../dart_firebase_admin.dart';
+import '../object_utils.dart';
+
+/// 'REDACTED', encoded as a base64 string.
+final b64Redacted = base64Encode('REDACTED'.codeUnits);
+
+enum MultiFactorId {
+ phone._('phone'),
+ totp._('totp');
+
+ const MultiFactorId._(this._value);
+
+ final String _value;
+}
+
+class UserRecord {
+ @internal
+ UserRecord({
+ required this.uid,
+ required this.email,
+ required this.emailVerified,
+ required this.displayName,
+ required this.photoUrl,
+ required this.phoneNumber,
+ required this.disabled,
+ required this.metadata,
+ required this.providerData,
+ required this.passwordHash,
+ required this.passwordSalt,
+ required this.customClaims,
+ required this.tenantId,
+ required this.tokensValidAfterTime,
+ required this.multiFactor,
+ });
+
+ @internal
+ factory UserRecord.fromResponse(
+ firebase_auth_v1.GoogleCloudIdentitytoolkitV1UserInfo response,
+ ) {
+ final localId = response.localId;
+ // The Firebase user id is required.
+ if (localId == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: Invalid user response',
+ );
+ }
+ // If disabled is not provided, the account is enabled by default.
+ final disabled = response.disabled ?? false;
+ final metadata = UserMetadata.fromResponse(response);
+
+ final providerData = [];
+ final providerUserInfo = response.providerUserInfo;
+ if (providerUserInfo != null) {
+ for (final entry in providerUserInfo) {
+ providerData.add(UserInfo.fromResponse(entry));
+ }
+ }
+
+ // If the password hash is redacted (probably due to missing permissions)
+ // then clear it out, similar to how the salt is returned. (Otherwise, it
+ // *looks* like a b64-encoded hash is present, which is confusing.)
+ final passwordHash =
+ response.passwordHash == b64Redacted ? null : response.passwordHash;
+
+ final customAttributes = response.customAttributes;
+ final customClaims = customAttributes != null
+ ? UnmodifiableMapView(
+ jsonDecode(customAttributes) as Map,
+ )
+ : null;
+
+ DateTime? tokensValidAfterTime;
+ final validSince = response.validSince;
+ if (validSince != null) {
+ // Convert validSince first to UTC milliseconds and then to UTC date string.
+ tokensValidAfterTime = DateTime.fromMillisecondsSinceEpoch(
+ // TODO double check that 1000
+ int.parse(validSince) * 1000,
+ isUtc: true,
+ );
+ }
+
+ MultiFactorSettings? multiFactor =
+ MultiFactorSettings.fromResponse(response);
+ if (multiFactor.enrolledFactors.isEmpty) {
+ multiFactor = null;
+ }
+
+ return UserRecord(
+ uid: localId,
+ email: response.email,
+ emailVerified: response.emailVerified ?? false,
+ displayName: response.displayName,
+ photoUrl: response.photoUrl,
+ phoneNumber: response.phoneNumber,
+ disabled: disabled,
+ metadata: metadata,
+ providerData: UnmodifiableListView(providerData),
+ passwordHash: passwordHash,
+ passwordSalt: response.salt,
+ customClaims: customClaims,
+ tenantId: response.tenantId,
+ tokensValidAfterTime: tokensValidAfterTime,
+ multiFactor: multiFactor,
+ );
+ }
+
+ /// The user's `uid`.
+ final String uid;
+
+ /// The user's primary email, if set.
+ final String? email;
+
+ /// Whether or not the user's primary email is verified.
+ final bool emailVerified;
+
+ /// The user's display name.
+ final String? displayName;
+
+ /// The user's photo URL.
+ final String? photoUrl;
+
+ /// The user's primary phone number, if set.
+ final String? phoneNumber;
+
+ /// Whether or not the user is disabled: `true` for disabled; `false` for
+ /// enabled.
+ final bool disabled;
+
+ /// Additional metadata about the user.
+ final UserMetadata metadata;
+
+ /// An array of providers (for example, Google, Facebook) linked to the user.
+ final List providerData;
+
+ /// The user's hashed password (base64-encoded), only if Firebase Auth hashing
+ /// algorithm (SCRYPT) is used. If a different hashing algorithm had been used
+ /// when uploading this user, as is typical when migrating from another Auth
+ /// system, this will be an empty string. If no password is set, this is
+ /// null. This is only available when the user is obtained from
+ /// {@link BaseAuth.listUsers}.
+ final String? passwordHash;
+
+ /// The user's password salt (base64-encoded), only if Firebase Auth hashing
+ /// algorithm (SCRYPT) is used. If a different hashing algorithm had been used to
+ /// upload this user, typical when migrating from another Auth system, this will
+ /// be an empty string. If no password is set, this is null. This is only
+ /// available when the user is obtained from {@link BaseAuth.listUsers}.
+ final String? passwordSalt;
+
+ /// The user's custom claims object if available, typically used to define
+ /// user roles and propagated to an authenticated user's ID token.
+ /// This is set via {@link BaseAuth.setCustomUserClaims}
+ final Map? customClaims;
+
+ /// The ID of the tenant the user belongs to, if available.
+ final String? tenantId;
+
+ /// The date the user's tokens are valid after, formatted as a UTC string.
+ /// This is updated every time the user's refresh token are revoked either
+// TODO review {@link}
+ /// from the {@link BaseAuth.revokeRefreshTokens}
+ /// API or from the Firebase Auth backend on big account changes (password
+ /// resets, password or email updates, etc).
+ final DateTime? tokensValidAfterTime;
+
+ /// The multi-factor related properties for the current user, if available.
+ final MultiFactorSettings? multiFactor;
+
+ /// Returns a JSON-serializable representation of this object.
+ ///
+ /// @returns A JSON-serializable representation of this object.
+ Map toJson() {
+ final providerDataJson = [];
+ final json = {
+ 'uid': uid,
+ 'email': email,
+ 'emailVerified': emailVerified,
+ 'displayName': displayName,
+ 'photoURL': photoUrl,
+ 'phoneNumber': phoneNumber,
+ 'disabled': disabled,
+ // Convert metadata to json.
+ 'metadata': metadata.toJson(),
+ 'passwordHash': passwordHash,
+ 'passwordSalt': passwordSalt,
+ 'customClaims': customClaims,
+ 'tokensValidAfterTime': tokensValidAfterTime,
+ 'tenantId': tenantId,
+ 'providerData': providerDataJson,
+ };
+
+ final multiFactor = this.multiFactor;
+ if (multiFactor != null) json['multiFactor'] = multiFactor.toJson();
+
+ json['providerData'] = [];
+ for (final entry in providerData) {
+ // Convert each provider data to json.
+ providerDataJson.add(entry.toJson());
+ }
+ return json;
+ }
+}
+
+class UserInfo {
+ UserInfo({
+ required this.uid,
+ required this.displayName,
+ required this.email,
+ required this.photoUrl,
+ required this.providerId,
+ required this.phoneNumber,
+ });
+
+ UserInfo.fromResponse(
+ firebase_auth_v1.GoogleCloudIdentitytoolkitV1ProviderUserInfo response,
+ ) : uid = response.rawId,
+ displayName = response.displayName,
+ email = response.email,
+ photoUrl = response.photoUrl,
+ providerId = response.providerId,
+ phoneNumber = response.phoneNumber {
+ if (response.rawId == null || response.providerId == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ );
+ }
+ }
+
+ final String? uid;
+ final String? displayName;
+ final String? email;
+ final String? photoUrl;
+ final String? providerId;
+ final String? phoneNumber;
+
+ Map toJson() {
+ return {
+ 'uid': uid,
+ 'displayName': displayName,
+ 'email': email,
+ 'photoURL': photoUrl,
+ 'providerId': providerId,
+ 'phoneNumber': phoneNumber,
+ };
+ }
+}
+
+class MultiFactorSettings {
+ MultiFactorSettings({required this.enrolledFactors});
+
+ factory MultiFactorSettings.fromResponse(
+ firebase_auth_v1.GoogleCloudIdentitytoolkitV1UserInfo response,
+ ) {
+ final parsedEnrolledFactors = [
+ ...?response.mfaInfo
+ ?.map(MultiFactorInfo.initMultiFactorInfo)
+ .whereNotNull(),
+ ];
+
+ return MultiFactorSettings(
+ enrolledFactors: UnmodifiableListView(parsedEnrolledFactors),
+ );
+ }
+
+ final List enrolledFactors;
+
+ Map toJson() {
+ return {
+ 'enrolledFactors': enrolledFactors.map((info) => info.toJson()).toList(),
+ };
+ }
+}
+
+/// Interface representing the common properties of a user-enrolled second factor.
+abstract class MultiFactorInfo {
+ MultiFactorInfo({
+ required this.uid,
+ required this.displayName,
+ required this.enrollmentTime,
+ });
+
+ MultiFactorInfo.fromResponse(
+ firebase_auth_v1.GoogleCloudIdentitytoolkitV1MfaEnrollment response,
+ ) : uid = response.mfaEnrollmentId.orThrow(
+ () => throw FirebaseAuthAdminException(
+ AuthClientErrorCode.internalError,
+ 'INTERNAL ASSERT FAILED: No uid found for MFA info.',
+ ),
+ ),
+ displayName = response.displayName,
+ enrollmentTime = response.enrolledAt
+ .let(int.parse)
+ .let(DateTime.fromMillisecondsSinceEpoch);
+
+ /// Initializes the MultiFactorInfo associated subclass using the server side.
+ /// If no MultiFactorInfo is associated with the response, null is returned.
+ ///
+ /// @param response - The server side response.
+ /// @internal
+ static MultiFactorInfo? initMultiFactorInfo(
+ firebase_auth_v1.GoogleCloudIdentitytoolkitV1MfaEnrollment response,
+ ) {
+ // PhoneMultiFactorInfo, TotpMultiFactorInfo currently available.
+ try {
+ final phoneInfo = response.phoneInfo;
+ // TODO Support TotpMultiFactorInfo
+ // final totpInfo = response.totpInfo;
+
+ if (phoneInfo != null) {
+ return PhoneMultiFactorInfo.fromResponse(response);
+ } /* else if (totpInfo != null) {
+ return TotpMultiFactorInfo(response);
+ }*/
+
+ // Ignore the other SDK unsupported MFA factors to prevent blocking developers using the current SDK.
+ } catch (e) {
+ // Ignore error.
+ }
+
+ return null;
+ }
+
+ /// The ID of the enrolled second factor. This ID is unique to the user.
+ final String uid;
+
+ /// The optional display name of the enrolled second factor.
+ final String? displayName;
+
+ /// The type identifier of the second factor.
+ /// For SMS second factors, this is `phone`.
+ /// For TOTP second factors, this is `totp`.
+ MultiFactorId get factorId;
+
+ /// The optional date the second factor was enrolled, formatted as a UTC string.
+ final DateTime? enrollmentTime;
+
+ /// Returns a JSON-serializable representation of this object.
+ ///
+ /// @returns A JSON-serializable representation of this object.
+ Map toJson() {
+ return {
+ 'uid': uid,
+ 'displayName': displayName,
+ 'factorId': factorId._value,
+ 'enrollmentTime': enrollmentTime,
+ };
+ }
+}
+
+/// Interface representing a phone specific user-enrolled second factor.
+class PhoneMultiFactorInfo extends MultiFactorInfo {
+ /// Initializes the PhoneMultiFactorInfo object using the server side response.
+ @internal
+ PhoneMultiFactorInfo.fromResponse(
+ firebase_auth_v1.GoogleCloudIdentitytoolkitV1MfaEnrollment response,
+ ) : phoneNumber = response.phoneInfo,
+ factorId = response.phoneInfo != null ? MultiFactorId.phone : throw 42,
+ super.fromResponse(response);
+
+ /// The phone number associated with a phone second factor.
+ final String? phoneNumber;
+
+ @override
+ final MultiFactorId factorId;
+
+ /// {@inheritdoc MultiFactorInfo.toJSON}
+ @override
+ Map toJson() {
+ return {
+ ...super.toJson(),
+ 'phoneNumber': phoneNumber,
+ };
+ }
+}
+
+class UserMetadata {
+ UserMetadata({
+ required this.creationTime,
+ required this.lastSignInTime,
+ required this.lastRefreshTime,
+ });
+
+ @internal
+ UserMetadata.fromResponse(
+ firebase_auth_v1.GoogleCloudIdentitytoolkitV1UserInfo response,
+ ) : creationTime = DateTime.fromMillisecondsSinceEpoch(
+ int.parse(response.createdAt!),
+ ),
+ lastSignInTime = DateTime.fromMillisecondsSinceEpoch(
+ int.parse(response.lastLoginAt!),
+ ),
+ lastRefreshTime = response.lastRefreshAt == null
+ ? null
+ : DateTime.fromMillisecondsSinceEpoch(
+ int.parse(response.lastRefreshAt!),
+ );
+
+ final DateTime creationTime;
+ final DateTime lastSignInTime;
+ final DateTime? lastRefreshTime;
+
+ Map toJson() {
+ return {
+ 'creationTime': creationTime.toUtc().toString(),
+ 'lastSignInTime': lastSignInTime.toUtc().toString(),
+ 'lastRefreshTime': lastRefreshTime?.toUtc().toString(),
+ };
+ }
+}
diff --git a/packages/dart_firebase_admin/lib/src/auth/user_import_builder.dart b/packages/dart_firebase_admin/lib/src/auth/user_import_builder.dart
new file mode 100644
index 0000000..47a02f1
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/user_import_builder.dart
@@ -0,0 +1,578 @@
+// TODO remove redundant "required"
+
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:collection/collection.dart';
+import 'package:firebaseapis/identitytoolkit/v1.dart' as v1;
+import 'package:meta/meta.dart';
+
+import '../dart_firebase_admin.dart';
+import '../app/core.dart';
+import '../object_utils.dart';
+import 'auth_config.dart';
+import 'base_auth.dart';
+
+enum HashAlgorithmType {
+ scrypt('SCRYPT'),
+ standardScrypt('STANDARD_SCRYPT'),
+ hmacSha512('HMAC_SHA512'),
+ hmacSha256('HMAC_SHA256'),
+ hmacSha1('HMAC_SHA1'),
+ hmacMd5('HMAC_MD5'),
+ md5('MD5'),
+ pbkdfSha1('PBKDF_SHA1'),
+ bcrypt('BCRYPT'),
+ pbkdf2Sha256('PBKDF2_SHA256'),
+ sha512('SHA512'),
+ sha256('SHA256'),
+ sha1('SHA1');
+
+ const HashAlgorithmType(this.value);
+
+ final String value;
+}
+
+class UserImportHashOptions {
+ UserImportHashOptions({
+ required this.algorithm,
+ required this.key,
+ required this.saltSeparator,
+ required this.rounds,
+ required this.memoryCost,
+ required this.parallelization,
+ required this.blockSize,
+ required this.derivedKeyLength,
+ });
+
+ /// The password hashing algorithm identifier. The following algorithm
+ /// identifiers are supported:
+ /// `SCRYPT`, `STANDARD_SCRYPT`, `HMAC_SHA512`, `HMAC_SHA256`, `HMAC_SHA1`,
+ /// `HMAC_MD5`, `MD5`, `PBKDF_SHA1`, `BCRYPT`, `PBKDF2_SHA256`, `SHA512`,
+ /// `SHA256` and `SHA1`.
+ final HashAlgorithmType algorithm;
+
+ /// The signing key used in the hash algorithm in buffer bytes.
+ /// Required by hashing algorithms `SCRYPT`, `HMAC_SHA512`, `HMAC_SHA256`,
+ /// `HAMC_SHA1` and `HMAC_MD5`.
+ final Uint8List? key;
+
+ /// The salt separator in buffer bytes which is appended to salt when
+ /// verifying a password. This is only used by the `SCRYPT` algorithm.
+ final Uint8List? saltSeparator;
+
+ /// The number of rounds for hashing calculation.
+ /// Required for `SCRYPT`, `MD5`, `SHA512`, `SHA256`, `SHA1`, `PBKDF_SHA1` and
+ /// `PBKDF2_SHA256`.
+ final int? rounds;
+
+ /// The memory cost required for `SCRYPT` algorithm, or the CPU/memory cost.
+ /// Required for `STANDARD_SCRYPT` algorithm.
+ final int? memoryCost;
+
+ /// The parallelization of the hashing algorithm. Required for the
+ /// `STANDARD_SCRYPT` algorithm.
+ final int? parallelization;
+
+ /// The block size (normally 8) of the hashing algorithm. Required for the
+ /// `STANDARD_SCRYPT` algorithm.
+ final int? blockSize;
+
+ /// The derived key length of the hashing algorithm. Required for the
+ /// `STANDARD_SCRYPT` algorithm.
+ final int? derivedKeyLength;
+}
+
+/// Interface representing the user import options needed for
+/// {@link BaseAuth.importUsers} method. This is used to
+/// provide the password hashing algorithm information.
+class UserImportOptions {
+ UserImportOptions({required this.hash});
+
+ /// The password hashing information.
+ final UserImportHashOptions hash;
+}
+
+class UploadAccountOptions {
+ UploadAccountOptions._({
+ this.hashAlgorithm,
+ this.signerKey,
+ this.rounds,
+ this.memoryCost,
+ this.saltSeparator,
+ this.cpuMemCost,
+ this.parallelization,
+ this.blockSize,
+ this.dkLen,
+ });
+
+ final HashAlgorithmType? hashAlgorithm;
+ final String? signerKey;
+ final int? rounds;
+ final int? memoryCost;
+ final String? saltSeparator;
+ final int? cpuMemCost;
+ final int? parallelization;
+ final int? blockSize;
+ final int? dkLen;
+}
+
+/// User provider data to include when importing a user.
+class UserProviderRequest {
+ UserProviderRequest({
+ required this.uid,
+ required this.displayName,
+ required this.email,
+ required this.phoneNumber,
+ required this.photoURL,
+ required this.providerId,
+ });
+
+ /// The user identifier for the linked provider.
+ final String uid;
+
+ /// The display name for the linked provider.
+ final String? displayName;
+
+ /// The email for the linked provider.
+ final String? email;
+
+ /// The phone number for the linked provider.
+ final String? phoneNumber;
+
+ /// The photo URL for the linked provider.
+ final String? photoURL;
+
+ /// The linked provider ID (for example, "google.com" for the Google provider).
+ final String providerId;
+}
+
+/// Interface representing a user to import to Firebase Auth via the
+/// {@link BaseAuth.importUsers} method.
+class UserImportRecord {
+ UserImportRecord({
+ required this.uid,
+ required this.email,
+ required this.emailVerified,
+ required this.displayName,
+ required this.phoneNumber,
+ required this.photoURL,
+ required this.disabled,
+ required this.metadata,
+ required this.providerData,
+ required this.customClaims,
+ required this.passwordHash,
+ required this.passwordSalt,
+ required this.tenantId,
+ required this.multiFactor,
+ });
+
+ /// The user's `uid`.
+ final String uid;
+
+ /// The user's primary email, if set.
+ final String? email;
+
+ /// Whether or not the user's primary email is verified.
+ final bool? emailVerified;
+
+ /// The user's display name.
+ final String? displayName;
+
+ /// The user's primary phone number, if set.
+ final String? phoneNumber;
+
+ /// The user's photo URL.
+ final String? photoURL;
+
+ /// Whether or not the user is disabled: `true` for disabled; `false` for
+ /// enabled.
+ final bool? disabled;
+
+ /// Additional metadata about the user.
+ final UserMetadataRequest? metadata;
+
+ /// An array of providers (for example, Google, Facebook) linked to the user.
+ final List? providerData;
+
+ /// The user's custom claims object if available, typically used to define
+ /// user roles and propagated to an authenticated user's ID token.
+ final Map? customClaims;
+
+ /// The buffer of bytes representing the user's hashed password.
+ /// When a user is to be imported with a password hash,
+ /// {@link UserImportOptions} are required to be
+ /// specified to identify the hashing algorithm used to generate this hash.
+ final Uint8List? passwordHash;
+
+ /// The buffer of bytes representing the user's password salt.
+ final Uint8List? passwordSalt;
+
+ /// The identifier of the tenant where user is to be imported to.
+ /// When not provided in an `admin.auth.Auth` context, the user is uploaded to
+ /// the default parent project.
+ /// When not provided in an `admin.auth.TenantAwareAuth` context, the user is uploaded
+ /// to the tenant corresponding to that `TenantAwareAuth` instance's tenant ID.
+ final String? tenantId;
+
+ /// The user's multi-factor related properties.
+ final MultiFactorUpdateSettings? multiFactor;
+}
+
+/// Interface representing an Auth second factor in Auth server format.
+class AuthFactorInfo {
+ AuthFactorInfo({
+ required this.mfaEnrollmentId,
+ required this.displayName,
+ required this.phoneInfo,
+ required this.enrolledAt,
+ });
+
+ // TODO allow any key
+
+ // Not required for signupNewUser endpoint.
+ final String? mfaEnrollmentId;
+ final String? displayName;
+ final String? phoneInfo;
+ final String? enrolledAt;
+}
+
+class UploadProviderUserInfo {
+ UploadProviderUserInfo({
+ required this.rawId,
+ required this.providerId,
+ required this.email,
+ required this.displayName,
+ required this.photoUrl,
+ });
+
+ final String rawId;
+ final String providerId;
+ final String? email;
+ final String? displayName;
+ final String? photoUrl;
+}
+
+/// Callback function to validate an UploadAccountUser object.
+typedef ValidatorFunction = void Function(
+ v1.GoogleCloudIdentitytoolkitV1UserInfo data,
+);
+
+/// User metadata to include when importing a user.
+class UserMetadataRequest {
+ UserMetadataRequest({
+ required this.lastSignInTime,
+ required this.creationTime,
+ });
+
+ /// The date the user last signed in, formatted as a UTC string.
+ final DateTime? lastSignInTime;
+
+ /// The date the user was created, formatted as a UTC string.
+ final DateTime? creationTime;
+}
+
+@internal
+class UserImportBuilder {
+ UserImportBuilder({
+ required this.users,
+ required this.options,
+ required this.userRequestValidator,
+ }) {
+ _validatedUsers = _populateUsers(users, userRequestValidator);
+ _validatedOptions = _populateOptions(
+ options,
+ requiresHashOptions: _requiresHashOptions,
+ );
+ }
+
+ final List users;
+ final UserImportOptions? options;
+ final ValidatorFunction? userRequestValidator;
+
+ var _requiresHashOptions = false;
+ var _validatedUsers = [];
+ UploadAccountOptions? _validatedOptions;
+ final _indexMap = {};
+ final _userImportResultErrors = [];
+
+ v1.GoogleCloudIdentitytoolkitV1UploadAccountRequest buildRequest() {
+ return v1.GoogleCloudIdentitytoolkitV1UploadAccountRequest(
+ hashAlgorithm: _validatedOptions?.hashAlgorithm?.value,
+ signerKey: _validatedOptions?.signerKey,
+ rounds: _validatedOptions?.rounds,
+ memoryCost: _validatedOptions?.memoryCost,
+ saltSeparator: _validatedOptions?.saltSeparator,
+ cpuMemCost: _validatedOptions?.cpuMemCost,
+ parallelization: _validatedOptions?.parallelization,
+ blockSize: _validatedOptions?.blockSize,
+ dkLen: _validatedOptions?.dkLen,
+ users: _validatedUsers.toList(),
+ );
+ }
+
+ UserImportResult buildResponse(
+ List failedUploads,
+ ) {
+ // Initialize user import result.
+ final importResult = UserImportResult(
+ successCount: _validatedUsers.length - failedUploads.length,
+ failureCount: _userImportResultErrors.length + failedUploads.length,
+ errors: [
+ ..._userImportResultErrors,
+ for (final failedUpload in failedUploads)
+ FirebaseArrayIndexError(
+ // Map backend request index to original developer provided array index.
+ index: _indexMap[failedUpload.index]!,
+ error: FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidUserImport,
+ failedUpload.message,
+ ),
+ ),
+ ],
+ );
+ // Sort errors by index.
+ importResult.errors.sort((a, b) => a.index - b.index);
+ // Return sorted result
+ return importResult;
+ }
+
+ UploadAccountOptions _populateOptions(
+ UserImportOptions? options, {
+ required bool requiresHashOptions,
+ }) {
+ if (!requiresHashOptions) return UploadAccountOptions._();
+
+ if (options == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidArgument,
+ '"UserImportOptions" are required when importing users with passwords.',
+ );
+ }
+
+ switch (options.hash.algorithm) {
+ case HashAlgorithmType.hmacSha512:
+ case HashAlgorithmType.hmacSha256:
+ case HashAlgorithmType.hmacSha1:
+ case HashAlgorithmType.hmacMd5:
+ final key = options.hash.key;
+ if (key == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidHashKey,
+ 'A non-empty "hash.key" byte buffer must be provided for hash '
+ 'algorithm ${options.hash.algorithm}.',
+ );
+ }
+
+ return UploadAccountOptions._(
+ hashAlgorithm: options.hash.algorithm,
+ signerKey: base64Encode(key),
+ );
+
+ case HashAlgorithmType.md5:
+ case HashAlgorithmType.sha1:
+ case HashAlgorithmType.sha256:
+ case HashAlgorithmType.sha512:
+ // MD5 is [0,8192] but SHA1, SHA256, and SHA512 are [1,8192]
+ final rounds = options.hash.rounds;
+ final minRounds =
+ options.hash.algorithm == HashAlgorithmType.md5 ? 0 : 1;
+ if (rounds == null || rounds < minRounds || rounds > 8192) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidHashRounds,
+ 'A "hash.rounds" value between $minRounds and 8192 must be provided '
+ 'for hash algorithm ${options.hash.algorithm}.',
+ );
+ }
+
+ return UploadAccountOptions._(
+ hashAlgorithm: options.hash.algorithm,
+ rounds: rounds,
+ );
+
+ case HashAlgorithmType.pbkdfSha1:
+ case HashAlgorithmType.pbkdf2Sha256:
+ final rounds = options.hash.rounds;
+ if (rounds == null || rounds < 0 || rounds > 120000) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidHashRounds,
+ 'A "hash.rounds" value between 0 and 120000 must be provided '
+ 'for hash algorithm ${options.hash.algorithm}.',
+ );
+ }
+
+ return UploadAccountOptions._(
+ hashAlgorithm: options.hash.algorithm,
+ rounds: rounds,
+ );
+
+ case HashAlgorithmType.scrypt:
+ final key = options.hash.key;
+ if (key == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidHashKey,
+ 'A "hash.key" byte buffer must be provided for '
+ 'hash algorithm ${options.hash.algorithm}.',
+ );
+ }
+ final rounds = options.hash.rounds;
+ if (rounds == null || rounds <= 0 || rounds > 8) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidHashRounds,
+ 'A valid "hash.rounds" number between 1 and 8 must be provided for '
+ 'hash algorithm ${options.hash.algorithm}.',
+ );
+ }
+ final memoryCost = options.hash.memoryCost;
+ if (memoryCost == null || memoryCost <= 0 || memoryCost > 14) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidHashMemoryCost,
+ 'A valid "hash.memoryCost" number between 1 and 14 must be provided '
+ 'for hash algorithm ${options.hash.algorithm}.',
+ );
+ }
+ final saltSeparator = options.hash.saltSeparator;
+
+ return UploadAccountOptions._(
+ hashAlgorithm: options.hash.algorithm,
+ signerKey: base64Encode(key),
+ rounds: rounds,
+ memoryCost: memoryCost,
+ saltSeparator: base64Encode(saltSeparator ?? Uint8List(0)),
+ );
+
+ case HashAlgorithmType.bcrypt:
+ return UploadAccountOptions._(
+ hashAlgorithm: options.hash.algorithm,
+ );
+
+ case HashAlgorithmType.standardScrypt:
+ final cpuMemCost = options.hash.memoryCost;
+ if (cpuMemCost == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidHashMemoryCost,
+ 'A valid "hash.memoryCost" number must be provided for '
+ 'hash algorithm ${options.hash.algorithm}.',
+ );
+ }
+ final parallelization = options.hash.parallelization;
+ if (parallelization == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidHashParallelization,
+ 'A valid "hash.parallelization" number must be provided for '
+ 'hash algorithm ${options.hash.algorithm}.',
+ );
+ }
+ final blockSize = options.hash.blockSize;
+ if (blockSize == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidHashBlockSize,
+ 'A valid "hash.blockSize" number must be provided for '
+ 'hash algorithm ${options.hash.algorithm}.',
+ );
+ }
+ final dkLen = options.hash.derivedKeyLength;
+ if (dkLen == null) {
+ throw FirebaseAuthAdminException(
+ AuthClientErrorCode.invalidHashDerivedKeyLength,
+ 'A valid "hash.derivedKeyLength" number must be provided for '
+ 'hash algorithm ${options.hash.algorithm}.',
+ );
+ }
+
+ return UploadAccountOptions._(
+ hashAlgorithm: options.hash.algorithm,
+ memoryCost: cpuMemCost,
+ parallelization: parallelization,
+ blockSize: blockSize,
+ dkLen: dkLen,
+ );
+ }
+ }
+
+ /// Validates and returns the users list of the uploadAccount request.
+ /// Whenever a user with an error is detected, the error is cached and will later be
+ /// merged into the user import result. This allows the processing of valid users without
+ /// failing early on the first error detected.
+ /// @param {UserImportRecord[]} users The UserImportRecords to convert to UnploadAccountUser
+ /// objects.
+ /// @param {ValidatorFunction=} userValidator The user validator function.
+ /// @returns {UploadAccountUser[]} The populated uploadAccount users.
+ List _populateUsers(
+ List users,
+ ValidatorFunction? userValidator,
+ ) {
+ final populatedUsers = [];
+ users.forEachIndexed((index, user) {
+ try {
+ final result = _populateUploadAccountUser(user, userValidator);
+ if (result.passwordHash != null) {
+ _requiresHashOptions = true;
+ }
+
+ // Only users that pass client screening will be passed to backend for processing.
+ populatedUsers.add(result);
+ // Map user's index (the one to be sent to backend) to original developer provided array.
+ _indexMap[populatedUsers.length - 1] = index;
+ } on FirebaseException catch (err) {
+ _userImportResultErrors.add(
+ FirebaseArrayIndexError(index: index, error: err),
+ );
+ }
+ });
+
+ return populatedUsers;
+ }
+}
+
+/// Converts a UserImportRecord to a UploadAccountUser object. Throws an error when invalid
+/// fields are provided.
+/// @param {UserImportRecord} user The UserImportRecord to conver to UploadAccountUser.
+/// @param {ValidatorFunction=} userValidator The user validator function.
+/// @returns {UploadAccountUser} The corresponding UploadAccountUser to return.
+v1.GoogleCloudIdentitytoolkitV1UserInfo _populateUploadAccountUser(
+ UserImportRecord user,
+ ValidatorFunction? userValidator,
+) {
+ final mfaInfo = user.multiFactor?.enrolledFactors
+ ?.map(
+ (factor) => factor.toMfaEnrollment(),
+ )
+ .toList();
+
+ final providerUserInfo = user.providerData
+ ?.map(
+ (providerData) => v1.GoogleCloudIdentitytoolkitV1ProviderUserInfo(
+ rawId: providerData.uid,
+ providerId: providerData.providerId,
+ email: providerData.email,
+ displayName: providerData.displayName,
+ photoUrl: providerData.photoURL,
+ ),
+ )
+ .toList();
+
+ final result = v1.GoogleCloudIdentitytoolkitV1UserInfo(
+ localId: user.uid,
+ email: user.email,
+ emailVerified: user.emailVerified,
+ displayName: user.displayName,
+ disabled: user.disabled,
+ photoUrl: user.photoURL,
+ phoneNumber: user.phoneNumber,
+ providerUserInfo: providerUserInfo != null && providerUserInfo.isNotEmpty
+ ? providerUserInfo
+ : null,
+ mfaInfo: mfaInfo != null && mfaInfo.isNotEmpty ? mfaInfo : null,
+ tenantId: user.tenantId,
+ customAttributes: user.customClaims.let(json.encode),
+ passwordHash: user.passwordHash.let(base64Encode),
+ salt: user.passwordSalt.let(base64Encode),
+ createdAt: user.metadata?.creationTime?.toIso8601String(),
+ lastLoginAt: user.metadata?.lastSignInTime?.toIso8601String(),
+ );
+
+ userValidator?.call(result);
+
+ return result;
+}
diff --git a/packages/dart_firebase_admin/lib/src/auth/user_import_builder.ts b/packages/dart_firebase_admin/lib/src/auth/user_import_builder.ts
new file mode 100644
index 0000000..12afb37
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/auth/user_import_builder.ts
@@ -0,0 +1,786 @@
+/*!
+ * Copyright 2018 Google Inc.
+ *
+ * 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.
+ */
+
+import { FirebaseArrayIndexError } from "../app/index";
+import { deepCopy, deepExtend } from "../utils/deep-copy";
+import * as utils from "../utils";
+import * as validator from "../utils/validator";
+import { AuthClientErrorCode, FirebaseAuthError } from "../utils/error";
+import {
+ UpdateMultiFactorInfoRequest,
+ UpdatePhoneMultiFactorInfoRequest,
+ MultiFactorUpdateSettings,
+} from "./auth-config";
+
+export type HashAlgorithmType =
+ | "SCRYPT"
+ | "STANDARD_SCRYPT"
+ | "HMAC_SHA512"
+ | "HMAC_SHA256"
+ | "HMAC_SHA1"
+ | "HMAC_MD5"
+ | "MD5"
+ | "PBKDF_SHA1"
+ | "BCRYPT"
+ | "PBKDF2_SHA256"
+ | "SHA512"
+ | "SHA256"
+ | "SHA1";
+
+/**
+ * Interface representing the user import options needed for
+ * {@link BaseAuth.importUsers} method. This is used to
+ * provide the password hashing algorithm information.
+ */
+export interface UserImportOptions {
+ /**
+ * The password hashing information.
+ */
+ hash: {
+ /**
+ * The password hashing algorithm identifier. The following algorithm
+ * identifiers are supported:
+ * `SCRYPT`, `STANDARD_SCRYPT`, `HMAC_SHA512`, `HMAC_SHA256`, `HMAC_SHA1`,
+ * `HMAC_MD5`, `MD5`, `PBKDF_SHA1`, `BCRYPT`, `PBKDF2_SHA256`, `SHA512`,
+ * `SHA256` and `SHA1`.
+ */
+ algorithm: HashAlgorithmType;
+
+ /**
+ * The signing key used in the hash algorithm in buffer bytes.
+ * Required by hashing algorithms `SCRYPT`, `HMAC_SHA512`, `HMAC_SHA256`,
+ * `HAMC_SHA1` and `HMAC_MD5`.
+ */
+ key?: Buffer;
+
+ /**
+ * The salt separator in buffer bytes which is appended to salt when
+ * verifying a password. This is only used by the `SCRYPT` algorithm.
+ */
+ saltSeparator?: Buffer;
+
+ /**
+ * The number of rounds for hashing calculation.
+ * Required for `SCRYPT`, `MD5`, `SHA512`, `SHA256`, `SHA1`, `PBKDF_SHA1` and
+ * `PBKDF2_SHA256`.
+ */
+ rounds?: number;
+
+ /**
+ * The memory cost required for `SCRYPT` algorithm, or the CPU/memory cost.
+ * Required for `STANDARD_SCRYPT` algorithm.
+ */
+ memoryCost?: number;
+
+ /**
+ * The parallelization of the hashing algorithm. Required for the
+ * `STANDARD_SCRYPT` algorithm.
+ */
+ parallelization?: number;
+
+ /**
+ * The block size (normally 8) of the hashing algorithm. Required for the
+ * `STANDARD_SCRYPT` algorithm.
+ */
+ blockSize?: number;
+ /**
+ * The derived key length of the hashing algorithm. Required for the
+ * `STANDARD_SCRYPT` algorithm.
+ */
+ derivedKeyLength?: number;
+ };
+}
+
+/**
+ * Interface representing a user to import to Firebase Auth via the
+ * {@link BaseAuth.importUsers} method.
+ */
+export interface UserImportRecord {
+ /**
+ * The user's `uid`.
+ */
+ uid: string;
+
+ /**
+ * The user's primary email, if set.
+ */
+ email?: string;
+
+ /**
+ * Whether or not the user's primary email is verified.
+ */
+ emailVerified?: boolean;
+
+ /**
+ * The user's display name.
+ */
+ displayName?: string;
+
+ /**
+ * The user's primary phone number, if set.
+ */
+ phoneNumber?: string;
+
+ /**
+ * The user's photo URL.
+ */
+ photoURL?: string;
+
+ /**
+ * Whether or not the user is disabled: `true` for disabled; `false` for
+ * enabled.
+ */
+ disabled?: boolean;
+
+ /**
+ * Additional metadata about the user.
+ */
+ metadata?: UserMetadataRequest;
+
+ /**
+ * An array of providers (for example, Google, Facebook) linked to the user.
+ */
+ providerData?: UserProviderRequest[];
+
+ /**
+ * The user's custom claims object if available, typically used to define
+ * user roles and propagated to an authenticated user's ID token.
+ */
+ customClaims?: { [key: string]: any };
+
+ /**
+ * The buffer of bytes representing the user's hashed password.
+ * When a user is to be imported with a password hash,
+ * {@link UserImportOptions} are required to be
+ * specified to identify the hashing algorithm used to generate this hash.
+ */
+ passwordHash?: Buffer;
+
+ /**
+ * The buffer of bytes representing the user's password salt.
+ */
+ passwordSalt?: Buffer;
+
+ /**
+ * The identifier of the tenant where user is to be imported to.
+ * When not provided in an `admin.auth.Auth` context, the user is uploaded to
+ * the default parent project.
+ * When not provided in an `admin.auth.TenantAwareAuth` context, the user is uploaded
+ * to the tenant corresponding to that `TenantAwareAuth` instance's tenant ID.
+ */
+ tenantId?: string;
+
+ /**
+ * The user's multi-factor related properties.
+ */
+ multiFactor?: MultiFactorUpdateSettings;
+}
+
+/**
+ * User metadata to include when importing a user.
+ */
+export interface UserMetadataRequest {
+ /**
+ * The date the user last signed in, formatted as a UTC string.
+ */
+ lastSignInTime?: string;
+
+ /**
+ * The date the user was created, formatted as a UTC string.
+ */
+ creationTime?: string;
+}
+
+/**
+ * User provider data to include when importing a user.
+ */
+export interface UserProviderRequest {
+ /**
+ * The user identifier for the linked provider.
+ */
+ uid: string;
+
+ /**
+ * The display name for the linked provider.
+ */
+ displayName?: string;
+
+ /**
+ * The email for the linked provider.
+ */
+ email?: string;
+
+ /**
+ * The phone number for the linked provider.
+ */
+ phoneNumber?: string;
+
+ /**
+ * The photo URL for the linked provider.
+ */
+ photoURL?: string;
+
+ /**
+ * The linked provider ID (for example, "google.com" for the Google provider).
+ */
+ providerId: string;
+}
+
+/**
+ * Interface representing the response from the
+ * {@link BaseAuth.importUsers} method for batch
+ * importing users to Firebase Auth.
+ */
+export interface UserImportResult {
+ /**
+ * The number of user records that failed to import to Firebase Auth.
+ */
+ failureCount: number;
+
+ /**
+ * The number of user records that successfully imported to Firebase Auth.
+ */
+ successCount: number;
+
+ /**
+ * An array of errors corresponding to the provided users to import. The
+ * length of this array is equal to [`failureCount`](#failureCount).
+ */
+ errors: FirebaseArrayIndexError[];
+}
+
+/** Interface representing an Auth second factor in Auth server format. */
+export interface AuthFactorInfo {
+ // Not required for signupNewUser endpoint.
+ mfaEnrollmentId?: string;
+ displayName?: string;
+ phoneInfo?: string;
+ enrolledAt?: string;
+ [key: string]: any;
+}
+
+/** UploadAccount endpoint request user interface. */
+interface UploadAccountUser {
+ localId: string;
+ email?: string;
+ emailVerified?: boolean;
+ displayName?: string;
+ disabled?: boolean;
+ photoUrl?: string;
+ phoneNumber?: string;
+ providerUserInfo?: Array<{
+ rawId: string;
+ providerId: string;
+ email?: string;
+ displayName?: string;
+ photoUrl?: string;
+ }>;
+ mfaInfo?: AuthFactorInfo[];
+ passwordHash?: string;
+ salt?: string;
+ lastLoginAt?: number;
+ createdAt?: number;
+ customAttributes?: string;
+ tenantId?: string;
+}
+
+/** UploadAccount endpoint request hash options. */
+export interface UploadAccountOptions {
+ hashAlgorithm?: string;
+ signerKey?: string;
+ rounds?: number;
+ memoryCost?: number;
+ saltSeparator?: string;
+ cpuMemCost?: number;
+ parallelization?: number;
+ blockSize?: number;
+ dkLen?: number;
+}
+
+/** UploadAccount endpoint complete request interface. */
+export interface UploadAccountRequest extends UploadAccountOptions {
+ users?: UploadAccountUser[];
+}
+
+/** Callback function to validate an UploadAccountUser object. */
+export type ValidatorFunction = (data: UploadAccountUser) => void;
+
+/**
+ * Converts a client format second factor object to server format.
+ * @param multiFactorInfo - The client format second factor.
+ * @returns The corresponding AuthFactorInfo server request format.
+ */
+export function convertMultiFactorInfoToServerFormat(
+ multiFactorInfo: UpdateMultiFactorInfoRequest
+): AuthFactorInfo {
+ let enrolledAt;
+ if (typeof multiFactorInfo.enrollmentTime !== "undefined") {
+ if (validator.isUTCDateString(multiFactorInfo.enrollmentTime)) {
+ // Convert from UTC date string (client side format) to ISO date string (server side format).
+ enrolledAt = new Date(multiFactorInfo.enrollmentTime).toISOString();
+ } else {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ENROLLMENT_TIME,
+ `The second factor "enrollmentTime" for "${multiFactorInfo.uid}" must be a valid ` +
+ "UTC date string."
+ );
+ }
+ }
+ // Currently only phone second factors are supported.
+ if (isPhoneFactor(multiFactorInfo)) {
+ // If any required field is missing or invalid, validation will still fail later.
+ const authFactorInfo: AuthFactorInfo = {
+ mfaEnrollmentId: multiFactorInfo.uid,
+ displayName: multiFactorInfo.displayName,
+ // Required for all phone second factors.
+ phoneInfo: multiFactorInfo.phoneNumber,
+ enrolledAt,
+ };
+ for (const objKey in authFactorInfo) {
+ if (typeof authFactorInfo[objKey] === "undefined") {
+ delete authFactorInfo[objKey];
+ }
+ }
+ return authFactorInfo;
+ } else {
+ // Unsupported second factor.
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.UNSUPPORTED_SECOND_FACTOR,
+ `Unsupported second factor "${JSON.stringify(multiFactorInfo)}" provided.`
+ );
+ }
+}
+
+function isPhoneFactor(
+ multiFactorInfo: UpdateMultiFactorInfoRequest
+): multiFactorInfo is UpdatePhoneMultiFactorInfoRequest {
+ return multiFactorInfo.factorId === "phone";
+}
+
+/**
+ * @param {any} obj The object to check for number field within.
+ * @param {string} key The entry key.
+ * @returns {number} The corresponding number if available. Otherwise, NaN.
+ */
+function getNumberField(obj: any, key: string): number {
+ if (typeof obj[key] !== "undefined" && obj[key] !== null) {
+ return parseInt(obj[key].toString(), 10);
+ }
+ return NaN;
+}
+
+/**
+ * Converts a UserImportRecord to a UploadAccountUser object. Throws an error when invalid
+ * fields are provided.
+ * @param {UserImportRecord} user The UserImportRecord to conver to UploadAccountUser.
+ * @param {ValidatorFunction=} userValidator The user validator function.
+ * @returns {UploadAccountUser} The corresponding UploadAccountUser to return.
+ */
+function populateUploadAccountUser(
+ user: UserImportRecord,
+ userValidator?: ValidatorFunction
+): UploadAccountUser {
+ const result: UploadAccountUser = {
+ localId: user.uid,
+ email: user.email,
+ emailVerified: user.emailVerified,
+ displayName: user.displayName,
+ disabled: user.disabled,
+ photoUrl: user.photoURL,
+ phoneNumber: user.phoneNumber,
+ providerUserInfo: [],
+ mfaInfo: [],
+ tenantId: user.tenantId,
+ customAttributes: user.customClaims && JSON.stringify(user.customClaims),
+ };
+ if (typeof user.passwordHash !== "undefined") {
+ if (!validator.isBuffer(user.passwordHash)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH);
+ }
+ result.passwordHash = utils.toWebSafeBase64(user.passwordHash);
+ }
+ if (typeof user.passwordSalt !== "undefined") {
+ if (!validator.isBuffer(user.passwordSalt)) {
+ throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT);
+ }
+ result.salt = utils.toWebSafeBase64(user.passwordSalt);
+ }
+ if (validator.isNonNullObject(user.metadata)) {
+ if (validator.isNonEmptyString(user.metadata.creationTime)) {
+ result.createdAt = new Date(user.metadata.creationTime).getTime();
+ }
+ if (validator.isNonEmptyString(user.metadata.lastSignInTime)) {
+ result.lastLoginAt = new Date(user.metadata.lastSignInTime).getTime();
+ }
+ }
+ if (validator.isArray(user.providerData)) {
+ user.providerData.forEach((providerData) => {
+ result.providerUserInfo!.push({
+ providerId: providerData.providerId,
+ rawId: providerData.uid,
+ email: providerData.email,
+ displayName: providerData.displayName,
+ photoUrl: providerData.photoURL,
+ });
+ });
+ }
+
+ // Convert user.multiFactor.enrolledFactors to server format.
+ if (
+ validator.isNonNullObject(user.multiFactor) &&
+ validator.isNonEmptyArray(user.multiFactor.enrolledFactors)
+ ) {
+ user.multiFactor.enrolledFactors.forEach((multiFactorInfo) => {
+ result.mfaInfo!.push(
+ convertMultiFactorInfoToServerFormat(multiFactorInfo)
+ );
+ });
+ }
+
+ // Remove blank fields.
+ let key: keyof UploadAccountUser;
+ for (key in result) {
+ if (typeof result[key] === "undefined") {
+ delete result[key];
+ }
+ }
+ if (result.providerUserInfo!.length === 0) {
+ delete result.providerUserInfo;
+ }
+ if (result.mfaInfo!.length === 0) {
+ delete result.mfaInfo;
+ }
+ // Validate the constructured user individual request. This will throw if an error
+ // is detected.
+ if (typeof userValidator === "function") {
+ userValidator(result);
+ }
+ return result;
+}
+
+/**
+ * Class that provides a helper for building/validating uploadAccount requests and
+ * UserImportResult responses.
+ */
+export class UserImportBuilder {
+ private requiresHashOptions: boolean;
+ private validatedUsers: UploadAccountUser[];
+ private validatedOptions: UploadAccountOptions;
+ private indexMap: { [key: number]: number };
+ private userImportResultErrors: FirebaseArrayIndexError[];
+
+ /**
+ * @param {UserImportRecord[]} users The list of user records to import.
+ * @param {UserImportOptions=} options The import options which includes hashing
+ * algorithm details.
+ * @param {ValidatorFunction=} userRequestValidator The user request validator function.
+ * @constructor
+ */
+ constructor(
+ users: UserImportRecord[],
+ options?: UserImportOptions,
+ userRequestValidator?: ValidatorFunction
+ ) {
+ this.requiresHashOptions = false;
+ this.validatedUsers = [];
+ this.userImportResultErrors = [];
+ this.indexMap = {};
+
+ this.validatedUsers = this.populateUsers(users, userRequestValidator);
+ this.validatedOptions = this.populateOptions(
+ options,
+ this.requiresHashOptions
+ );
+ }
+
+ /**
+ * Returns the corresponding constructed uploadAccount request.
+ * @returns {UploadAccountRequest} The constructed uploadAccount request.
+ */
+ public buildRequest(): UploadAccountRequest {
+ const users = this.validatedUsers.map((user) => {
+ return deepCopy(user);
+ });
+ return deepExtend(
+ { users },
+ deepCopy(this.validatedOptions)
+ ) as UploadAccountRequest;
+ }
+
+ /**
+ * Populates the UserImportResult using the client side detected errors and the server
+ * side returned errors.
+ * @returns {UserImportResult} The user import result based on the returned failed
+ * uploadAccount response.
+ */
+ public buildResponse(
+ failedUploads: Array<{ index: number; message: string }>
+ ): UserImportResult {
+ // Initialize user import result.
+ const importResult: UserImportResult = {
+ successCount: this.validatedUsers.length,
+ failureCount: this.userImportResultErrors.length,
+ errors: deepCopy(this.userImportResultErrors),
+ };
+ importResult.failureCount += failedUploads.length;
+ importResult.successCount -= failedUploads.length;
+ failedUploads.forEach((failedUpload) => {
+ importResult.errors.push({
+ // Map backend request index to original developer provided array index.
+ index: this.indexMap[failedUpload.index],
+ error: new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_USER_IMPORT,
+ failedUpload.message
+ ),
+ });
+ });
+ // Sort errors by index.
+ importResult.errors.sort((a, b) => {
+ return a.index - b.index;
+ });
+ // Return sorted result.
+ return importResult;
+ }
+
+ /**
+ * Validates and returns the hashing options of the uploadAccount request.
+ * Throws an error whenever an invalid or missing options is detected.
+ * @param {UserImportOptions} options The UserImportOptions.
+ * @param {boolean} requiresHashOptions Whether to require hash options.
+ * @returns {UploadAccountOptions} The populated UploadAccount options.
+ */
+ private populateOptions(
+ options: UserImportOptions | undefined,
+ requiresHashOptions: boolean
+ ): UploadAccountOptions {
+ let populatedOptions: UploadAccountOptions;
+ if (!requiresHashOptions) {
+ return {};
+ }
+ if (!validator.isNonNullObject(options)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"UserImportOptions" are required when importing users with passwords.'
+ );
+ }
+ if (!validator.isNonNullObject(options.hash)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.MISSING_HASH_ALGORITHM,
+ '"hash.algorithm" is missing from the provided "UserImportOptions".'
+ );
+ }
+ if (
+ typeof options.hash.algorithm === "undefined" ||
+ !validator.isNonEmptyString(options.hash.algorithm)
+ ) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_HASH_ALGORITHM,
+ '"hash.algorithm" must be a string matching the list of supported algorithms.'
+ );
+ }
+
+ let rounds: number;
+ switch (options.hash.algorithm) {
+ case "HMAC_SHA512":
+ case "HMAC_SHA256":
+ case "HMAC_SHA1":
+ case "HMAC_MD5":
+ if (!validator.isBuffer(options.hash.key)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_HASH_KEY,
+ 'A non-empty "hash.key" byte buffer must be provided for ' +
+ `hash algorithm ${options.hash.algorithm}.`
+ );
+ }
+ populatedOptions = {
+ hashAlgorithm: options.hash.algorithm,
+ signerKey: utils.toWebSafeBase64(options.hash.key),
+ };
+ break;
+
+ case "MD5":
+ case "SHA1":
+ case "SHA256":
+ case "SHA512": {
+ // MD5 is [0,8192] but SHA1, SHA256, and SHA512 are [1,8192]
+ rounds = getNumberField(options.hash, "rounds");
+ const minRounds = options.hash.algorithm === "MD5" ? 0 : 1;
+ if (isNaN(rounds) || rounds < minRounds || rounds > 8192) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_HASH_ROUNDS,
+ `A valid "hash.rounds" number between ${minRounds} and 8192 must be provided for ` +
+ `hash algorithm ${options.hash.algorithm}.`
+ );
+ }
+ populatedOptions = {
+ hashAlgorithm: options.hash.algorithm,
+ rounds,
+ };
+ break;
+ }
+ case "PBKDF_SHA1":
+ case "PBKDF2_SHA256":
+ rounds = getNumberField(options.hash, "rounds");
+ if (isNaN(rounds) || rounds < 0 || rounds > 120000) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_HASH_ROUNDS,
+ 'A valid "hash.rounds" number between 0 and 120000 must be provided for ' +
+ `hash algorithm ${options.hash.algorithm}.`
+ );
+ }
+ populatedOptions = {
+ hashAlgorithm: options.hash.algorithm,
+ rounds,
+ };
+ break;
+
+ case "SCRYPT": {
+ if (!validator.isBuffer(options.hash.key)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_HASH_KEY,
+ 'A "hash.key" byte buffer must be provided for ' +
+ `hash algorithm ${options.hash.algorithm}.`
+ );
+ }
+ rounds = getNumberField(options.hash, "rounds");
+ if (isNaN(rounds) || rounds <= 0 || rounds > 8) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_HASH_ROUNDS,
+ 'A valid "hash.rounds" number between 1 and 8 must be provided for ' +
+ `hash algorithm ${options.hash.algorithm}.`
+ );
+ }
+ const memoryCost = getNumberField(options.hash, "memoryCost");
+ if (isNaN(memoryCost) || memoryCost <= 0 || memoryCost > 14) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_HASH_MEMORY_COST,
+ 'A valid "hash.memoryCost" number between 1 and 14 must be provided for ' +
+ `hash algorithm ${options.hash.algorithm}.`
+ );
+ }
+ if (
+ typeof options.hash.saltSeparator !== "undefined" &&
+ !validator.isBuffer(options.hash.saltSeparator)
+ ) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_HASH_SALT_SEPARATOR,
+ '"hash.saltSeparator" must be a byte buffer.'
+ );
+ }
+ populatedOptions = {
+ hashAlgorithm: options.hash.algorithm,
+ signerKey: utils.toWebSafeBase64(options.hash.key),
+ rounds,
+ memoryCost,
+ saltSeparator: utils.toWebSafeBase64(
+ options.hash.saltSeparator || Buffer.from("")
+ ),
+ };
+ break;
+ }
+ case "BCRYPT":
+ populatedOptions = {
+ hashAlgorithm: options.hash.algorithm,
+ };
+ break;
+
+ case "STANDARD_SCRYPT": {
+ const cpuMemCost = getNumberField(options.hash, "memoryCost");
+ if (isNaN(cpuMemCost)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_HASH_MEMORY_COST,
+ 'A valid "hash.memoryCost" number must be provided for ' +
+ `hash algorithm ${options.hash.algorithm}.`
+ );
+ }
+ const parallelization = getNumberField(options.hash, "parallelization");
+ if (isNaN(parallelization)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_HASH_PARALLELIZATION,
+ 'A valid "hash.parallelization" number must be provided for ' +
+ `hash algorithm ${options.hash.algorithm}.`
+ );
+ }
+ const blockSize = getNumberField(options.hash, "blockSize");
+ if (isNaN(blockSize)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_HASH_BLOCK_SIZE,
+ 'A valid "hash.blockSize" number must be provided for ' +
+ `hash algorithm ${options.hash.algorithm}.`
+ );
+ }
+ const dkLen = getNumberField(options.hash, "derivedKeyLength");
+ if (isNaN(dkLen)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_HASH_DERIVED_KEY_LENGTH,
+ 'A valid "hash.derivedKeyLength" number must be provided for ' +
+ `hash algorithm ${options.hash.algorithm}.`
+ );
+ }
+ populatedOptions = {
+ hashAlgorithm: options.hash.algorithm,
+ cpuMemCost,
+ parallelization,
+ blockSize,
+ dkLen,
+ };
+ break;
+ }
+ default:
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_HASH_ALGORITHM,
+ `Unsupported hash algorithm provider "${options.hash.algorithm}".`
+ );
+ }
+ return populatedOptions;
+ }
+
+ /**
+ * Validates and returns the users list of the uploadAccount request.
+ * Whenever a user with an error is detected, the error is cached and will later be
+ * merged into the user import result. This allows the processing of valid users without
+ * failing early on the first error detected.
+ * @param {UserImportRecord[]} users The UserImportRecords to convert to UnploadAccountUser
+ * objects.
+ * @param {ValidatorFunction=} userValidator The user validator function.
+ * @returns {UploadAccountUser[]} The populated uploadAccount users.
+ */
+ private populateUsers(
+ users: UserImportRecord[],
+ userValidator?: ValidatorFunction
+ ): UploadAccountUser[] {
+ const populatedUsers: UploadAccountUser[] = [];
+ users.forEach((user, index) => {
+ try {
+ const result = populateUploadAccountUser(user, userValidator);
+ if (typeof result.passwordHash !== "undefined") {
+ this.requiresHashOptions = true;
+ }
+ // Only users that pass client screening will be passed to backend for processing.
+ populatedUsers.push(result);
+ // Map user's index (the one to be sent to backend) to original developer provided array.
+ this.indexMap[populatedUsers.length - 1] = index;
+ } catch (error) {
+ // Save the client side error with respect to the developer provided array.
+ this.userImportResultErrors.push({
+ index,
+ error,
+ });
+ }
+ });
+ return populatedUsers;
+ }
+}
diff --git a/packages/dart_firebase_admin/lib/src/auth/user_info.dart b/packages/dart_firebase_admin/lib/src/auth/user_info.dart
deleted file mode 100644
index 612f962..0000000
--- a/packages/dart_firebase_admin/lib/src/auth/user_info.dart
+++ /dev/null
@@ -1,19 +0,0 @@
-part of '../../dart_firebase_admin.dart';
-
-class UserInfo {
- UserInfo._(this._delegate);
-
- final firebase_auth_v1.GoogleCloudIdentitytoolkitV1ProviderUserInfo _delegate;
-
- String get displayName => _delegate.displayName!;
-
- String get email => _delegate.email!;
-
- String get phoneNumber => _delegate.phoneNumber!;
-
- String get photoURL => _delegate.photoUrl!;
-
- String get providerId => _delegate.providerId!;
-
- String get uid => _delegate.rawId!;
-}
diff --git a/packages/dart_firebase_admin/lib/src/auth/user_metadata.dart b/packages/dart_firebase_admin/lib/src/auth/user_metadata.dart
deleted file mode 100644
index 4de3fda..0000000
--- a/packages/dart_firebase_admin/lib/src/auth/user_metadata.dart
+++ /dev/null
@@ -1,21 +0,0 @@
-part of '../../dart_firebase_admin.dart';
-
-class UserMetadata {
- UserMetadata._(this._delegate);
-
- final firebase_auth_v1.GoogleCloudIdentitytoolkitV1UserInfo _delegate;
-
- DateTime get creationTime => DateTime.fromMillisecondsSinceEpoch(
- int.parse(_delegate.createdAt!),
- );
-
- DateTime get lastSignInTime => DateTime.fromMillisecondsSinceEpoch(
- int.parse(_delegate.lastLoginAt!),
- );
-
- DateTime? get lastRefreshTime => _delegate.lastRefreshAt == null
- ? null
- : DateTime.fromMillisecondsSinceEpoch(
- int.parse(_delegate.lastRefreshAt!),
- );
-}
diff --git a/packages/dart_firebase_admin/lib/src/auth/user_record.dart b/packages/dart_firebase_admin/lib/src/auth/user_record.dart
deleted file mode 100644
index 6dd7eb8..0000000
--- a/packages/dart_firebase_admin/lib/src/auth/user_record.dart
+++ /dev/null
@@ -1,42 +0,0 @@
-part of '../../dart_firebase_admin.dart';
-
-class UserRecord {
- UserRecord._(this._delegate);
-
- final firebase_auth_v1.GoogleCloudIdentitytoolkitV1UserInfo _delegate;
-
- Map? get customClaims => _delegate.customAttributes == null
- ? null
- : jsonDecode(_delegate.customAttributes!) as Map;
-
- bool get disabled => _delegate.disabled ?? false;
-
- String? get displayName => _delegate.displayName;
-
- String? get email => _delegate.email;
-
- UserMetadata get metadata => UserMetadata._(_delegate);
-
- Object get multiFactor => {};
-
- String? get passwordHash => _delegate.passwordHash;
-
- String? get passwordSalt => _delegate.salt;
-
- String? get phoneNumber => _delegate.phoneNumber;
-
- String? get photoURL => _delegate.photoUrl;
-
- List get providerData =>
- _delegate.providerUserInfo?.map(UserInfo._).toList() ?? [];
-
- String? get tenantId => _delegate.tenantId;
-
- DateTime? get tokensValidAfterTime => _delegate.validSince == null
- ? null
- : DateTime.fromMillisecondsSinceEpoch(
- int.parse(_delegate.validSince! * 1000),
- );
-
- String get uid => _delegate.localId!;
-}
diff --git a/packages/dart_firebase_admin/lib/src/credential.dart b/packages/dart_firebase_admin/lib/src/credential.dart
index e85f3c1..da53d15 100644
--- a/packages/dart_firebase_admin/lib/src/credential.dart
+++ b/packages/dart_firebase_admin/lib/src/credential.dart
@@ -1,8 +1,10 @@
-part of '../dart_firebase_admin.dart';
+part of 'dart_firebase_admin.dart';
+/// Authentication informations for Firebase Admin SDK.
class Credential {
Credential._(this._serviceAccountCredentials);
+ /// Log in to firebase from a service account file.
factory Credential.fromServiceAccount(File serviceAccountFile) {
final content = serviceAccountFile.readAsStringSync();
@@ -17,11 +19,13 @@ class Credential {
return Credential._(serviceAccountCredentials);
}
+ /// Log in to firebase using the environment variable.
Credential.fromApplicationDefaultCredentials() : this._(null);
final auth.ServiceAccountCredentials? _serviceAccountCredentials;
- Future _getAuthClient(List scopes) {
+ @internal
+ Future getAuthClient(List scopes) {
final serviceAccountCredentials = _serviceAccountCredentials;
if (serviceAccountCredentials == null) {
return auth.clientViaApplicationDefaultCredentials(scopes: scopes);
diff --git a/packages/dart_firebase_admin/lib/src/dart_firebase_admin.dart b/packages/dart_firebase_admin/lib/src/dart_firebase_admin.dart
new file mode 100644
index 0000000..51043d8
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/dart_firebase_admin.dart
@@ -0,0 +1,15 @@
+library dart_firebase_admin;
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:firebaseapis/identitytoolkit/v1.dart' as firebase_auth_v1;
+import 'package:googleapis_auth/auth_io.dart' as auth;
+import 'package:meta/meta.dart';
+
+import 'app/core.dart';
+
+part 'auth/auth_exception.dart';
+part 'credential.dart';
+part 'exception.dart';
+part 'firebase_admin.dart';
diff --git a/packages/dart_firebase_admin/lib/src/exception.dart b/packages/dart_firebase_admin/lib/src/exception.dart
index d5d0d43..9f1a28f 100644
--- a/packages/dart_firebase_admin/lib/src/exception.dart
+++ b/packages/dart_firebase_admin/lib/src/exception.dart
@@ -1,4 +1,4 @@
-part of '../dart_firebase_admin.dart';
+part of 'dart_firebase_admin.dart';
/// A set of platform level error codes.
///
@@ -43,52 +43,16 @@ String _platformErrorCodeMessage(String code) {
}
/// Base interface for all Firebase Admin related errors.
-abstract class FirebaseAdminException implements Exception {
+abstract class FirebaseAdminException extends FirebaseException {
FirebaseAdminException(this.service, this._code, [this._message]);
final String service;
final String _code;
final String? _message;
+ @override
String get code => '$service/${_code.replaceAll('_', '-').toLowerCase()}';
- String get message => _message ?? _platformErrorCodeMessage(_code);
-}
-
-/// Converts a Exception to a FirebaseAdminException.
-Never _handleException(Object exception, StackTrace stackTrace) {
- if (exception is firebase_auth_v1.DetailedApiRequestError) {
- Error.throwWithStackTrace(
- FirebaseAuthAdminException.fromServerError(exception),
- stackTrace,
- );
- }
-
- Error.throwWithStackTrace(exception, stackTrace);
-}
-
-/// A generic guard wrapper for API calls to handle exceptions.
-R guard(R Function() cb) {
- try {
- final value = cb();
-
- if (value is Future) {
- return value.catchError(_handleException) as R;
- }
-
- return value;
- } catch (error, stackTrace) {
- _handleException(error, stackTrace);
- }
-}
-
-class FirebaseArrayIndexException implements Exception {
- FirebaseArrayIndexException(this.index, this.message);
-
- final int index;
-
- final String message;
-
@override
- String toString() => 'FirebaseArrayIndexException: $message';
+ String get message => _message ?? _platformErrorCodeMessage(_code);
}
diff --git a/packages/dart_firebase_admin/lib/src/firebase_admin.dart b/packages/dart_firebase_admin/lib/src/firebase_admin.dart
index 3383084..8dc969f 100644
--- a/packages/dart_firebase_admin/lib/src/firebase_admin.dart
+++ b/packages/dart_firebase_admin/lib/src/firebase_admin.dart
@@ -1,24 +1,22 @@
-part of '../dart_firebase_admin.dart';
+part of 'dart_firebase_admin.dart';
class FirebaseAdminApp {
- FirebaseAdminApp._(this._projectId, this._credential);
+ FirebaseAdminApp.initializeApp(this.projectId, this.credential);
- factory FirebaseAdminApp.initializeApp(
- String projectId,
- Credential credential,
- ) {
- return FirebaseAdminApp._(projectId, credential);
- }
+ final String projectId;
+ final Credential credential;
- final String _projectId;
- final Credential _credential;
-}
+ bool get isUsingEmulator => _isUsingEmulator;
+ var _isUsingEmulator = false;
-extension FirebaseAdminStringExtension on String {
- bool get isUid => isNotEmpty && length <= 128;
- // TODO check these are correct
- // https://github.com/firebase/firebase-admin-node/blob/aea280d325c202fedc3890850d8c04f2f7e9cd54/src/utils/validator.ts#L160
- bool get isEmail => RegExp(r'/^[^@]+@[^@]+$/').hasMatch(this);
- bool get isPhoneNumber =>
- startsWith('+') && RegExp(r'/[\da-zA-Z]+/').hasMatch(this);
+ @internal
+ Uri authApiHost = Uri.https('identitytoolkit.googleapis.com', '/');
+ @internal
+ Uri firestoreApiHost = Uri.https('identitytoolkit.googleapis.com', '/');
+
+ void useEmulator() {
+ _isUsingEmulator = true;
+ authApiHost = Uri.http('127.0.0.1:9099', 'identitytoolkit.googleapis.com/');
+ firestoreApiHost = Uri.http('127.0.0.1:8080', '/');
+ }
}
diff --git a/packages/dart_firebase_admin/lib/src/google_cloud_firestore/convert.dart b/packages/dart_firebase_admin/lib/src/google_cloud_firestore/convert.dart
new file mode 100644
index 0000000..af4cd5d
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/google_cloud_firestore/convert.dart
@@ -0,0 +1,26 @@
+part of 'firestore.dart';
+
+/// Verifies that a `Value` only has a single type set.
+void _assertValidProtobufValue(firestore1.Value proto) {
+ final values = [
+ proto.booleanValue,
+ proto.doubleValue,
+ proto.integerValue,
+ proto.stringValue,
+ proto.timestampValue,
+ proto.nullValue,
+ proto.mapValue,
+ proto.arrayValue,
+ proto.referenceValue,
+ proto.geoPointValue,
+ proto.bytesValue,
+ ];
+
+ if (values.whereNotNull().length != 1) {
+ throw ArgumentError.value(
+ proto,
+ 'proto',
+ 'Unable to infer type value',
+ );
+ }
+}
diff --git a/packages/dart_firebase_admin/lib/src/google_cloud_firestore/document.dart b/packages/dart_firebase_admin/lib/src/google_cloud_firestore/document.dart
new file mode 100644
index 0000000..0729275
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/google_cloud_firestore/document.dart
@@ -0,0 +1,499 @@
+part of 'firestore.dart';
+
+class Optional {
+ const Optional(this.value);
+ final T? value;
+}
+
+/// A DocumentSnapshot is an immutable representation for a document in a
+/// Firestore database. The data can be extracted with [data].
+///
+/// For a DocumentSnapshot that points to a non-existing document, any data
+/// access will return 'undefined'. You can use the
+/// [exists] property to explicitly verify a document's existence.
+@immutable
+class DocumentSnapshot {
+ const DocumentSnapshot._({
+ required this.ref,
+ required this.readTime,
+ required this.createTime,
+ required this.updateTime,
+ required firestore1.MapValue? fieldsProto,
+ }) : _fieldsProto = fieldsProto;
+
+ factory DocumentSnapshot._fromObject(
+ DocumentReference ref,
+ DocumentData data,
+ ) {
+ final serializer = ref.firestore._serializer;
+
+ return DocumentSnapshot._(
+ ref: ref,
+ fieldsProto: serializer.encodeFields(data),
+ readTime: null,
+ createTime: null,
+ updateTime: null,
+ );
+ }
+
+ factory DocumentSnapshot.fromUpdateMap(
+ DocumentReference ref,
+ UpdateMap data,
+ ) {
+ final serializer = ref.firestore._serializer;
+
+ /// Merges 'value' at the field path specified by the path array into
+ /// 'target'.
+ ApiMapValue? merge({
+ required ApiMapValue target,
+ required Object? value,
+ required List path,
+ required int pos,
+ }) {
+ final key = path[pos];
+ final isLast = pos == path.length - 1;
+
+ if (!target.containsKey(key)) {
+ if (isLast) {
+ if (value is _FieldTransform) {
+ // If there is already data at this path, we need to retain it.
+ // Otherwise, we don't include it in the DocumentSnapshot.
+ return target.isNotEmpty ? target : null;
+ }
+ // The merge is done
+ final leafNode = serializer.encodeValue(value);
+ if (leafNode != null) {
+ target[key] = leafNode;
+ }
+ return target;
+ } else {
+ // We need to expand the target object.
+ final childNode = {};
+
+ final nestedValue = merge(
+ target: childNode,
+ value: value,
+ path: path,
+ pos: pos + 1,
+ );
+
+ if (nestedValue != null) {
+ target[key] = firestore1.Value(
+ mapValue: firestore1.MapValue(fields: nestedValue),
+ );
+ return target;
+ } else {
+ return target.isNotEmpty ? target : null;
+ }
+ }
+ } else {
+ assert(!isLast, "Can't merge current value into a nested object");
+ target[key] = firestore1.Value(
+ mapValue: firestore1.MapValue(
+ fields: merge(
+ target: target[key]!.mapValue!.fields!,
+ value: value,
+ path: path,
+ pos: pos + 1,
+ ),
+ ),
+ );
+ return target;
+ }
+ }
+
+ final res = {};
+ for (final entry in data.entries) {
+ final path = entry.key._toList();
+ merge(target: res, value: entry.value, path: path, pos: 0);
+ }
+
+ return DocumentSnapshot._(
+ ref: ref,
+ fieldsProto: firestore1.MapValue(fields: res),
+ readTime: null,
+ createTime: null,
+ updateTime: null,
+ );
+ }
+
+ static DocumentSnapshot _fromDocument(
+ firestore1.Document document,
+ String? readTime,
+ Firestore firestore,
+ ) {
+ final ref = DocumentReference._(
+ firestore: firestore,
+ path: _QualifiedResourcePath.fromSlashSeparatedString(document.name!),
+ converter: _jsonConverter,
+ );
+
+ final builder = _DocumentSnapshotBuilder(ref)
+ ..fieldsProto = firestore1.MapValue(fields: document.fields ?? {})
+ ..createTime = document.createTime.let(Timestamp._fromString)
+ ..readTime = readTime.let(Timestamp._fromString)
+ ..updateTime = document.updateTime.let(Timestamp._fromString);
+
+ return builder.build();
+ }
+
+ static DocumentSnapshot _missing(
+ String document,
+ String? readTime,
+ Firestore firestore,
+ ) {
+ final ref = DocumentReference._(
+ firestore: firestore,
+ path: _QualifiedResourcePath.fromSlashSeparatedString(document),
+ converter: _jsonConverter,
+ );
+
+ final builder = _DocumentSnapshotBuilder(ref)
+ ..readTime = readTime.let(Timestamp._fromString);
+
+ return builder.build();
+ }
+
+ /// A [DocumentReference] for the document stored in this snapshot.
+ final DocumentReference ref;
+ final Timestamp? readTime;
+ final Timestamp? createTime;
+ final Timestamp? updateTime;
+ final firestore1.MapValue? _fieldsProto;
+
+ /// The ID of the document for which this DocumentSnapshot contains data.
+ String get id => ref.id;
+
+ /// True if the document exists.
+ ///
+ /// @type {boolean}
+ /// @name DocumentSnapshot#exists
+ /// @readonly
+ ///
+ /// ```dart
+ /// final documentRef = firestore.doc('col/doc');
+ ///
+ /// documentRef.get().then((documentSnapshot) {
+ /// if (documentSnapshot.exists) {
+ /// print('Data: ${JSON.stringify(documentSnapshot.data())}');
+ /// }
+ /// });
+ /// ```
+ bool get exists => this._fieldsProto != null;
+
+ /// Retrieves all fields in the document as an object. Returns 'undefined' if
+ /// the document doesn't exist.
+ ///
+ /// Returns an object containing all fields in the document or
+ /// 'null' if the document doesn't exist.
+ ///
+ /// ```dart
+ /// final documentRef = firestore.doc('col/doc');
+ ///
+ /// documentRef.get().then((documentSnapshot) {
+ /// final data = documentSnapshot.data();
+ /// print('Retrieved data: ${JSON.stringify(data)}');
+ /// });
+ /// ```
+ T? data() {
+ final fieldsProto = this._fieldsProto;
+ final fields = fieldsProto?.fields;
+ if (fields == null || fieldsProto == null) return null;
+
+ final converter = ref._converter;
+ // We only want to use the converter and create a new QueryDocumentSnapshot
+ // if a converter has been provided.
+ if (!identical(converter, _jsonConverter)) {
+ final untypedReference = DocumentReference._(
+ firestore: ref.firestore,
+ path: ref._path,
+ converter: _jsonConverter,
+ );
+
+ return converter.fromFirestore(
+ QueryDocumentSnapshot._(
+ ref: untypedReference,
+ fieldsProto: fieldsProto,
+ readTime: readTime,
+ createTime: createTime,
+ updateTime: updateTime,
+ ),
+ );
+ } else {
+ final object = {
+ for (final prop in fields.entries)
+ prop.key: ref.firestore._serializer.decodeValue(prop.value),
+ };
+
+ return object as T;
+ }
+ }
+
+ /// Retrieves the field specified by [field].
+ ///
+ /// Will return `null` if the field does not exists.
+ /// Will return `Optional(null)` if the field exists but is `null`.
+ Optional? get(Object field) {
+ final fieldPath = FieldPath.from(field);
+ final protoField = _protoField(fieldPath);
+
+ if (protoField == null) return null;
+
+ return Optional(
+ ref.firestore._serializer.decodeValue(protoField),
+ );
+ }
+
+ firestore1.Value? _protoField(FieldPath field) {
+ final fieldsProto = this._fieldsProto?.fields;
+ if (fieldsProto == null) return null;
+ var fields = fieldsProto;
+
+ final components = field._toList();
+ for (var i = 0; i < components.length - 1; i++) {
+ final component = components[i];
+
+ final newFields = fields[component]?.mapValue;
+ // The field component is not present.
+ if (newFields == null) return null;
+
+ fields = newFields.fields!;
+ }
+
+ return fields[components.last];
+ }
+
+ firestore1.Write _toWriteProto() {
+ return firestore1.Write(
+ update: firestore1.Document(
+ name: ref._formattedName,
+ fields: _fieldsProto?.fields,
+ ),
+ );
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return other is DocumentSnapshot &&
+ runtimeType == other.runtimeType &&
+ ref == other.ref &&
+ const DeepCollectionEquality().equals(_fieldsProto, other._fieldsProto);
+ }
+
+ @override
+ int get hashCode => Object.hash(
+ runtimeType,
+ ref,
+ const DeepCollectionEquality().hash(_fieldsProto),
+ );
+}
+
+class _DocumentSnapshotBuilder {
+ _DocumentSnapshotBuilder(this.ref);
+
+ final DocumentReference ref;
+
+ Timestamp? readTime;
+ Timestamp? createTime;
+ Timestamp? updateTime;
+ firestore1.MapValue? fieldsProto;
+
+ DocumentSnapshot build() {
+ assert(
+ (this.fieldsProto != null) == (this.createTime != null),
+ 'Create time should be set iff document exists.',
+ );
+ assert(
+ (this.fieldsProto != null) == (this.updateTime != null),
+ 'Update time should be set iff document exists.',
+ );
+
+ final fieldsProto = this.fieldsProto;
+ if (fieldsProto != null) {
+ return QueryDocumentSnapshot._(
+ ref: ref,
+ fieldsProto: fieldsProto,
+ readTime: readTime,
+ createTime: createTime,
+ updateTime: updateTime,
+ );
+ }
+
+ return DocumentSnapshot._(
+ ref: ref,
+ readTime: readTime,
+ createTime: createTime,
+ updateTime: updateTime,
+ fieldsProto: null,
+ );
+ }
+}
+
+class QueryDocumentSnapshot extends DocumentSnapshot {
+ const QueryDocumentSnapshot._({
+ required super.ref,
+ required super.readTime,
+ required super.createTime,
+ required super.updateTime,
+ required super.fieldsProto,
+ }) : super._();
+
+ @override
+ Timestamp get createTime => super.createTime!;
+
+ @override
+ Timestamp get updateTime => super.updateTime!;
+
+ @override
+ T data() {
+ final data = super.data();
+ if (data == null) {
+ throw StateError(
+ 'The data in a QueryDocumentSnapshot should always exist.',
+ );
+ }
+ return data;
+ }
+}
+
+/// A Firestore Document Transform.
+///
+/// A DocumentTransform contains pending server-side transforms and their
+/// corresponding field paths.
+class _DocumentTransform {
+ _DocumentTransform({required this.ref, required this.transforms});
+
+ factory _DocumentTransform.fromObject(
+ DocumentReference ref,
+ DocumentData data,
+ ) {
+ final updateMap = {
+ for (final entry in data.entries) FieldPath([entry.key]): entry.value,
+ };
+
+ return _DocumentTransform.fromUpdateMap(ref, updateMap);
+ }
+
+ factory _DocumentTransform.fromUpdateMap(
+ DocumentReference ref,
+ UpdateMap data,
+ ) {
+ final transforms = {};
+
+ void encode(
+ Object? val,
+ FieldPath path, {
+ required bool allowTransforms,
+ }) {
+ if (val is _FieldTransform && val.includeInDocumentTransform) {
+ if (allowTransforms) {
+ transforms[path] = val;
+ } else {
+ throw ArgumentError(
+ '${val.methodName}() is not supported inside of array values.',
+ );
+ }
+ } else if (val is List) {
+ val.forEachIndexed((i, value) {
+ // We need to verify that no array value contains a document transform
+ encode(
+ value,
+ path._append('$i'),
+ allowTransforms: false,
+ );
+ });
+ } else if (val is Map) {
+ for (final entry in val.entries) {
+ encode(
+ entry.value,
+ path._append(entry.key.toString()),
+ allowTransforms: allowTransforms,
+ );
+ }
+ }
+ }
+
+ for (final entry in data.entries) {
+ encode(entry.value, entry.key, allowTransforms: true);
+ }
+
+ return _DocumentTransform(
+ ref: ref,
+ transforms: transforms,
+ );
+ }
+
+ final DocumentReference ref;
+ final Map transforms;
+
+ void validate() {
+ for (final transform in transforms.values) {
+ transform.validate();
+ }
+ }
+
+ /// Converts a document transform to the Firestore 'FieldTransform' Proto.
+ List toProto(_Serializer serializer) {
+ return [
+ for (final entry in transforms.entries)
+ entry.value._toProto(serializer, entry.key),
+ ];
+ }
+}
+
+/// A condition to check before performing an operation.
+class Precondition {
+ /// Checks that the document exists or not.
+ // ignore: avoid_positional_boolean_parameters, cf https://github.com/dart-lang/linter/issues/1638
+ Precondition.exists(bool this._exists) : _lastUpdateTime = null;
+
+ /// Checks that the document has last been updated at the specified time.
+ Precondition.timestamp(Timestamp this._lastUpdateTime) : _exists = null;
+
+ final bool? _exists;
+ final Timestamp? _lastUpdateTime;
+
+ /// Whether this DocumentTransform contains any enforcement.
+ bool get _isEmpty => _exists == null && _lastUpdateTime == null;
+
+ firestore1.Precondition? _toProto() {
+ if (_isEmpty) return null;
+
+ final lastUpdateTime = _lastUpdateTime;
+ if (lastUpdateTime != null) {
+ return firestore1.Precondition(
+ updateTime: lastUpdateTime._toProto().timestampValue,
+ );
+ }
+
+ return firestore1.Precondition(exists: _exists);
+ }
+}
+
+class _DocumentMask {
+ _DocumentMask(List fieldPaths)
+ : _sortedPaths = fieldPaths.sorted((a, b) => a.compareTo(b));
+
+ factory _DocumentMask.fromUpdateMap(Map data) {
+ final fieldPaths = [];
+
+ for (final entry in data.entries) {
+ final value = entry.value;
+ if (value is! _FieldTransform || value.includeInDocumentMask) {
+ fieldPaths.add(entry.key);
+ }
+ }
+
+ return _DocumentMask(fieldPaths);
+ }
+
+ final List _sortedPaths;
+
+ firestore1.DocumentMask toProto() {
+ if (_sortedPaths.isEmpty) return firestore1.DocumentMask();
+
+ return firestore1.DocumentMask(
+ fieldPaths: _sortedPaths.map((e) => e._formattedName).toList(),
+ );
+ }
+}
diff --git a/packages/dart_firebase_admin/lib/src/google_cloud_firestore/document_change.dart b/packages/dart_firebase_admin/lib/src/google_cloud_firestore/document_change.dart
new file mode 100644
index 0000000..05dbf2a
--- /dev/null
+++ b/packages/dart_firebase_admin/lib/src/google_cloud_firestore/document_change.dart
@@ -0,0 +1,50 @@
+part of 'firestore.dart';
+
+enum DocumentChangeType {
+ added,
+ removed,
+ modified,
+}
+
+/// A DocumentChange represents a change to the documents matching a query.
+/// It contains the document affected and the type of change that occurred.
+@immutable
+class DocumentChange {
+ const DocumentChange._({
+ required this.oldIndex,
+ required this.newIndex,
+ required this.doc,
+ required this.type,
+ });
+
+ /// The index of the changed document in the result set immediately prior to
+ /// this DocumentChange (i.e. supposing that all prior DocumentChange objects
+ /// have been applied). Is -1 for 'added' events.
+ final int oldIndex;
+
+ /// The index of the changed document in the result set immediately after
+ /// this DocumentChange (i.e. supposing that all prior DocumentChange
+ /// objects and the current DocumentChange object have been applied).
+ /// Is -1 for 'removed' events.
+ final int newIndex;
+
+ /// The document affected by this change.
+ final QueryDocumentSnapshot