├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── example_request.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yaml └── workflows │ └── build.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── all_lint_rules.yaml ├── analysis_options.yaml ├── melos.yaml ├── packages └── dart_firebase_admin │ ├── .pubignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── example │ ├── .gitignore │ ├── analysis_options.yaml │ ├── lib │ │ └── main.dart │ ├── pubspec.yaml │ └── storage.rules │ ├── lib │ ├── app_check.dart │ ├── auth.dart │ ├── dart_firebase_admin.dart │ ├── firestore.dart │ ├── messaging.dart │ ├── security_rules.dart │ └── src │ │ ├── app.dart │ │ ├── app │ │ ├── credential.dart │ │ ├── exception.dart │ │ └── firebase_admin.dart │ │ ├── app_check │ │ ├── ap_check_api_internal.dart │ │ ├── app_check.dart │ │ ├── app_check_api.dart │ │ ├── token_generator.dart │ │ └── token_verifier.dart │ │ ├── auth.dart │ │ ├── auth │ │ ├── action_code_settings_builder.dart │ │ ├── auth.dart │ │ ├── auth_api_request.dart │ │ ├── auth_config.dart │ │ ├── auth_exception.dart │ │ ├── base_auth.dart │ │ ├── identifier.dart │ │ ├── token_generator.dart │ │ ├── token_verifier.dart │ │ ├── user.dart │ │ └── user_import_builder.dart │ │ ├── google_cloud_firestore │ │ ├── backoff.dart │ │ ├── collection_group.dart │ │ ├── convert.dart │ │ ├── document.dart │ │ ├── document_change.dart │ │ ├── document_reader.dart │ │ ├── field_value.dart │ │ ├── filter.dart │ │ ├── firestore.dart │ │ ├── firestore.freezed.dart │ │ ├── firestore_api_request_internal.dart │ │ ├── firestore_exception.dart │ │ ├── geo_point.dart │ │ ├── path.dart │ │ ├── reference.dart │ │ ├── serializer.dart │ │ ├── status_code.dart │ │ ├── timestamp.dart │ │ ├── transaction.dart │ │ ├── types.dart │ │ ├── util.dart │ │ ├── validate.dart │ │ └── write_batch.dart │ │ ├── messaging.dart │ │ ├── messaging │ │ ├── fmc_exception.dart │ │ ├── messaging_api.dart │ │ └── messaging_api_request_internal.dart │ │ ├── object_utils.dart │ │ ├── security_rules │ │ ├── security_rules.dart │ │ ├── security_rules_api_internals.dart │ │ └── security_rules_internals.dart │ │ └── utils │ │ ├── crypto_signer.dart │ │ ├── error.dart │ │ ├── index.dart │ │ ├── jwt.dart │ │ ├── utils.dart │ │ ├── validator.dart │ │ └── validator.ts │ ├── pubspec.yaml │ └── test │ ├── analysis_options.yaml │ ├── app_check │ └── app_check_test.dart │ ├── auth │ ├── auth_test.dart │ ├── integration_test.dart │ ├── jwt_test.dart │ ├── token_verifier_test.dart │ └── user_test.dart │ ├── client │ ├── get_id_token.js │ └── package.json │ ├── credential_test.dart │ ├── firebase_admin_app_test.dart │ ├── google_cloud_firestore │ ├── collection_group_test.dart │ ├── collection_test.dart │ ├── document_test.dart │ ├── firestore_test.dart │ ├── query_test.dart │ ├── timestamp_test.dart │ ├── transaction_test.dart │ └── util │ │ └── helpers.dart │ ├── messaging │ └── messaging_test.dart │ ├── mock.dart │ └── security_rules │ └── security_rules_test.dart ├── pubspec.yaml └── scripts └── coverage.sh /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: There is a problem in how provider behaves 4 | title: "" 5 | labels: bug, needs triage 6 | assignees: 7 | - rrousselGit 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | 15 | 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: I have a problem and I need help 4 | url: https://github.com/rrousselGit/riverpod/discussions 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/example_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation improvement request 3 | about: >- 4 | Suggest a new example/documentation or ask for clarification about an 5 | existing one. 6 | title: "" 7 | labels: documentation, needs triage 8 | assignees: 9 | - rrousselGit 10 | --- 11 | 12 | **Describe what scenario you think is uncovered by the existing examples/articles** 13 | A clear and concise description of the problem that you want explained. 14 | 15 | **Describe why existing examples/articles do not cover this case** 16 | Explain which examples/articles you have seen before making this request, and 17 | why they did not help you with your problem. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the documentation request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: enhancement, needs triage 6 | assignees: 7 | - rrousselGit 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Related Issues 2 | 3 | fixes #your-issue-number 4 | 5 | 19 | 20 | ## Checklist 21 | 22 | Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (`[x]`). 23 | 24 | - [ ] I have updated the `CHANGELOG.md` of the relevant packages. 25 | Changelog files must be edited under the form: 26 | 27 | ```md 28 | ## Unreleased fix/major/minor 29 | 30 | - Description of your change. (thanks to @yourGithubId) 31 | ``` 32 | 33 | - [ ] If this contains new features or behavior changes, 34 | I have updated the documentation to match those changes. 35 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | - package-ecosystem: "pub" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "**.md" 7 | - "**.mdx" 8 | 9 | schedule: 10 | # runs the CI everyday at 10AM 11 | - cron: "0 10 * * *" 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | defaults: 18 | run: 19 | working-directory: packages/dart_firebase_admin 20 | 21 | steps: 22 | - uses: actions/checkout@v3.1.0 23 | with: 24 | fetch-depth: 2 25 | - uses: actions/setup-node@v4 26 | - uses: subosito/flutter-action@v2.7.1 27 | with: 28 | channel: master 29 | - name: Add pub cache bin to PATH 30 | run: echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH 31 | - name: Add pub cache to PATH 32 | run: echo "PUB_CACHE="$HOME/.pub-cache"" >> $GITHUB_ENV 33 | 34 | - name: Install firebase CLI 35 | run: npm install -g firebase-tools 36 | 37 | - name: Install dependencies 38 | run: dart pub get && cd example && dart pub get && cd - 39 | 40 | - name: Check format 41 | run: dart format --set-exit-if-changed . 42 | 43 | - name: Analyze 44 | run: dart analyze 45 | 46 | - run: mkdir $HOME/.config/gcloud -p 47 | - run: echo $CREDS > $HOME/.config/gcloud/application_default_credentials.json 48 | env: 49 | CREDS: ${{ secrets.CREDS }} 50 | 51 | - name: Run tests 52 | run: ${{github.workspace}}/scripts/coverage.sh 53 | 54 | - name: Upload coverage to codecov 55 | run: curl -s https://codecov.io/bash | bash 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | firebase-debug.log 2 | ui-debug.log 3 | firestore-debug.log 4 | 5 | node_modules 6 | packages/dart_firebase_admin/test/client/package-lock.json 7 | 8 | build 9 | coverage 10 | 11 | .DS_Store 12 | .atom/ 13 | .idea/ 14 | .vscode/* 15 | !.vscode/tasks.json 16 | !.vscode/settings.json 17 | 18 | .packages 19 | .pub/ 20 | .dart_tool/ 21 | pubspec.lock 22 | 23 | *.iml 24 | 25 | .flutter-plugins 26 | .flutter-plugins-dependencies 27 | 28 | .project 29 | .classpath 30 | .settings 31 | .last_build_id 32 | 33 | service-account.json 34 | 35 | **/pubspec_overrides.yaml 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Firebaseappcheck", 4 | "HAMC", 5 | "hmac", 6 | "jsonwebtoken", 7 | "Millis", 8 | "OIDC", 9 | "pbkdf", 10 | "responsetype", 11 | "rrggbb", 12 | "rrggbbaa", 13 | "SIGNIN", 14 | "subcollection", 15 | "subcollections", 16 | "totp", 17 | "Webpush", 18 | "webush" 19 | ] 20 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## 2024-05-06 7 | 8 | ### Changes 9 | 10 | --- 11 | 12 | Packages with breaking changes: 13 | 14 | - There are no breaking changes in this release. 15 | 16 | Packages with other changes: 17 | 18 | - [`dart_firebase_admin` - `v0.3.1`](#dart_firebase_admin---v031) 19 | 20 | --- 21 | 22 | #### `dart_firebase_admin` - `v0.3.1` 23 | 24 | - **FEAT**: Use GOOGLE_APPLICATION_CREDENTIALS if json value (#32). 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/dart_firebase_admin/README.md -------------------------------------------------------------------------------- /all_lint_rules.yaml: -------------------------------------------------------------------------------- 1 | linter: 2 | rules: 3 | - always_use_package_imports 4 | - avoid_dynamic_calls 5 | - avoid_empty_else 6 | - avoid_print 7 | - avoid_relative_lib_imports 8 | - avoid_returning_null_for_future 9 | - avoid_slow_async_io 10 | - avoid_type_to_string 11 | - avoid_types_as_parameter_names 12 | - avoid_web_libraries_in_flutter 13 | - cancel_subscriptions 14 | - close_sinks 15 | - collection_methods_unrelated_type 16 | - comment_references 17 | - control_flow_in_finally 18 | - deprecated_member_use_from_same_package 19 | - diagnostic_describe_all_properties 20 | - discarded_futures 21 | - empty_statements 22 | - hash_and_equals 23 | - implicit_reopen 24 | - invalid_case_patterns 25 | - iterable_contains_unrelated_type 26 | - list_remove_unrelated_type 27 | - literal_only_boolean_expressions 28 | - no_adjacent_strings_in_list 29 | - no_duplicate_case_values 30 | - no_logic_in_create_state 31 | - no_self_assignments 32 | - no_wildcard_variable_uses 33 | - prefer_relative_imports 34 | - prefer_void_to_null 35 | - test_types_in_equals 36 | - throw_in_finally 37 | - unnecessary_statements 38 | - unrelated_type_equality_checks 39 | - unsafe_html 40 | - use_build_context_synchronously 41 | - use_key_in_widget_constructors 42 | - valid_regexps 43 | - depend_on_referenced_packages 44 | - package_names 45 | - secure_pubspec_urls 46 | - sort_pub_dependencies 47 | - always_declare_return_types 48 | - always_put_control_body_on_new_line 49 | - always_put_required_named_parameters_first 50 | - always_require_non_null_named_parameters 51 | - always_specify_types 52 | - annotate_overrides 53 | - avoid_annotating_with_dynamic 54 | - avoid_bool_literals_in_conditional_expressions 55 | - avoid_catches_without_on_clauses 56 | - avoid_catching_errors 57 | - avoid_classes_with_only_static_members 58 | - avoid_double_and_int_checks 59 | - avoid_equals_and_hash_code_on_mutable_classes 60 | - avoid_escaping_inner_quotes 61 | - avoid_field_initializers_in_const_classes 62 | - avoid_final_parameters 63 | - avoid_function_literals_in_foreach_calls 64 | - avoid_implementing_value_types 65 | - avoid_init_to_null 66 | - avoid_js_rounded_ints 67 | - avoid_multiple_declarations_per_line 68 | - avoid_null_checks_in_equality_operators 69 | - avoid_positional_boolean_parameters 70 | - avoid_private_typedef_functions 71 | - avoid_redundant_argument_values 72 | - avoid_renaming_method_parameters 73 | - avoid_return_types_on_setters 74 | - avoid_returning_null 75 | - avoid_returning_null_for_void 76 | - avoid_returning_this 77 | - avoid_setters_without_getters 78 | - avoid_shadowing_type_parameters 79 | - avoid_single_cascade_in_expression_statements 80 | - avoid_types_on_closure_parameters 81 | - avoid_unnecessary_containers 82 | - avoid_unused_constructor_parameters 83 | - avoid_void_async 84 | - await_only_futures 85 | - camel_case_extensions 86 | - camel_case_types 87 | - cascade_invocations 88 | - cast_nullable_to_non_nullable 89 | - combinators_ordering 90 | - conditional_uri_does_not_exist 91 | - constant_identifier_names 92 | - curly_braces_in_flow_control_structures 93 | - dangling_library_doc_comments 94 | - deprecated_consistency 95 | - directives_ordering 96 | - do_not_use_environment 97 | - empty_catches 98 | - empty_constructor_bodies 99 | - eol_at_end_of_file 100 | - exhaustive_cases 101 | - file_names 102 | - flutter_style_todos 103 | - implementation_imports 104 | - implicit_call_tearoffs 105 | - join_return_with_assignment 106 | - leading_newlines_in_multiline_strings 107 | - library_annotations 108 | - library_names 109 | - library_prefixes 110 | - library_private_types_in_public_api 111 | - lines_longer_than_80_chars 112 | - matching_super_parameters 113 | - missing_whitespace_between_adjacent_strings 114 | - no_default_cases 115 | - no_leading_underscores_for_library_prefixes 116 | - no_leading_underscores_for_local_identifiers 117 | - no_literal_bool_comparisons 118 | - no_runtimeType_toString 119 | - non_constant_identifier_names 120 | - noop_primitive_operations 121 | - null_check_on_nullable_type_parameter 122 | - null_closures 123 | - omit_local_variable_types 124 | - one_member_abstracts 125 | - only_throw_errors 126 | - overridden_fields 127 | - package_api_docs 128 | - package_prefixed_library_names 129 | - parameter_assignments 130 | - prefer_adjacent_string_concatenation 131 | - prefer_asserts_in_initializer_lists 132 | - prefer_asserts_with_message 133 | - prefer_collection_literals 134 | - prefer_conditional_assignment 135 | - prefer_const_constructors 136 | - prefer_const_constructors_in_immutables 137 | - prefer_const_declarations 138 | - prefer_const_literals_to_create_immutables 139 | - prefer_constructors_over_static_methods 140 | - prefer_contains 141 | - prefer_double_quotes 142 | - prefer_expression_function_bodies 143 | - prefer_final_fields 144 | - prefer_final_in_for_each 145 | - prefer_final_locals 146 | - prefer_final_parameters 147 | - prefer_for_elements_to_map_fromIterable 148 | - prefer_foreach 149 | - prefer_function_declarations_over_variables 150 | - prefer_generic_function_type_aliases 151 | - prefer_if_elements_to_conditional_expressions 152 | - prefer_if_null_operators 153 | - prefer_initializing_formals 154 | - prefer_inlined_adds 155 | - prefer_int_literals 156 | - prefer_interpolation_to_compose_strings 157 | - prefer_is_empty 158 | - prefer_is_not_empty 159 | - prefer_is_not_operator 160 | - prefer_iterable_whereType 161 | - prefer_mixin 162 | - prefer_null_aware_method_calls 163 | - prefer_null_aware_operators 164 | - prefer_single_quotes 165 | - prefer_spread_collections 166 | - prefer_typing_uninitialized_variables 167 | - provide_deprecation_message 168 | - public_member_api_docs 169 | - recursive_getters 170 | - require_trailing_commas 171 | - sized_box_for_whitespace 172 | - sized_box_shrink_expand 173 | - slash_for_doc_comments 174 | - sort_child_properties_last 175 | - sort_constructors_first 176 | - sort_unnamed_constructors_first 177 | - tighten_type_of_initializing_formals 178 | - type_annotate_public_apis 179 | - type_init_formals 180 | - type_literal_in_constant_pattern 181 | - unawaited_futures 182 | - unnecessary_await_in_return 183 | - unnecessary_brace_in_string_interps 184 | - unnecessary_breaks 185 | - unnecessary_const 186 | - unnecessary_constructor_name 187 | - unnecessary_final 188 | - unnecessary_getters_setters 189 | - unnecessary_lambdas 190 | - unnecessary_late 191 | - unnecessary_library_directive 192 | - unnecessary_new 193 | - unnecessary_null_aware_assignments 194 | - unnecessary_null_aware_operator_on_extension_on_nullable 195 | - unnecessary_null_checks 196 | - unnecessary_null_in_if_null_operators 197 | - unnecessary_nullable_for_final_variable_declarations 198 | - unnecessary_overrides 199 | - unnecessary_parenthesis 200 | - unnecessary_raw_strings 201 | - unnecessary_string_escapes 202 | - unnecessary_string_interpolations 203 | - unnecessary_this 204 | - unnecessary_to_list_in_spreads 205 | - unreachable_from_main 206 | - use_colored_box 207 | - use_decorated_box 208 | - use_enums 209 | - use_full_hex_values_for_flutter_colors 210 | - use_function_type_syntax_for_parameters 211 | - use_if_null_to_convert_nulls_to_bools 212 | - use_is_even_rather_than_modulo 213 | - use_late_for_private_fields_and_variables 214 | - use_named_constants 215 | - use_raw_strings 216 | - use_rethrow_when_possible 217 | - use_setters_to_change_properties 218 | - use_string_buffers 219 | - use_string_in_part_of_directives 220 | - use_super_parameters 221 | - use_test_throws_matchers 222 | - use_to_and_as_if_applicable 223 | - void_checks -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: all_lint_rules.yaml 2 | analyzer: 3 | language: 4 | strict-casts: true 5 | strict-inference: true 6 | strict-raw-types: true 7 | errors: 8 | # Otherwise cause the import of all_lint_rules to warn because of some rules conflicts. 9 | # We explicitly enabled even conflicting rules and are fixing the conflict 10 | # in this file 11 | included_file_warning: ignore 12 | # false positive when using Freezed 13 | invalid_annotation_target: ignore 14 | 15 | linter: 16 | rules: 17 | public_member_api_docs: false 18 | 19 | # False positive for custom enum-like classes (such as Flutter's "Colors") 20 | avoid_classes_with_only_static_members: false 21 | 22 | # False positive when the future is returned by the function 23 | discarded_futures: false 24 | 25 | # Low value and lacks a quick fix 26 | combinators_ordering: false 27 | 28 | # Low value and high cost to change on all files 29 | eol_at_end_of_file: false 30 | 31 | # Conflicts with unused variables 32 | no_leading_underscores_for_local_identifiers: false 33 | 34 | # false positive 35 | one_member_abstracts: false 36 | 37 | # too verbose 38 | prefer_final_parameters: false 39 | 40 | # Too verbose with little value, and this is taken care of by the Flutter devtool anyway. 41 | diagnostic_describe_all_properties: false 42 | 43 | # Personal preference. I prefer "if (bool) return;" over having it in multiple lines 44 | always_put_control_body_on_new_line: false 45 | 46 | # Personal preference. I don't find it more readable 47 | cascade_invocations: false 48 | 49 | # Conflicts with `prefer_single_quotes` 50 | # Single quotes are easier to type and don't compromise on readability. 51 | prefer_double_quotes: false 52 | 53 | # Conflicts with `omit_local_variable_types` and other rules. 54 | # As per Dart guidelines, we want to avoid unnecessary types to make the code 55 | # more readable. 56 | # See https://dart.dev/guides/language/effective-dart/design#avoid-type-annotating-initialized-local-variables 57 | always_specify_types: false 58 | 59 | # Incompatible with `prefer_final_locals` 60 | # Having immutable local variables makes larger functions more predictable 61 | # so we will use `prefer_final_locals` instead. 62 | unnecessary_final: false 63 | 64 | # Not quite suitable for Flutter, which may have a `build` method with a single 65 | # return, but that return is still complex enough that a "body" is worth it. 66 | prefer_expression_function_bodies: false 67 | 68 | # Conflicts with the convention used by flutter, which puts `Key key` 69 | # and `@required Widget child` last. 70 | always_put_required_named_parameters_first: false 71 | 72 | # This project doesn't use Flutter-style todos 73 | flutter_style_todos: false 74 | 75 | # There are situations where we voluntarily want to catch everything, 76 | # especially as a library. 77 | avoid_catches_without_on_clauses: false 78 | 79 | # Boring as it sometimes force a line of 81 characters to be split in two. 80 | # As long as we try to respect that 80 characters limit, going slightly 81 | # above is fine. 82 | lines_longer_than_80_chars: false 83 | 84 | # Conflicts with disabling `implicit-dynamic` 85 | avoid_annotating_with_dynamic: false 86 | 87 | # conflicts with `prefer_relative_imports` 88 | always_use_package_imports: false 89 | 90 | # Disabled for now until we have NNBD as it otherwise conflicts with `missing_return` 91 | no_default_cases: false 92 | -------------------------------------------------------------------------------- /melos.yaml: -------------------------------------------------------------------------------- 1 | name: dart_firebase_admin 2 | 3 | packages: 4 | - "packages/**" -------------------------------------------------------------------------------- /packages/dart_firebase_admin/.pubignore: -------------------------------------------------------------------------------- 1 | ## Make dry-run happy due to fake private key 2 | test/credential_test.dart -------------------------------------------------------------------------------- /packages/dart_firebase_admin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.1 - 2025-03-21 2 | 3 | - Bump intl to `0.20.0` 4 | - Fixed `verifyIdToken` (thanks to @jtdLab) 5 | - Added Transaction support (thanks to @evandrobubiak) 6 | - Firebase Emulators now obtain port information from the environment ; if available (thanks to @dinko7) 7 | - Fix incorrect read of GOOGLE_APPLICATION_CREDENTIALS. It now correctly expects a file path instead of JSON 8 | - Added `AppCheck` and `SecurityRules` support 9 | 10 | ## 0.4.0 - 2024-09-11 11 | 12 | - Added `firestore.listCollections()` and `doc.listCollections()` 13 | - Fixes some errors incorrectly coming back as "unknown". 14 | - `Apns` parameters are no-longer required 15 | - Fixes argument error in FMC when sending booleans 16 | - Renamed various error codes to remove duplicates and removed 17 | unused codes. 18 | - Fixes crash when updating users (thanks to @HeySreelal) 19 | - Marked various classes that cannot be extended as base/final. 20 | - Added a default constructor on `Timestamp` (thanks to @KKimj) 21 | - Fixes the `Auth.verifyIdToken()` implementation by adding the 22 | token signature verification part. 23 | 24 | ## 0.3.1 25 | 26 | - **FEAT**: Use GOOGLE_APPLICATION_CREDENTIALS if json value (#32). 27 | 28 | ## 0.3.0 - 2024-01-02 29 | 30 | - **Breaking**: Removed the value `toJson` methods on objects. 31 | These were not intended to be public. 32 | - Added Firebase Messaging 33 | - Upgraded outdated dependencies 34 | 35 | ## 0.2.0 - 2023-11-30 36 | 37 | - Increased minimum Dart SDK to `3.2.0`. 38 | This fixes a compilation error due to `utf8.encode`. 39 | - Added `Credential.fromServiceAccountParams` (thanks to @akaboshinit) 40 | - Added `FirebaseAdminApp.close`, to close open connections and stop the SDK. 41 | - Fixed various typos 42 | - Added `Firestore.collectionGroup` support 43 | - Fix `Auth.getUserByEmail` parsing error. 44 | 45 | ## 0.1.0 - 2023-10-15 46 | 47 | Added Firebase Auth 48 | 49 | ## 0.0.2 50 | 51 | Fix 404 error when not using the emulator. 52 | 53 | ## 0.0.1 54 | 55 | Initial release 56 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/example/.gitignore: -------------------------------------------------------------------------------- 1 | android 2 | firebase.json 3 | .firebaserc 4 | firestore.indexes.json 5 | firestore.rules 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | firebase-debug.log* 14 | firebase-debug.*.log* 15 | 16 | # Firebase cache 17 | .firebase/ 18 | 19 | # Firebase config 20 | 21 | # Uncomment this if you'd like others to create their own Firebase project. 22 | # For a team working on the same Firebase project(s), it is recommended to leave 23 | # it commented so all members can deploy to the same project(s) in .firebaserc. 24 | # .firebaserc 25 | 26 | # Runtime data 27 | pids 28 | *.pid 29 | *.seed 30 | *.pid.lock 31 | 32 | # Directory for instrumented libs generated by jscoverage/JSCover 33 | lib-cov 34 | 35 | # Coverage directory used by tools like istanbul 36 | coverage 37 | 38 | # nyc test coverage 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # Bower dependency directory (https://bower.io/) 45 | bower_components 46 | 47 | # node-waf configuration 48 | .lock-wscript 49 | 50 | # Compiled binary addons (http://nodejs.org/api/addons.html) 51 | build/Release 52 | 53 | # Dependency directories 54 | node_modules/ 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: ../../../analysis_options.yaml 2 | linter: 3 | rules: 4 | public_member_api_docs: false 5 | avoid_print: false 6 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_firebase_admin/dart_firebase_admin.dart'; 2 | import 'package:dart_firebase_admin/firestore.dart'; 3 | import 'package:dart_firebase_admin/messaging.dart'; 4 | 5 | Future main() async { 6 | final admin = FirebaseAdminApp.initializeApp( 7 | 'dart-firebase-admin', 8 | Credential.fromApplicationDefaultCredentials(), 9 | ); 10 | 11 | // // admin.useEmulator(); 12 | 13 | final messaging = Messaging(admin); 14 | 15 | final result = await messaging.send( 16 | TokenMessage( 17 | token: 18 | 'e8Ap1n9UTQenyB-UEjNQt9:APA91bHhgc9RZYDcCKb7U1scQo1K0ZTSMItop8IqctrOcgvmN__oBo4vgbFX-ji4atr1PVw3Loug-eOCBmj4HVZjUE0aQBA0mGry7uL-7JuMaojhtl13MpvQtbZptvX_8f6vDcqei88O', 19 | notification: Notification( 20 | title: 'Hello', 21 | body: 'World', 22 | ), 23 | ), 24 | ); 25 | 26 | print(result); 27 | 28 | final firestore = Firestore(admin); 29 | 30 | final collection = firestore.collection('users'); 31 | 32 | await collection.doc('123').set({ 33 | 'name': 'John Doe', 34 | 'age': 30, 35 | }); 36 | 37 | final snapshot = await collection.get(); 38 | 39 | for (final doc in snapshot.docs) { 40 | print(doc.data()); 41 | } 42 | 43 | await admin.close(); 44 | } 45 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_firebase_admin_example 2 | publish_to: none 3 | 4 | environment: 5 | sdk: ">=3.0.0 <4.0.0" 6 | 7 | dependencies: 8 | dart_firebase_admin: 9 | path: ../ 10 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/example/storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | 3 | // Craft rules based on data in your Firestore database 4 | // allow write: if firestore.get( 5 | // /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin; 6 | service firebase.storage { 7 | match /b/{bucket}/o { 8 | match /{allPaths=**} { 9 | allow read, write: if false; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/app_check.dart: -------------------------------------------------------------------------------- 1 | export 'src/app_check/app_check.dart'; 2 | export 'src/app_check/app_check_api.dart'; 3 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/auth.dart: -------------------------------------------------------------------------------- 1 | export 'src/auth.dart' hide UserMetadataToJson; 2 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/dart_firebase_admin.dart: -------------------------------------------------------------------------------- 1 | export 'src/app.dart' hide envSymbol; 2 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/firestore.dart: -------------------------------------------------------------------------------- 1 | export 'src/google_cloud_firestore/firestore.dart' 2 | hide $SettingsCopyWith, ApiMapValue; 3 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/messaging.dart: -------------------------------------------------------------------------------- 1 | export 'src/messaging.dart' hide FirebaseMessagingRequestHandler; 2 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/security_rules.dart: -------------------------------------------------------------------------------- 1 | export 'src/security_rules/security_rules.dart'; 2 | export 'src/security_rules/security_rules_api_internals.dart' 3 | hide SecurityRulesApiClient; 4 | export 'src/security_rules/security_rules_internals.dart'; 5 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/app.dart: -------------------------------------------------------------------------------- 1 | library app; 2 | 3 | import 'dart:async'; 4 | import 'dart:convert'; 5 | import 'dart:io'; 6 | 7 | import 'package:googleapis/identitytoolkit/v3.dart' as auth3; 8 | import 'package:googleapis_auth/auth_io.dart' as auth; 9 | import 'package:googleapis_auth/googleapis_auth.dart'; 10 | import 'package:http/http.dart'; 11 | import 'package:meta/meta.dart'; 12 | 13 | part 'app/credential.dart'; 14 | part 'app/exception.dart'; 15 | part 'app/firebase_admin.dart'; 16 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/app/credential.dart: -------------------------------------------------------------------------------- 1 | part of '../app.dart'; 2 | 3 | @internal 4 | const envSymbol = #_envSymbol; 5 | 6 | class _RequestImpl extends BaseRequest { 7 | _RequestImpl(super.method, super.url, [Stream>? stream]) 8 | : _stream = stream ?? const Stream.empty(); 9 | 10 | final Stream> _stream; 11 | 12 | @override 13 | ByteStream finalize() { 14 | super.finalize(); 15 | return ByteStream(_stream); 16 | } 17 | } 18 | 19 | /// Will close the underlying `http.Client` depending on a constructor argument. 20 | class _EmulatorClient extends BaseClient { 21 | _EmulatorClient(this.client); 22 | 23 | final Client client; 24 | 25 | @override 26 | Future send(BaseRequest request) async { 27 | // Make new request object and perform the authenticated request. 28 | final modifiedRequest = _RequestImpl( 29 | request.method, 30 | request.url, 31 | request.finalize(), 32 | ); 33 | modifiedRequest.headers.addAll(request.headers); 34 | modifiedRequest.headers['Authorization'] = 'Bearer owner'; 35 | 36 | return client.send(modifiedRequest); 37 | } 38 | 39 | @override 40 | void close() { 41 | client.close(); 42 | super.close(); 43 | } 44 | } 45 | 46 | /// Authentication information for Firebase Admin SDK. 47 | class Credential { 48 | Credential._( 49 | this.serviceAccountCredentials, { 50 | this.serviceAccountId, 51 | }) : assert( 52 | serviceAccountId == null || serviceAccountCredentials == null, 53 | 'Cannot specify both serviceAccountId and serviceAccountCredentials', 54 | ); 55 | 56 | /// Log in to firebase from a service account file. 57 | factory Credential.fromServiceAccount(File serviceAccountFile) { 58 | final content = serviceAccountFile.readAsStringSync(); 59 | 60 | final json = jsonDecode(content); 61 | if (json is! Map) { 62 | throw const FormatException('Invalid service account file'); 63 | } 64 | 65 | final serviceAccountCredentials = 66 | auth.ServiceAccountCredentials.fromJson(json); 67 | 68 | return Credential._(serviceAccountCredentials); 69 | } 70 | 71 | /// Log in to firebase from a service account file parameters. 72 | factory Credential.fromServiceAccountParams({ 73 | required String clientId, 74 | required String privateKey, 75 | required String email, 76 | }) { 77 | final serviceAccountCredentials = auth.ServiceAccountCredentials( 78 | email, 79 | ClientId(clientId), 80 | privateKey, 81 | ); 82 | 83 | return Credential._(serviceAccountCredentials); 84 | } 85 | 86 | /// Log in to firebase using the environment variable. 87 | factory Credential.fromApplicationDefaultCredentials({ 88 | String? serviceAccountId, 89 | }) { 90 | ServiceAccountCredentials? creds; 91 | 92 | final env = 93 | Zone.current[envSymbol] as Map? ?? Platform.environment; 94 | final maybeConfig = env['GOOGLE_APPLICATION_CREDENTIALS']; 95 | if (maybeConfig != null && File(maybeConfig).existsSync()) { 96 | try { 97 | final text = File(maybeConfig).readAsStringSync(); 98 | final decodedValue = jsonDecode(text); 99 | if (decodedValue is Map) { 100 | creds = ServiceAccountCredentials.fromJson(decodedValue); 101 | } 102 | } on FormatException catch (_) {} 103 | } 104 | 105 | return Credential._( 106 | creds, 107 | serviceAccountId: serviceAccountId, 108 | ); 109 | } 110 | 111 | @internal 112 | final String? serviceAccountId; 113 | 114 | @internal 115 | final auth.ServiceAccountCredentials? serviceAccountCredentials; 116 | } 117 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/app/exception.dart: -------------------------------------------------------------------------------- 1 | part of '../app.dart'; 2 | 3 | /// Composite type which includes both a `FirebaseError` object and an index 4 | /// which can be used to get the errored item. 5 | class FirebaseArrayIndexError { 6 | FirebaseArrayIndexError({required this.index, required this.error}); 7 | 8 | /// The index of the errored item within the original array passed as part of the 9 | /// called API method. 10 | final int index; 11 | 12 | /// The error object. 13 | final FirebaseAdminException error; 14 | } 15 | 16 | /// A set of platform level error codes. 17 | /// 18 | /// See https://firebase.google.com/docs/reference/admin/error-handling#platform-error-codes 19 | /// for more information. 20 | String _platformErrorCodeMessage(String code) { 21 | switch (code) { 22 | case 'INVALID_ARGUMENT': 23 | return 'Client specified an invalid argument.'; 24 | case 'FAILED_PRECONDITION': 25 | return 'Request cannot be executed in the current system state, such as deleting a non-empty directory.'; 26 | case 'OUT_OF_RANGE': 27 | return 'Client specified an invalid range.'; 28 | case 'UNAUTHENTICATED': 29 | return 'Request not authenticated due to missing, invalid or expired OAuth token.'; 30 | case 'PERMISSION_DENIED': 31 | return 'Client does not have sufficient permission. This can happen because the OAuth token does not have the right scopes, the client does not have permission, or the API has not been enabled for the client project.'; 32 | case 'NOT_FOUND': 33 | return 'Specified resource not found, or the request is rejected due to undisclosed reasons such as whitelisting.'; 34 | case 'CONFLICT': 35 | return 'Concurrency conflict, such as read-modify-write conflict. Only used by a few legacy services. Most services use ABORTED or ALREADY_EXISTS instead of this. Refer to the service-specific documentation to see which one to handle in your code.'; 36 | case 'ABORTED': 37 | return 'Concurrency conflict, such as read-modify-write conflict.'; 38 | case 'ALREADY_EXISTS': 39 | return 'The resource that a client tried to create already exists.'; 40 | case 'RESOURCE_EXHAUSTED': 41 | return 'Either out of resource quota or reaching rate limiting.'; 42 | case 'CANCELLED': 43 | return 'Request cancelled by the client.'; 44 | case 'DATA_LOSS': 45 | return 'Unrecoverable data loss or data corruption. The client should report the error to the user.'; 46 | case 'INTERNAL': 47 | return 'Internal server error. Typically a server bug.'; 48 | case 'UNAVAILABLE': 49 | return 'Service unavailable. Typically the server is temporarily down. This error code is also assigned to local network errors (connection refused, no route to host).'; 50 | case 'DEADLINE_EXCEEDED': 51 | return 'Request deadline exceeded. This will happen only if the caller sets a deadline that is shorter than the target API’s default deadline (i.e. requested deadline is not enough for the server to process the request), and the request did not finish within the deadline.'; 52 | case 'UNKNOWN': 53 | default: 54 | return 'Unknown server error. Typically a server bug. This error code is also assigned to local response parsing (unmarshal) errors, and a wide range of other low-level I/O errors that are not easily diagnosable.'; 55 | } 56 | } 57 | 58 | /// Base interface for all Firebase Admin related errors. 59 | abstract class FirebaseAdminException implements Exception { 60 | FirebaseAdminException(this.service, this._code, [this._message]); 61 | 62 | final String service; 63 | final String _code; 64 | final String? _message; 65 | 66 | /// Error codes are strings using the following format: `"service/String-code"`. 67 | /// Some examples include `"auth/invalid-uid"` and 68 | /// `"messaging/invalid-recipient"`. 69 | /// 70 | /// While the message for a given error can change, the code will remain the same 71 | /// between backward-compatible versions of the Firebase SDK. 72 | String get code => '$service/${_code.replaceAll('_', '-').toLowerCase()}'; 73 | 74 | /// An explanatory message for the error that just occurred. 75 | /// 76 | /// This message is designed to be helpful to you, the developer. Because 77 | /// it generally does not convey meaningful information to end users, 78 | /// this message should not be displayed in your application. 79 | String get message => _message ?? _platformErrorCodeMessage(_code); 80 | 81 | @override 82 | String toString() { 83 | return '$runtimeType($code, $message)'; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/app/firebase_admin.dart: -------------------------------------------------------------------------------- 1 | part of '../app.dart'; 2 | 3 | class FirebaseAdminApp { 4 | FirebaseAdminApp.initializeApp( 5 | this.projectId, 6 | this.credential, { 7 | Client? client, 8 | }) : _clientOverride = client; 9 | 10 | /// The ID of the Google Cloud project associated with the app. 11 | final String projectId; 12 | 13 | /// The [Credential] used to authenticate the Admin SDK. 14 | final Credential credential; 15 | 16 | bool get isUsingEmulator => _isUsingEmulator; 17 | var _isUsingEmulator = false; 18 | 19 | @internal 20 | Uri authApiHost = Uri.https('identitytoolkit.googleapis.com', '/'); 21 | @internal 22 | Uri firestoreApiHost = Uri.https('firestore.googleapis.com', '/'); 23 | @internal 24 | String tasksEmulatorHost = 'https://cloudfunctions.googleapis.com/'; 25 | 26 | /// Use the Firebase Emulator Suite to run the app locally. 27 | void useEmulator() { 28 | _isUsingEmulator = true; 29 | final env = 30 | Zone.current[envSymbol] as Map? ?? Platform.environment; 31 | 32 | authApiHost = Uri.http( 33 | env['FIREBASE_AUTH_EMULATOR_HOST'] ?? '127.0.0.1:9099', 34 | 'identitytoolkit.googleapis.com/', 35 | ); 36 | firestoreApiHost = Uri.http( 37 | env['FIRESTORE_EMULATOR_HOST'] ?? '127.0.0.1:8080', 38 | '/', 39 | ); 40 | tasksEmulatorHost = Uri.http( 41 | env['CLOUD_TASKS_EMULATOR_HOST'] ?? '127.0.0.1:5001', 42 | '/', 43 | ).toString(); 44 | } 45 | 46 | @internal 47 | late final client = _getClient( 48 | [ 49 | auth3.IdentityToolkitApi.cloudPlatformScope, 50 | auth3.IdentityToolkitApi.firebaseScope, 51 | ], 52 | ); 53 | final Client? _clientOverride; 54 | 55 | Future _getClient(List scopes) async { 56 | if (_clientOverride != null) { 57 | return _clientOverride; 58 | } 59 | 60 | if (isUsingEmulator) { 61 | return _EmulatorClient(Client()); 62 | } 63 | 64 | final serviceAccountCredentials = credential.serviceAccountCredentials; 65 | final client = serviceAccountCredentials == null 66 | ? await auth.clientViaApplicationDefaultCredentials(scopes: scopes) 67 | : await auth.clientViaServiceAccount(serviceAccountCredentials, scopes); 68 | 69 | return client; 70 | } 71 | 72 | /// Stops the app and releases any resources associated with it. 73 | Future close() async { 74 | final client = await this.client; 75 | client.close(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/app_check/ap_check_api_internal.dart: -------------------------------------------------------------------------------- 1 | import 'package:googleapis/firebaseappcheck/v1.dart' as appcheck1; 2 | import 'package:googleapis_beta/firebaseappcheck/v1beta.dart' as appcheck1_beta; 3 | import 'package:meta/meta.dart'; 4 | 5 | import '../app.dart'; 6 | import '../utils/crypto_signer.dart'; 7 | import '../utils/jwt.dart'; 8 | import 'app_check_api.dart'; 9 | 10 | /// Class that facilitates sending requests to the Firebase App Check backend API. 11 | @internal 12 | class AppCheckApiClient { 13 | AppCheckApiClient(this.app); 14 | 15 | final FirebaseAdminApp app; 16 | 17 | Future _v1( 18 | Future Function(appcheck1.FirebaseappcheckApi client) fn, 19 | ) async { 20 | return fn(appcheck1.FirebaseappcheckApi(await app.client)); 21 | } 22 | 23 | Future _v1Beta( 24 | Future Function(appcheck1_beta.FirebaseappcheckApi client) fn, 25 | ) async { 26 | return fn(appcheck1_beta.FirebaseappcheckApi(await app.client)); 27 | } 28 | 29 | /// Exchange a signed custom token to App Check token 30 | /// 31 | /// [customToken] - The custom token to be exchanged. 32 | /// [appId] - The mobile App ID. 33 | /// 34 | /// Returns a future that fulfills with a [AppCheckToken]. 35 | Future exchangeToken(String customToken, String appId) { 36 | return _v1((client) async { 37 | final response = await client.projects.apps.exchangeCustomToken( 38 | appcheck1.GoogleFirebaseAppcheckV1ExchangeCustomTokenRequest( 39 | customToken: customToken, 40 | ), 41 | 'projects/${app.projectId}/apps/$appId', 42 | ); 43 | 44 | return AppCheckToken( 45 | token: response.token!, 46 | ttlMillis: _stringToMilliseconds(response.ttl!), 47 | ); 48 | }); 49 | } 50 | 51 | Future verifyReplayProtection(String token) { 52 | return _v1Beta((client) async { 53 | final response = await client.projects.verifyAppCheckToken( 54 | appcheck1_beta.GoogleFirebaseAppcheckV1betaVerifyAppCheckTokenRequest( 55 | appCheckToken: token, 56 | ), 57 | 'projects/${app.projectId}', 58 | ); 59 | 60 | return response.alreadyConsumed ?? false; 61 | }); 62 | } 63 | 64 | /// Converts a duration string with the suffix `s` to milliseconds. 65 | /// 66 | /// [duration] - The duration as a string with the suffix "s" preceded by the 67 | /// number of seconds, with fractional seconds. For example, 3 seconds with 0 nanoseconds 68 | /// is expressed as "3s", while 3 seconds and 1 nanosecond is expressed as "3.000000001s", 69 | /// and 3 seconds and 1 microsecond is expressed as "3.000001s". 70 | /// 71 | /// Returns the duration in milliseconds. 72 | int _stringToMilliseconds(String duration) { 73 | if (duration.isEmpty || !duration.endsWith('s')) { 74 | throw FirebaseAppCheckException( 75 | AppCheckErrorCode.invalidArgument, 76 | '`ttl` must be a valid duration string with the suffix `s`.', 77 | ); 78 | } 79 | 80 | final seconds = duration.substring(0, duration.length - 1); 81 | return (double.parse(seconds) * 1000).floor(); 82 | } 83 | } 84 | 85 | final appCheckErrorCodeMapping = { 86 | 'ABORTED': AppCheckErrorCode.aborted, 87 | 'INVALID_ARGUMENT': AppCheckErrorCode.invalidArgument, 88 | 'INVALID_CREDENTIAL': AppCheckErrorCode.invalidCredential, 89 | 'INTERNAL': AppCheckErrorCode.internalError, 90 | 'PERMISSION_DENIED': AppCheckErrorCode.permissionDenied, 91 | 'UNAUTHENTICATED': AppCheckErrorCode.unauthenticated, 92 | 'NOT_FOUND': AppCheckErrorCode.notFound, 93 | 'UNKNOWN': AppCheckErrorCode.unknownError, 94 | }; 95 | 96 | enum AppCheckErrorCode { 97 | aborted('aborted'), 98 | invalidArgument('invalid-argument'), 99 | invalidCredential('invalid-credential'), 100 | internalError('internal-error'), 101 | permissionDenied('permission-denied'), 102 | unauthenticated('unauthenticated'), 103 | notFound('not-found'), 104 | appCheckTokenExpired('app-check-token-expired'), 105 | unknownError('unknown-error'); 106 | 107 | const AppCheckErrorCode(this.code); 108 | 109 | static AppCheckErrorCode from(String code) { 110 | switch (code) { 111 | case CryptoSignerErrorCode.invalidCredential: 112 | return AppCheckErrorCode.invalidCredential; 113 | case CryptoSignerErrorCode.invalidArgument: 114 | return AppCheckErrorCode.invalidArgument; 115 | default: 116 | return AppCheckErrorCode.internalError; 117 | } 118 | } 119 | 120 | final String code; 121 | } 122 | 123 | /// Firebase App Check error code structure. This extends PrefixedFirebaseError. 124 | /// 125 | /// [code] - The error code. 126 | /// [message] - The error message. 127 | class FirebaseAppCheckException extends FirebaseAdminException { 128 | FirebaseAppCheckException(AppCheckErrorCode code, [String? _message]) 129 | : super('app-check', code.code, _message); 130 | 131 | factory FirebaseAppCheckException.fromJwtException(JwtException error) { 132 | if (error.code == JwtErrorCode.tokenExpired) { 133 | const errorMessage = 134 | 'The provided App Check token has expired. Get a fresh App Check token' 135 | ' from your client app and try again.'; 136 | return FirebaseAppCheckException( 137 | AppCheckErrorCode.appCheckTokenExpired, 138 | errorMessage, 139 | ); 140 | } else if (error.code == JwtErrorCode.invalidSignature) { 141 | const errorMessage = 142 | 'The provided App Check token has invalid signature.'; 143 | return FirebaseAppCheckException( 144 | AppCheckErrorCode.invalidArgument, 145 | errorMessage, 146 | ); 147 | } else if (error.code == JwtErrorCode.noMatchingKid) { 148 | const errorMessage = 149 | 'The provided App Check token has "kid" claim which does not ' 150 | 'correspond to a known public key. Most likely the provided App Check token ' 151 | 'is expired, so get a fresh token from your client app and try again.'; 152 | return FirebaseAppCheckException( 153 | AppCheckErrorCode.invalidArgument, 154 | errorMessage, 155 | ); 156 | } 157 | return FirebaseAppCheckException( 158 | AppCheckErrorCode.invalidArgument, 159 | error.message, 160 | ); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/app_check/app_check.dart: -------------------------------------------------------------------------------- 1 | import '../app.dart'; 2 | import '../utils/crypto_signer.dart'; 3 | import 'ap_check_api_internal.dart'; 4 | import 'app_check_api.dart'; 5 | import 'token_generator.dart'; 6 | import 'token_verifier.dart'; 7 | 8 | class AppCheck { 9 | AppCheck(this.app); 10 | 11 | final FirebaseAdminApp app; 12 | late final _tokenGenerator = 13 | AppCheckTokenGenerator(CryptoSigner.fromApp(app)); 14 | late final _client = AppCheckApiClient(app); 15 | late final _appCheckTokenVerifier = AppCheckTokenVerifier(app); 16 | 17 | /// Creates a new [AppCheckToken] that can be sent 18 | /// back to a client. 19 | /// 20 | /// [appId] - The app ID to use as the JWT app_id. 21 | /// [options] - Optional options object when creating a new App Check Token. 22 | /// 23 | /// Returns a future that fulfills with a [AppCheckToken]. 24 | Future createToken( 25 | String appId, [ 26 | AppCheckTokenOptions? options, 27 | ]) async { 28 | final customToken = await _tokenGenerator.createCustomToken(appId, options); 29 | 30 | return _client.exchangeToken(customToken, appId); 31 | } 32 | 33 | /// Verifies a Firebase App Check token (JWT). If the token is valid, the promise is 34 | /// fulfilled with the token's decoded claims; otherwise, the promise is 35 | /// rejected. 36 | /// 37 | /// @param appCheckToken - The App Check token to verify. 38 | /// @param options - Optional {@link VerifyAppCheckTokenOptions} object when verifying an App Check Token. 39 | /// 40 | /// @returns A promise fulfilled with the token's decoded claims 41 | /// if the App Check token is valid; otherwise, a rejected promise. 42 | Future verifyToken( 43 | String appCheckToken, [ 44 | VerifyAppCheckTokenOptions? options, 45 | ]) async { 46 | final decodedToken = 47 | await _appCheckTokenVerifier.verifyToken(appCheckToken); 48 | 49 | if (options?.consume ?? false) { 50 | final alreadyConsumed = 51 | await _client.verifyReplayProtection(appCheckToken); 52 | return VerifyAppCheckTokenResponse( 53 | alreadyConsumed: alreadyConsumed, 54 | appId: decodedToken.appId, 55 | token: decodedToken, 56 | ); 57 | } 58 | 59 | return VerifyAppCheckTokenResponse( 60 | alreadyConsumed: null, 61 | appId: decodedToken.appId, 62 | token: decodedToken, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/app_check/app_check_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'ap_check_api_internal.dart'; 4 | import 'app_check.dart'; 5 | 6 | class AppCheckToken { 7 | @internal 8 | AppCheckToken({required this.token, required this.ttlMillis}); 9 | 10 | /// The Firebase App Check token. 11 | final String token; 12 | 13 | /// The time-to-live duration of the token in milliseconds. 14 | final int ttlMillis; 15 | } 16 | 17 | class AppCheckTokenOptions { 18 | AppCheckTokenOptions({ 19 | this.ttlMillis, 20 | }) { 21 | if (ttlMillis case final ttlMillis?) { 22 | if (ttlMillis.inMinutes < 30 || ttlMillis.inDays > 7) { 23 | throw FirebaseAppCheckException( 24 | AppCheckErrorCode.invalidArgument, 25 | 'ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).', 26 | ); 27 | } 28 | } 29 | } 30 | 31 | /// The length of time, in milliseconds, for which the App Check token will 32 | /// be valid. This value must be between 30 minutes and 7 days, inclusive. 33 | final Duration? ttlMillis; 34 | } 35 | 36 | class VerifyAppCheckTokenOptions { 37 | /// To use the replay protection feature, set this to `true`. The [AppCheck.verifyToken] 38 | /// method will mark the token as consumed after verifying it. 39 | /// 40 | /// Tokens that are found to be already consumed will be marked as such in the response. 41 | /// 42 | /// Tokens are only considered to be consumed if it is sent to App Check backend by calling the 43 | /// [AppCheck.verifyToken] method with this field set to `true`; other uses of the token 44 | /// do not consume it. 45 | /// 46 | /// This replay protection feature requires an additional network call to the App Check backend 47 | /// and forces your clients to obtain a fresh attestation from your chosen attestation providers. 48 | /// This can therefore negatively impact performance and can potentially deplete your attestation 49 | /// providers' quotas faster. We recommend that you use this feature only for protecting 50 | /// low volume, security critical, or expensive operations. 51 | bool? consume; 52 | } 53 | 54 | class VerifyAppCheckTokenResponse { 55 | @internal 56 | VerifyAppCheckTokenResponse({ 57 | required this.appId, 58 | required this.token, 59 | required this.alreadyConsumed, 60 | }); 61 | 62 | /// The App ID corresponding to the App the App Check token belonged to. 63 | final String appId; 64 | 65 | /// The decoded Firebase App Check token. 66 | final DecodedAppCheckToken token; 67 | 68 | /// Indicates weather this token was already consumed. 69 | /// If this is the first time [AppCheck.verifyToken] method has seen this token, 70 | /// this field will contain the value `false`. The given token will then be 71 | /// marked as `already_consumed` for all future invocations of this [AppCheck.verifyToken] 72 | /// method for this token. 73 | /// 74 | /// When this field is `true`, the caller is attempting to reuse a previously consumed token. 75 | /// You should take precautions against such a caller; for example, you can take actions such as 76 | /// rejecting the request or ask the caller to pass additional layers of security checks. 77 | final bool? alreadyConsumed; 78 | } 79 | 80 | class DecodedAppCheckToken { 81 | DecodedAppCheckToken._({ 82 | required this.iss, 83 | required this.sub, 84 | required this.aud, 85 | required this.exp, 86 | required this.iat, 87 | required this.appId, 88 | }); 89 | 90 | DecodedAppCheckToken.fromMap(Map map) 91 | : this._( 92 | iss: map['iss'] as String, 93 | sub: map['sub'] as String, 94 | aud: (map['aud'] as List).cast(), 95 | exp: map['exp'] as int, 96 | iat: map['iat'] as int, 97 | appId: map['sub'] as String, 98 | ); 99 | 100 | /// The issuer identifier for the issuer of the response. 101 | /// This value is a URL with the format 102 | /// `https://firebaseappcheck.googleapis.com/`, where `` is the 103 | /// same project number specified in the [DecodedAppCheckToken.aud] property. 104 | final String iss; 105 | 106 | /// The Firebase App ID corresponding to the app the token belonged to. 107 | /// As a convenience, this value is copied over to the [appId] property. 108 | final String sub; 109 | 110 | /// The audience for which this token is intended. 111 | /// This value is a JSON array of two strings, the first is the project number of your 112 | /// Firebase project, and the second is the project ID of the same project. 113 | final List aud; 114 | 115 | /// The App Check token's expiration time, in seconds since the Unix epoch. That is, the 116 | /// time at which this App Check token expires and should no longer be considered valid. 117 | final int exp; 118 | 119 | /// The App Check token's issued-at time, in seconds since the Unix epoch. That is, the 120 | /// time at which this App Check token was issued and should start to be considered 121 | /// valid. 122 | final int iat; 123 | 124 | /// The App ID corresponding to the App the App Check token belonged to. 125 | /// This value is not actually one of the JWT token claims. It is added as a 126 | /// convenience, and is set as the value of the [DecodedAppCheckToken.sub] property. 127 | final String appId; 128 | } 129 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/app_check/token_generator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import '../utils/crypto_signer.dart'; 6 | import 'ap_check_api_internal.dart'; 7 | import 'app_check_api.dart'; 8 | 9 | // Audience to use for Firebase App Check Custom tokens 10 | const firebaseAppCheckAudience = 11 | 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService'; 12 | 13 | const oneMinuteInSeconds = 60; 14 | 15 | /// Class for generating Firebase App Check tokens. 16 | @internal 17 | class AppCheckTokenGenerator { 18 | AppCheckTokenGenerator(this.signer); 19 | 20 | final CryptoSigner signer; 21 | 22 | /// Creates a new custom token that can be exchanged to an App Check token. 23 | /// 24 | /// [appId] - The Application ID to use for the generated token. 25 | /// 26 | /// @returns A Promise fulfilled with a custom token signed with a service account key 27 | /// that can be exchanged to an App Check token. 28 | Future createCustomToken( 29 | String appId, [ 30 | AppCheckTokenOptions? options, 31 | ]) async { 32 | try { 33 | final account = await signer.getAccountId(); 34 | 35 | final header = { 36 | 'alg': signer.algorithm, 37 | 'typ': 'JWT', 38 | }; 39 | final iat = (DateTime.now().millisecondsSinceEpoch / 1000).floor(); 40 | final body = { 41 | 'iss': account, 42 | 'sub': account, 43 | 'app_id': appId, 44 | 'aud': firebaseAppCheckAudience, 45 | 'exp': iat + (oneMinuteInSeconds * 5), 46 | 'iat': iat, 47 | }; 48 | 49 | final token = '${_encodeSegment(header)}.${_encodeSegment(body)}'; 50 | 51 | final signature = await signer.sign(utf8.encode(token)); 52 | 53 | return '$token.${_encodeSegmentBuffer(signature)}'; 54 | } on CryptoSignerException catch (err) { 55 | throw _appCheckErrorFromCryptoSignerError(err); 56 | } 57 | } 58 | 59 | String _encodeSegment(Map segment) { 60 | return _encodeSegmentBuffer(utf8.encode(jsonEncode(segment))); 61 | } 62 | 63 | String _encodeSegmentBuffer(List buffer) { 64 | final base64 = _toWebSafeBase64(buffer); 65 | 66 | return base64.replaceAll(RegExp(r'=+$'), ''); 67 | } 68 | 69 | String _toWebSafeBase64(List data) { 70 | return base64Encode(data).replaceAll('/', '_').replaceAll('+', '-'); 71 | } 72 | } 73 | 74 | /// Creates a new `FirebaseAppCheckError` by extracting the error code, message and other relevant 75 | /// details from a `CryptoSignerError`. 76 | /// 77 | /// [err] - The Error to convert into a [FirebaseAppCheckException] error 78 | /// Returns a Firebase App Check error that can be returned to the user. 79 | FirebaseAppCheckException _appCheckErrorFromCryptoSignerError( 80 | CryptoSignerException err, 81 | ) { 82 | // TODO handle CryptoSignerException.cause 83 | 84 | return FirebaseAppCheckException( 85 | AppCheckErrorCode.from(err.code), 86 | err.message, 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/app_check/token_verifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../app.dart'; 4 | import '../utils/jwt.dart'; 5 | import 'ap_check_api_internal.dart'; 6 | import 'app_check_api.dart'; 7 | 8 | const appCheckIssuer = 'https://firebaseappcheck.googleapis.com/'; 9 | const jwksUrl = 'https://firebaseappcheck.googleapis.com/v1/jwks'; 10 | 11 | /// Class for verifying Firebase App Check tokens. 12 | /// 13 | @internal 14 | class AppCheckTokenVerifier { 15 | AppCheckTokenVerifier(this.app); 16 | 17 | final FirebaseAdminApp app; 18 | final _signatureVerifier = 19 | PublicKeySignatureVerifier.withJwksUrl(Uri.parse(jwksUrl)); 20 | 21 | Future verifyToken(String token) async { 22 | final decoded = await _decodeAndVerify(token, app.projectId); 23 | 24 | return DecodedAppCheckToken.fromMap(decoded.payload); 25 | } 26 | 27 | Future _decodeAndVerify(String token, String projectId) async { 28 | final decodedToken = await _safeDecode(token); 29 | 30 | _verifyContent(decodedToken, projectId); 31 | await _verifySignature(token); 32 | return decodedToken; 33 | } 34 | 35 | Future _safeDecode(String jwtToken) async { 36 | try { 37 | return await decodeJwt(jwtToken); 38 | } catch (err) { 39 | const errorMessage = 40 | 'Decoding App Check token failed. Make sure you passed ' 41 | 'the entire string JWT which represents the Firebase App Check token.'; 42 | throw FirebaseAppCheckException( 43 | AppCheckErrorCode.invalidArgument, 44 | errorMessage, 45 | ); 46 | } 47 | } 48 | 49 | /// Verifies the content of a Firebase App Check JWT. 50 | /// 51 | /// [fullDecodedToken] - The decoded JWT. 52 | /// [projectId] - The Firebase Project Id. 53 | void _verifyContent(DecodedToken fullDecodedToken, String? projectId) { 54 | final header = fullDecodedToken.header; 55 | final payload = fullDecodedToken.payload; 56 | 57 | const projectIdMatchMessage = 58 | ' Make sure the App Check token comes from the same ' 59 | 'Firebase project as the service account used to authenticate this SDK.'; 60 | final scopedProjectId = 'projects/$projectId'; 61 | 62 | String? errorMessage; 63 | if (header['alg'] case final alg && != algorithmRS256) { 64 | errorMessage = 65 | 'The provided App Check token has incorrect algorithm. Expected "$algorithmRS256" but got "$alg".'; 66 | } else if (payload['aud'] case final List aud 67 | when !aud.contains(scopedProjectId)) { 68 | errorMessage = 69 | 'The provided App Check token has incorrect "aud" (audience) claim. Expected "$scopedProjectId" but got "$aud".$projectIdMatchMessage'; 70 | } else if (payload['iss'] case final iss 71 | when iss is! String || !iss.startsWith(appCheckIssuer)) { 72 | errorMessage = 73 | 'The provided App Check token has incorrect "iss" (issuer) claim.'; 74 | } else if (payload['sub'] case final sub when sub is! String) { 75 | errorMessage = 76 | 'The provided App Check token has no "sub" (subject) claim.'; 77 | } else if (payload['sub'] == '') { 78 | errorMessage = 79 | 'The provided App Check token has an empty string "sub" (subject) claim.'; 80 | } 81 | 82 | if (errorMessage != null) { 83 | throw FirebaseAppCheckException( 84 | AppCheckErrorCode.invalidArgument, 85 | errorMessage, 86 | ); 87 | } 88 | } 89 | 90 | Future _verifySignature(String jwtToken) async { 91 | try { 92 | await _signatureVerifier.verify(jwtToken); 93 | } on JwtException catch (error, stack) { 94 | Error.throwWithStackTrace( 95 | FirebaseAppCheckException.fromJwtException(error), 96 | stack, 97 | ); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/auth.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:collection/collection.dart'; 5 | import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart' as dart_jsonwebtoken; 6 | import 'package:googleapis/identitytoolkit/v1.dart' as auth1; 7 | import 'package:googleapis/identitytoolkit/v1.dart' as v1; 8 | import 'package:googleapis/identitytoolkit/v2.dart' as auth2; 9 | import 'package:googleapis/identitytoolkit/v2.dart' as v2; 10 | import 'package:googleapis/identitytoolkit/v3.dart' as auth3; 11 | import 'package:http/http.dart'; 12 | import 'package:meta/meta.dart'; 13 | 14 | import 'app.dart'; 15 | import 'object_utils.dart'; 16 | import 'utils/crypto_signer.dart'; 17 | import 'utils/jwt.dart'; 18 | import 'utils/utils.dart'; 19 | import 'utils/validator.dart'; 20 | 21 | part 'auth/action_code_settings_builder.dart'; 22 | part 'auth/auth.dart'; 23 | part 'auth/auth_api_request.dart'; 24 | part 'auth/auth_config.dart'; 25 | part 'auth/auth_exception.dart'; 26 | part 'auth/base_auth.dart'; 27 | part 'auth/identifier.dart'; 28 | part 'auth/token_generator.dart'; 29 | part 'auth/token_verifier.dart'; 30 | part 'auth/user.dart'; 31 | part 'auth/user_import_builder.dart'; 32 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/auth/action_code_settings_builder.dart: -------------------------------------------------------------------------------- 1 | part of '../auth.dart'; 2 | 3 | class ActionCodeSettingsIos { 4 | ActionCodeSettingsIos(this.bundleId); 5 | 6 | /// Defines the required iOS bundle ID of the app where the link should be 7 | /// handled if the application is already installed on the device. 8 | final String bundleId; 9 | } 10 | 11 | class ActionCodeSettingsAndroid { 12 | ActionCodeSettingsAndroid({ 13 | required this.packageName, 14 | this.installApp, 15 | this.minimumVersion, 16 | }); 17 | 18 | /// Defines the required Android package name of the app where the link should be 19 | /// handled if the Android app is installed. 20 | final String packageName; 21 | 22 | /// Whether to install the Android app if the device supports it and the app is 23 | /// not already installed. 24 | final bool? installApp; 25 | 26 | /// The Android minimum version if available. If the installed app is an older 27 | /// version, the user is taken to the GOogle Play Store to upgrade the app. 28 | final String? minimumVersion; 29 | } 30 | 31 | /// This is the interface that defines the required continue/state URL with 32 | /// optional Android and iOS bundle identifiers. 33 | class ActionCodeSettings { 34 | ActionCodeSettings({ 35 | required this.url, 36 | this.handleCodeInApp, 37 | this.iOS, 38 | this.android, 39 | this.dynamicLinkDomain, 40 | }); 41 | 42 | /// Defines the link continue/state URL, which has different meanings in 43 | /// different contexts: 44 | ///
    45 | ///
  • When the link is handled in the web action widgets, this is the deep 46 | /// link in the `continueUrl` query parameter.
  • 47 | ///
  • When the link is handled in the app directly, this is the `continueUrl` 48 | /// query parameter in the deep link of the Dynamic Link.
  • 49 | ///
50 | final String url; 51 | 52 | /// Whether to open the link via a mobile app or a browser. 53 | /// The default is false. When set to true, the action code link is sent 54 | /// as a Universal Link or Android App Link and is opened by the app if 55 | /// installed. In the false case, the code is sent to the web widget first 56 | /// and then redirects to the app if installed. 57 | final bool? handleCodeInApp; 58 | 59 | /// Defines the iOS bundle ID. This will try to open the link in an iOS app if it 60 | /// is installed. 61 | final ActionCodeSettingsIos? iOS; 62 | 63 | /// Defines the Android package name. This will try to open the link in an 64 | /// android app if it is installed. If `installApp` is passed, it specifies 65 | /// whether to install the Android app if the device supports it and the app is 66 | /// not already installed. If this field is provided without a `packageName`, an 67 | /// error is thrown explaining that the `packageName` must be provided in 68 | /// conjunction with this field. If `minimumVersion` is specified, and an older 69 | /// version of the app is installed, the user is taken to the Play Store to 70 | /// upgrade the app. 71 | final ActionCodeSettingsAndroid? android; 72 | 73 | /// Defines the dynamic link domain to use for the current link if it is to be 74 | /// opened using Firebase Dynamic Links, as multiple dynamic link domains can be 75 | /// configured per project. This field provides the ability to explicitly choose 76 | /// configured per project. This fields provides the ability explicitly choose 77 | /// one. If none is provided, the oldest domain is used by default. 78 | final String? dynamicLinkDomain; 79 | } 80 | 81 | class _ActionCodeSettingsBuilder { 82 | _ActionCodeSettingsBuilder(ActionCodeSettings actionCodeSettings) 83 | : _continueUrl = actionCodeSettings.url, 84 | _canHandleCodeInApp = actionCodeSettings.handleCodeInApp ?? false, 85 | _dynamicLinkDomain = actionCodeSettings.dynamicLinkDomain, 86 | _ibi = actionCodeSettings.iOS?.bundleId, 87 | _apn = actionCodeSettings.android?.packageName, 88 | _amv = actionCodeSettings.android?.minimumVersion, 89 | _installApp = actionCodeSettings.android?.installApp ?? false { 90 | if (Uri.tryParse(actionCodeSettings.url) == null) { 91 | throw FirebaseAuthAdminException(AuthClientErrorCode.invalidContinueUri); 92 | } 93 | 94 | final dynamicLinkDomain = actionCodeSettings.dynamicLinkDomain; 95 | if (dynamicLinkDomain != null && dynamicLinkDomain.isEmpty) { 96 | throw FirebaseAuthAdminException( 97 | AuthClientErrorCode.invalidDynamicLinkDomain, 98 | ); 99 | } 100 | 101 | final ios = actionCodeSettings.iOS; 102 | if (ios != null) { 103 | if (ios.bundleId.isEmpty) { 104 | throw FirebaseAuthAdminException( 105 | AuthClientErrorCode.invalidArgument, 106 | '"ActionCodeSettings.iOS.bundleId" must be a valid non-empty string.', 107 | ); 108 | } 109 | } 110 | 111 | final android = actionCodeSettings.android; 112 | if (android != null) { 113 | if (android.packageName.isEmpty) { 114 | throw FirebaseAuthAdminException( 115 | AuthClientErrorCode.invalidArgument, 116 | '"ActionCodeSettings.android.packageName" must be a valid non-empty string.', 117 | ); 118 | } 119 | final minimumVersion = android.minimumVersion; 120 | if (minimumVersion != null && minimumVersion.isEmpty) { 121 | throw FirebaseAuthAdminException( 122 | AuthClientErrorCode.invalidArgument, 123 | '"ActionCodeSettings.android.minimumVersion" must be a valid non-empty string.', 124 | ); 125 | } 126 | } 127 | } 128 | 129 | final String _continueUrl; 130 | final String? _apn; 131 | final String? _amv; 132 | final bool _installApp; 133 | final String? _ibi; 134 | final bool _canHandleCodeInApp; 135 | final String? _dynamicLinkDomain; 136 | 137 | void buildRequest( 138 | auth1.GoogleCloudIdentitytoolkitV1GetOobCodeRequest request, 139 | ) { 140 | request.continueUrl = _continueUrl; 141 | request.canHandleCodeInApp = _canHandleCodeInApp; 142 | request.dynamicLinkDomain = _dynamicLinkDomain; 143 | request.androidPackageName = _apn; 144 | request.androidMinimumVersion = _amv; 145 | request.androidInstallApp = _installApp; 146 | request.iOSBundleId = _ibi; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/auth/auth.dart: -------------------------------------------------------------------------------- 1 | part of '../auth.dart'; 2 | 3 | /// Auth service bound to the provided app. 4 | /// An Auth instance can have multiple tenants. 5 | class Auth extends _BaseAuth { 6 | Auth(FirebaseAdminApp app) 7 | : super( 8 | app: app, 9 | authRequestHandler: _AuthRequestHandler(app), 10 | ); 11 | 12 | // TODO tenantManager 13 | // TODO projectConfigManager 14 | } 15 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/auth/identifier.dart: -------------------------------------------------------------------------------- 1 | part of '../auth.dart'; 2 | 3 | /// Identifies a user to be looked up. 4 | /// 5 | /// See also: 6 | /// - [ProviderIdentifier] 7 | /// - [PhoneIdentifier] 8 | /// - [EmailIdentifier] 9 | /// - [UidIdentifier] 10 | sealed class UserIdentifier {} 11 | 12 | /// Used for looking up an account by federated provider. 13 | /// 14 | /// See [_BaseAuth.getUsers]. 15 | class ProviderIdentifier extends UserIdentifier { 16 | ProviderIdentifier({required this.providerId, required this.providerUid}) { 17 | if (providerId.isEmpty) { 18 | throw FirebaseAuthAdminException(AuthClientErrorCode.invalidProviderId); 19 | } 20 | if (providerUid.isEmpty) { 21 | throw FirebaseAuthAdminException(AuthClientErrorCode.invalidProviderUid); 22 | } 23 | } 24 | 25 | final String providerId; 26 | final String providerUid; 27 | } 28 | 29 | /// Used for looking up an account by phone number. 30 | /// 31 | /// See [_BaseAuth.getUsers]. 32 | class PhoneIdentifier extends UserIdentifier { 33 | PhoneIdentifier({required this.phoneNumber}) { 34 | assertIsPhoneNumber(phoneNumber); 35 | } 36 | 37 | final String phoneNumber; 38 | } 39 | 40 | /// Used for looking up an account by email. 41 | /// 42 | /// See [_BaseAuth.getUsers]. 43 | class EmailIdentifier extends UserIdentifier { 44 | EmailIdentifier({required this.email}) { 45 | assertIsEmail(email); 46 | } 47 | 48 | final String email; 49 | } 50 | 51 | /// Used for looking up an account by uid. 52 | /// 53 | /// See [_BaseAuth.getUsers]. 54 | class UidIdentifier extends UserIdentifier { 55 | UidIdentifier({required this.uid}) { 56 | assertIsUid(uid); 57 | } 58 | 59 | final String uid; 60 | } 61 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/auth/token_generator.dart: -------------------------------------------------------------------------------- 1 | part of '../auth.dart'; 2 | 3 | const _oneHourInSeconds = 60 * 60; 4 | 5 | // Audience to use for Firebase Auth Custom tokens 6 | const _firebaseAudience = 7 | 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; 8 | 9 | // List of blacklisted claims which cannot be provided when creating a custom token 10 | const _blacklistedClaims = [ 11 | 'acr', 12 | 'amr', 13 | 'at_hash', 14 | 'aud', 15 | 'auth_time', 16 | 'azp', 17 | 'cnf', 18 | 'c_hash', 19 | 'exp', 20 | 'iat', 21 | 'iss', 22 | 'jti', 23 | 'nbf', 24 | 'nonce', 25 | ]; 26 | 27 | class _FirebaseTokenGenerator { 28 | _FirebaseTokenGenerator( 29 | this._signer, { 30 | required this.tenantId, 31 | }) { 32 | final tenantId = this.tenantId; 33 | if (tenantId != null && tenantId.isEmpty) { 34 | throw FirebaseAuthAdminException( 35 | AuthClientErrorCode.invalidArgument, 36 | '`tenantId` argument must be a non-empty string.', 37 | ); 38 | } 39 | } 40 | 41 | final CryptoSigner _signer; 42 | final String? tenantId; 43 | 44 | /// Creates a new Firebase Auth Custom token. 45 | Future createCustomToken( 46 | String uid, { 47 | Map? developerClaims, 48 | }) async { 49 | String? errorMessage; 50 | if (uid.isEmpty) { 51 | errorMessage = '`uid` argument must be a non-empty string uid.'; 52 | } else if (uid.length > 128) { 53 | errorMessage = '`uid` argument must not be longer than 128 characters.'; 54 | } 55 | 56 | if (errorMessage != null) { 57 | throw FirebaseAuthAdminException( 58 | AuthClientErrorCode.invalidArgument, 59 | errorMessage, 60 | ); 61 | } 62 | 63 | final claims = {...?developerClaims}; 64 | if (developerClaims != null) { 65 | for (final key in developerClaims.keys) { 66 | if (_blacklistedClaims.contains(key)) { 67 | throw FirebaseAuthAdminException( 68 | AuthClientErrorCode.invalidArgument, 69 | 'Developer claim "$key" is reserved and cannot be specified.', 70 | ); 71 | } 72 | } 73 | } 74 | 75 | try { 76 | final account = await _signer.getAccountId(); 77 | 78 | final header = { 79 | 'alg': _signer.algorithm, 80 | 'typ': 'JWT', 81 | }; 82 | final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000; 83 | final body = { 84 | 'aud': _firebaseAudience, 85 | 'iat': iat, 86 | 'exp': iat + _oneHourInSeconds, 87 | 'iss': account, 88 | 'sub': account, 89 | 'uid': uid, 90 | if (tenantId case final tenantId?) 'tenant_id': tenantId, 91 | if (claims.isNotEmpty) 'claims': claims, 92 | }; 93 | 94 | final token = '${_encodeSegment(header)}.${_encodeSegment(body)}'; 95 | final signPromise = await _signer.sign(utf8.encode(token)); 96 | 97 | return '$token.${_encodeSegment(signPromise)}'; 98 | } on CryptoSignerException catch (err, stack) { 99 | Error.throwWithStackTrace(_handleCryptoSignerError(err), stack); 100 | } 101 | } 102 | 103 | String _encodeSegment(Object? segment) { 104 | final buffer = 105 | segment is Uint8List ? segment : utf8.encode(jsonEncode(segment)); 106 | return base64Encode(buffer).replaceFirst(RegExp(r'=+$'), ''); 107 | } 108 | } 109 | 110 | /// Creates a new FirebaseAuthError by extracting the error code, message and other relevant 111 | /// details from a CryptoSignerError. 112 | Object _handleCryptoSignerError(CryptoSignerException err) { 113 | return FirebaseAuthAdminException( 114 | _mapToAuthClientErrorCode(err.code), 115 | err.message, 116 | ); 117 | } 118 | 119 | AuthClientErrorCode _mapToAuthClientErrorCode(String code) { 120 | switch (code) { 121 | case CryptoSignerErrorCode.invalidCredential: 122 | return AuthClientErrorCode.invalidCredential; 123 | case CryptoSignerErrorCode.invalidArgument: 124 | return AuthClientErrorCode.invalidArgument; 125 | default: 126 | return AuthClientErrorCode.internalError; 127 | } 128 | } 129 | 130 | /// A CryptoSigner implementation that is used when communicating with the Auth emulator. 131 | /// It produces unsigned tokens. 132 | class _EmulatedSigner implements CryptoSigner { 133 | @override 134 | String get algorithm => 'none'; 135 | 136 | @override 137 | Future sign(Uint8List buffer) async => utf8.encode(''); 138 | 139 | @override 140 | Future getAccountId() async => 'firebase-auth-emulator@example.com'; 141 | } 142 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/backoff.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | 4 | import 'package:meta/meta.dart'; 5 | 6 | @internal 7 | class ExponentialBackoffSetting { 8 | const ExponentialBackoffSetting({ 9 | this.initialDelayMs, 10 | this.backoffFactor, 11 | this.maxDelayMs, 12 | this.jitterFactor, 13 | }); 14 | 15 | final int? initialDelayMs; 16 | final double? backoffFactor; 17 | final int? maxDelayMs; 18 | final double? jitterFactor; 19 | } 20 | 21 | /// A helper for running delayed tasks following an exponential backoff curve 22 | /// between attempts. 23 | /// 24 | /// Each delay is made up of a "base" delay which follows the exponential 25 | /// backoff curve, and a "jitter" (+/- 50% by default) that is calculated and 26 | /// added to the base delay. This prevents clients from accidentally 27 | /// synchronizing their delays causing spikes of load to the backend. 28 | /// 29 | @internal 30 | class ExponentialBackoff { 31 | ExponentialBackoff({ 32 | ExponentialBackoffSetting options = const ExponentialBackoffSetting(), 33 | }) : initialDelayMs = options.initialDelayMs ?? defaultBackOffInitialDelayMs, 34 | backoffFactor = options.backoffFactor ?? defaultBackOffFactor, 35 | maxDelayMs = options.maxDelayMs ?? defaultBackOffMaxDelayMs, 36 | jitterFactor = options.jitterFactor ?? defaultJitterFactor; 37 | 38 | static const defaultBackOffInitialDelayMs = 1000; 39 | static const defaultBackOffFactor = 1.5; 40 | static const defaultBackOffMaxDelayMs = 60 * 1000; 41 | static const defaultJitterFactor = 1.0; 42 | 43 | static const maxRetryAttempts = 10; 44 | 45 | final int initialDelayMs; 46 | final double backoffFactor; 47 | final int maxDelayMs; 48 | final double jitterFactor; 49 | 50 | int _retryCount = 0; 51 | int _currentBaseMs = 0; 52 | bool _awaitingBackoffCompletion = false; 53 | 54 | /// Returns a future that resolves after currentDelayMs, and increases the 55 | /// delay for any subsequent attempts. 56 | /// 57 | /// @return A [Future] that resolves when the current delay elapsed. 58 | Future backoffAndWait() async { 59 | if (_awaitingBackoffCompletion) { 60 | throw Exception('A backoff operation is already in progress.'); 61 | } 62 | 63 | if (_retryCount > maxRetryAttempts) { 64 | throw Exception('Exceeded maximum number of retries allowed.'); 65 | } 66 | 67 | final delayWithJitterMs = _currentBaseMs + _jitterDelayMs(); 68 | 69 | _currentBaseMs = (_currentBaseMs * backoffFactor).toInt(); 70 | _currentBaseMs = _currentBaseMs.clamp(initialDelayMs, maxDelayMs); 71 | _retryCount += 1; 72 | 73 | await Future.delayed(Duration(milliseconds: delayWithJitterMs)); 74 | _awaitingBackoffCompletion = false; 75 | } 76 | 77 | /// Resets the backoff delay and retry count. 78 | /// 79 | /// The very next [backoffAndWait] will have no delay. If it is called again 80 | /// (i.e. due to an error), [initialDelayMs] (plus jitter) will be used, and 81 | /// subsequent ones will increase according to the [backoffFactor]. 82 | void reset() { 83 | _retryCount = 0; 84 | _currentBaseMs = 0; 85 | } 86 | 87 | /// Resets the backoff delay to the maximum delay (e.g. for use after a 88 | /// RESOURCE_EXHAUSTED error). 89 | void resetToMax() { 90 | _currentBaseMs = maxDelayMs; 91 | } 92 | 93 | int _jitterDelayMs() { 94 | return ((Random().nextDouble() - 0.5) * jitterFactor * _currentBaseMs) 95 | .toInt(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/collection_group.dart: -------------------------------------------------------------------------------- 1 | part of 'firestore.dart'; 2 | 3 | final class CollectionGroup extends Query { 4 | CollectionGroup._( 5 | String collectionId, { 6 | required super.firestore, 7 | required _FirestoreDataConverter converter, 8 | }) : super._( 9 | queryOptions: 10 | _QueryOptions.forCollectionGroupQuery(collectionId, converter), 11 | ); 12 | 13 | @override 14 | CollectionGroup withConverter({ 15 | required FromFirestore fromFirestore, 16 | required ToFirestore toFirestore, 17 | }) { 18 | return CollectionGroup._( 19 | _queryOptions.collectionId, 20 | firestore: firestore, 21 | converter: ( 22 | fromFirestore: fromFirestore, 23 | toFirestore: toFirestore, 24 | ), 25 | ); 26 | } 27 | 28 | @override 29 | // ignore: hash_and_equals, already implemented by Query 30 | bool operator ==(Object other) { 31 | return super == other && other is CollectionGroup; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/convert.dart: -------------------------------------------------------------------------------- 1 | part of 'firestore.dart'; 2 | 3 | /// Verifies that a `Value` only has a single type set. 4 | void _assertValidProtobufValue(firestore1.Value proto) { 5 | final values = [ 6 | proto.booleanValue, 7 | proto.doubleValue, 8 | proto.integerValue, 9 | proto.stringValue, 10 | proto.timestampValue, 11 | proto.nullValue, 12 | proto.mapValue, 13 | proto.arrayValue, 14 | proto.referenceValue, 15 | proto.geoPointValue, 16 | proto.bytesValue, 17 | ]; 18 | 19 | if (values.nonNulls.length != 1) { 20 | throw ArgumentError.value( 21 | proto, 22 | 'proto', 23 | 'Unable to infer type value', 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/document_change.dart: -------------------------------------------------------------------------------- 1 | part of 'firestore.dart'; 2 | 3 | enum DocumentChangeType { 4 | added, 5 | removed, 6 | modified, 7 | } 8 | 9 | /// A DocumentChange represents a change to the documents matching a query. 10 | /// It contains the document affected and the type of change that occurred. 11 | @immutable 12 | class DocumentChange { 13 | const DocumentChange._({ 14 | required this.oldIndex, 15 | required this.newIndex, 16 | required this.doc, 17 | required this.type, 18 | }); 19 | 20 | /// The index of the changed document in the result set immediately prior to 21 | /// this DocumentChange (i.e. supposing that all prior DocumentChange objects 22 | /// have been applied). Is -1 for 'added' events. 23 | final int oldIndex; 24 | 25 | /// The index of the changed document in the result set immediately after 26 | /// this DocumentChange (i.e. supposing that all prior DocumentChange 27 | /// objects and the current DocumentChange object have been applied). 28 | /// Is -1 for 'removed' events. 29 | final int newIndex; 30 | 31 | /// The document affected by this change. 32 | final QueryDocumentSnapshot doc; 33 | 34 | /// The type of change ('added', 'modified', or 'removed'). 35 | final DocumentChangeType type; 36 | 37 | @override 38 | bool operator ==(Object other) { 39 | return identical(this, other) || 40 | other is DocumentChange && 41 | runtimeType == other.runtimeType && 42 | oldIndex == other.oldIndex && 43 | newIndex == other.newIndex && 44 | doc == other.doc && 45 | type == other.type; 46 | } 47 | 48 | @override 49 | int get hashCode => Object.hash(runtimeType, oldIndex, newIndex, doc, type); 50 | } 51 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/document_reader.dart: -------------------------------------------------------------------------------- 1 | part of 'firestore.dart'; 2 | 3 | class _BatchGetResponse { 4 | _BatchGetResponse(this.result, this.transaction); 5 | 6 | List> result; 7 | String? transaction; 8 | } 9 | 10 | class _DocumentReader { 11 | _DocumentReader({ 12 | required this.firestore, 13 | required this.documents, 14 | required this.fieldMask, 15 | this.transactionId, 16 | this.readTime, 17 | this.transactionOptions, 18 | }) : _outstandingDocuments = documents.map((e) => e._formattedName).toSet(), 19 | assert( 20 | [transactionId, readTime, transactionOptions].nonNulls.length <= 1, 21 | 'Only transactionId or readTime or transactionOptions must be provided. transactionId = $transactionId, readTime = $readTime, transactionOptions = $transactionOptions', 22 | ); 23 | 24 | String? _retrievedTransactionId; 25 | final Firestore firestore; 26 | final List> documents; 27 | final List? fieldMask; 28 | final String? transactionId; 29 | final Timestamp? readTime; 30 | final firestore1.TransactionOptions? transactionOptions; 31 | final Set _outstandingDocuments; 32 | final _retreivedDocuments = >{}; 33 | 34 | /// Invokes the BatchGetDocuments RPC and returns the results. 35 | Future>> get() async { 36 | return _get().then((value) => value.result); 37 | } 38 | 39 | Future<_BatchGetResponse> _get() async { 40 | await _fetchDocuments(); 41 | 42 | // BatchGetDocuments doesn't preserve document order. We use the request 43 | // order to sort the resulting documents. 44 | final orderedDocuments = >[]; 45 | 46 | for (final docRef in documents) { 47 | final document = _retreivedDocuments[docRef._formattedName]; 48 | if (document != null) { 49 | // Recreate the DocumentSnapshot with the DocumentReference 50 | // containing the original converter. 51 | final finalDoc = _DocumentSnapshotBuilder(docRef) 52 | ..fieldsProto = document._fieldsProto 53 | ..createTime = document.createTime 54 | ..readTime = document.readTime 55 | ..updateTime = document.updateTime; 56 | 57 | orderedDocuments.add(finalDoc.build()); 58 | } else { 59 | throw StateError('Did not receive document for "${docRef.path}".'); 60 | } 61 | } 62 | return _BatchGetResponse(orderedDocuments, _retrievedTransactionId); 63 | } 64 | 65 | Future _fetchDocuments() async { 66 | if (_outstandingDocuments.isEmpty) return; 67 | 68 | final request = firestore1.BatchGetDocumentsRequest( 69 | documents: _outstandingDocuments.toList(), 70 | mask: fieldMask.let((fieldMask) { 71 | return firestore1.DocumentMask( 72 | fieldPaths: fieldMask.map((e) => e._formattedName).toList(), 73 | ); 74 | }), 75 | transaction: transactionId, 76 | newTransaction: transactionOptions, 77 | readTime: readTime?._toProto().timestampValue, 78 | ); 79 | 80 | var resultCount = 0; 81 | try { 82 | final documents = await firestore._client.v1((client) async { 83 | return client.projects.databases.documents.batchGet( 84 | request, 85 | firestore._formattedDatabaseName, 86 | ); 87 | }).catchError(_handleException); 88 | 89 | for (final response in documents) { 90 | DocumentSnapshot? documentSnapshot; 91 | 92 | if (response.transaction?.isNotEmpty ?? false) { 93 | this._retrievedTransactionId = response.transaction; 94 | } 95 | 96 | final found = response.found; 97 | if (found != null) { 98 | documentSnapshot = DocumentSnapshot._fromDocument( 99 | found, 100 | response.readTime, 101 | firestore, 102 | ); 103 | } else if (response.missing != null) { 104 | final missing = response.missing!; 105 | documentSnapshot = DocumentSnapshot._missing( 106 | missing, 107 | response.readTime, 108 | firestore, 109 | ); 110 | } 111 | 112 | if (documentSnapshot != null) { 113 | final path = documentSnapshot.ref._formattedName; 114 | _outstandingDocuments.remove(path); 115 | _retreivedDocuments[path] = documentSnapshot; 116 | resultCount++; 117 | } 118 | } 119 | } on FirebaseFirestoreAdminException catch (firestoreError) { 120 | final shoulRetry = request.transaction != null && 121 | request.newTransaction != null && 122 | // Only retry if we made progress. 123 | resultCount > 0 && 124 | // Don't retry permanent errors. 125 | StatusCode.batchGetRetryCodes 126 | .contains(firestoreError.errorCode.statusCode); 127 | if (shoulRetry) { 128 | return _fetchDocuments(); 129 | } else { 130 | rethrow; 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/filter.dart: -------------------------------------------------------------------------------- 1 | part of 'firestore.dart'; 2 | 3 | enum WhereFilter { 4 | lessThan('LESS_THAN'), 5 | lessThanOrEqual('LESS_THAN_OR_EQUAL'), 6 | equal('EQUAL'), 7 | notEqual('NOT_EQUAL'), 8 | greaterThanOrEqual('GREATER_THAN_OR_EQUAL'), 9 | greaterThan('GREATER_THAN'), 10 | isIn('IN'), 11 | notIn('NOT_IN'), 12 | arrayContains('ARRAY_CONTAINS'), 13 | arrayContainsAny('ARRAY_CONTAINS_ANY'); 14 | 15 | const WhereFilter(this.proto); 16 | 17 | final String proto; 18 | } 19 | 20 | /// A `Filter` represents a restriction on one or more field values and can 21 | /// be used to refine the results of a [Query]. 22 | /// `Filters`s are created by invoking [Filter.where], [Filter.or], 23 | /// or [Filter.and] and can then be passed to [Query.where]. 24 | /// to create a new [Query] instance that also contains this `Filter`. 25 | @immutable 26 | sealed class Filter { 27 | /// Creates and returns a new [Filter], which can be applied to [Query.where], 28 | /// [Filter.or] or [Filter.and]. When applied to a [Query] it requires that 29 | /// documents must contain the specified field and that its value should 30 | /// satisfy the relation constraint provided. 31 | /// 32 | /// - [fieldPath]: The name of a property value to compare. 33 | /// - [op] A comparison operation in the form of a string. 34 | /// Acceptable operator strings are "<", "<=", "==", "!=", ">=", ">", "array-contains", 35 | /// "in", "not-in", and "array-contains-any". 36 | /// - [value] The value to which to compare the field for inclusion in 37 | /// a query. 38 | /// 39 | /// ```dart 40 | /// final collectionRef = firestore.collection('col'); 41 | /// 42 | /// collectionRef.where(Filter.where('foo', '==', 'bar')).get().then((querySnapshot) { 43 | /// querySnapshot.forEach((documentSnapshot) { 44 | /// print('Found document at ${documentSnapshot.ref.path}'); 45 | /// }); 46 | /// }); 47 | /// ``` 48 | factory Filter.where( 49 | Object fieldPath, 50 | WhereFilter op, 51 | Object? value, 52 | ) = _UnaryFilter.fromString; 53 | 54 | /// Creates and returns a new [Filter], which can be applied to [Query.where], 55 | /// [Filter.or] or [Filter.and]. When applied to a [Query] it requires that 56 | /// documents must contain the specified field and that its value should 57 | /// satisfy the relation constraint provided. 58 | /// 59 | /// - [fieldPath]: The name of a property value to compare. 60 | /// - [op] A comparison operation in the form of a string. 61 | /// Acceptable operator strings are "<", "<=", "==", "!=", ">=", ">", "array-contains", 62 | /// "in", "not-in", and "array-contains-any". 63 | /// - [value] The value to which to compare the field for inclusion in 64 | /// a query. 65 | /// 66 | /// ```dart 67 | /// final collectionRef = firestore.collection('col'); 68 | /// 69 | /// collectionRef.where(Filter.where('foo', '==', 'bar')).get().then((querySnapshot) { 70 | /// querySnapshot.forEach((documentSnapshot) { 71 | /// print('Found document at ${documentSnapshot.ref.path}'); 72 | /// }); 73 | /// }); 74 | /// ``` 75 | factory Filter.whereFieldPath( 76 | FieldPath fieldPath, 77 | WhereFilter op, 78 | Object? value, 79 | ) = _UnaryFilter; 80 | 81 | /// Creates and returns a new [Filter] that is a disjunction of the given 82 | /// [Filter]s. A disjunction filter includes a document if it satisfies any 83 | /// of the given [Filter]s. 84 | /// 85 | /// The returned Filter can be applied to [Query.where] [Filter.or], or 86 | /// [Filter.and]. When applied to a [Query] it requires that documents must 87 | /// satisfy one of the provided [Filter]s. 88 | /// 89 | /// - [filters] The [Filter]s 90 | /// for OR operation. These must be created with calls to [Filter], 91 | /// 92 | /// ```dart 93 | /// final collectionRef = firestore.collection('col'); 94 | /// 95 | /// // doc.foo == 'bar' || doc.baz > 0 96 | /// final orFilter = Filter.or(Filter.where('foo', WhereFilter.equal, 'bar'), Filter.where('baz', WhereFilter.greaterThan, 0)); 97 | /// 98 | /// collectionRef.where(orFilter).get().then((querySnapshot) { 99 | /// querySnapshot.forEach((documentSnapshot) { 100 | /// print('Found document at ${documentSnapshot.ref.path}'); 101 | /// }); 102 | /// }); 103 | /// ``` 104 | factory Filter.or(List filters) = _CompositeFilter.or; 105 | 106 | /// Creates and returns a new [Filter] that is a 107 | /// conjunction of the given [Filter]s. A conjunction filter includes 108 | /// a document if it satisfies all of the given [Filter]s. 109 | /// 110 | /// The returned Filter can be applied to [Query.where()], [Filter.or], or 111 | /// [Filter.and]. When applied to a [Query] it requires that documents must satisfy 112 | /// one of the provided [Filter]s. 113 | /// 114 | /// - [filters]: The [Filter]s 115 | /// for AND operation. These must be created with calls to [Filter.where], 116 | /// [Filter.or], or [Filter.and]. 117 | /// 118 | /// ```dart 119 | /// final collectionRef = firestore.collection('col'); 120 | /// 121 | /// // doc.foo == 'bar' && doc.baz > 0 122 | /// final andFilter = Filter.and(Filter.where('foo', WhereFilter.equal, 'bar'), Filter.where('baz', WhereFilter.greaterThan, 0)); 123 | /// 124 | /// collectionRef.where(andFilter).get().then((querySnapshot) { 125 | /// querySnapshot.forEach((documentSnapshot) { 126 | /// print('Found document at ${documentSnapshot.ref.path}'); 127 | /// }); 128 | /// }); 129 | /// ``` 130 | factory Filter.and(List filters) = _CompositeFilter.and; 131 | } 132 | 133 | class _UnaryFilter implements Filter { 134 | _UnaryFilter( 135 | this.fieldPath, 136 | this.op, 137 | this.value, 138 | ) { 139 | if (value == null || identical(value, double.nan)) { 140 | if (op != WhereFilter.equal && op != WhereFilter.notEqual) { 141 | throw ArgumentError( 142 | 'Invalid query for value $value. Only == and != are supported.', 143 | ); 144 | } 145 | } 146 | } 147 | 148 | _UnaryFilter.fromString( 149 | Object field, 150 | WhereFilter op, 151 | Object? value, 152 | ) : this(FieldPath.from(field), op, value); 153 | 154 | final FieldPath fieldPath; 155 | final WhereFilter op; 156 | final Object? value; 157 | } 158 | 159 | class _CompositeFilter implements Filter { 160 | _CompositeFilter({required this.filters, required this.operator}); 161 | 162 | _CompositeFilter.or(List filters) 163 | : this(filters: filters, operator: _CompositeOperator.or); 164 | 165 | _CompositeFilter.and(List filters) 166 | : this(filters: filters, operator: _CompositeOperator.and); 167 | 168 | final List filters; 169 | final _CompositeOperator operator; 170 | } 171 | 172 | enum _CompositeOperator { 173 | and, 174 | or; 175 | 176 | String get proto { 177 | return switch (this) { 178 | _CompositeOperator.and => 'AND', 179 | _CompositeOperator.or => 'OR', 180 | }; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/firestore_api_request_internal.dart: -------------------------------------------------------------------------------- 1 | part of 'firestore.dart'; 2 | 3 | String? _getErrorCode(Object? response) { 4 | if (response is! Map || !response.containsKey('error')) return null; 5 | 6 | final error = response['error']; 7 | if (error is String) return error; 8 | 9 | error as Map; 10 | 11 | final details = error['details']; 12 | if (details is List) { 13 | const fcmErrorType = 'type.googleapis.com/google.firebase.fcm.v1.FcmError'; 14 | for (final element in details) { 15 | if (element is Map && element['@type'] == fcmErrorType) { 16 | return element['errorCode'] as String?; 17 | } 18 | } 19 | } 20 | 21 | if (error.containsKey('status')) { 22 | return error['status'] as String?; 23 | } 24 | 25 | return error['message'] as String?; 26 | } 27 | 28 | /// Extracts error message from the given response object. 29 | String? _getErrorMessage(Object? response) { 30 | switch (response) { 31 | case {'error': {'message': final String? message}}: 32 | return message; 33 | } 34 | 35 | return null; 36 | } 37 | 38 | /// Creates a new FirebaseFirestoreAdminException by extracting the error code, message and other relevant 39 | /// details from an HTTP error response. 40 | FirebaseFirestoreAdminException _createFirebaseError({ 41 | required String body, 42 | required int? statusCode, 43 | required bool isJson, 44 | }) { 45 | if (isJson) { 46 | // For JSON responses, map the server response to a client-side error. 47 | 48 | final json = jsonDecode(body); 49 | final errorCode = _getErrorCode(json)!; 50 | final errorMessage = _getErrorMessage(json); 51 | 52 | return FirebaseFirestoreAdminException.fromServerError( 53 | serverErrorCode: errorCode, 54 | message: errorMessage, 55 | rawServerResponse: json, 56 | ); 57 | } 58 | 59 | // Non-JSON response 60 | FirestoreClientErrorCode error; 61 | switch (statusCode) { 62 | case 400: 63 | error = FirestoreClientErrorCode.invalidArgument; 64 | case 401: 65 | case 403: 66 | error = FirestoreClientErrorCode.unauthenticated; 67 | case 500: 68 | error = FirestoreClientErrorCode.internal; 69 | case 503: 70 | error = FirestoreClientErrorCode.unavailable; 71 | case 409: // HTTP Mapping: 409 Conflict 72 | error = FirestoreClientErrorCode.aborted; 73 | default: 74 | // Treat non-JSON responses with unexpected status codes as unknown errors. 75 | error = FirestoreClientErrorCode.unknown; 76 | } 77 | 78 | return FirebaseFirestoreAdminException( 79 | error, 80 | '${error.message} Raw server response: "$body". Status code: ' 81 | '$statusCode.', 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/firestore_exception.dart: -------------------------------------------------------------------------------- 1 | part of 'firestore.dart'; 2 | 3 | /// A generic guard wrapper for API calls to handle exceptions. 4 | R _firestoreGuard(R Function() cb) { 5 | try { 6 | final value = cb(); 7 | 8 | if (value is Future) { 9 | return value.catchError(_handleException) as R; 10 | } 11 | 12 | return value; 13 | } catch (error, stackTrace) { 14 | _handleException(error, stackTrace); 15 | } 16 | } 17 | 18 | /// Converts a Exception to a FirebaseAdminException. 19 | Never _handleException(Object exception, StackTrace stackTrace) { 20 | if (exception is firestore1.DetailedApiRequestError) { 21 | Error.throwWithStackTrace( 22 | _createFirebaseError( 23 | statusCode: exception.status, 24 | body: switch (exception.jsonResponse) { 25 | null => '', 26 | final json => jsonEncode(json), 27 | }, 28 | isJson: exception.jsonResponse != null, 29 | ), 30 | stackTrace, 31 | ); 32 | } 33 | 34 | Error.throwWithStackTrace(exception, stackTrace); 35 | } 36 | 37 | class FirebaseFirestoreAdminException extends FirebaseAdminException 38 | implements Exception { 39 | FirebaseFirestoreAdminException( 40 | this.errorCode, [ 41 | String? message, 42 | ]) : super('firestore', errorCode.code, message ?? errorCode.message); 43 | 44 | @internal 45 | factory FirebaseFirestoreAdminException.fromServerError({ 46 | required String serverErrorCode, 47 | String? message, 48 | Object? rawServerResponse, 49 | }) { 50 | // If not found, default to unknown error. 51 | final error = firestoreServerToClientCode[serverErrorCode] ?? 52 | FirestoreClientErrorCode.unknown; 53 | message ??= error.message; 54 | 55 | if (error == FirestoreClientErrorCode.unknown && 56 | rawServerResponse != null) { 57 | try { 58 | message += ' Raw server response: "${jsonEncode(rawServerResponse)}"'; 59 | } catch (e) { 60 | // Ignore JSON parsing error. 61 | } 62 | } 63 | 64 | return FirebaseFirestoreAdminException(error, message); 65 | } 66 | 67 | final FirestoreClientErrorCode errorCode; 68 | 69 | @override 70 | String toString() => 'FirebaseFirestoreAdminException: $code: $message'; 71 | } 72 | 73 | /// Firestore server to client enum error codes. 74 | /// https://cloud.google.com/firestore/docs/use-rest-api#error_codes 75 | @internal 76 | const firestoreServerToClientCode = { 77 | // The operation was aborted, typically due to a concurrency issue like transaction aborts, etc. 78 | 'ABORTED': FirestoreClientErrorCode.aborted, 79 | // Some document that we attempted to create already exists. 80 | 'ALREADY_EXISTS': FirestoreClientErrorCode.alreadyExists, 81 | // The operation was cancelled (typically by the caller). 82 | 'CANCELLED': FirestoreClientErrorCode.cancelled, 83 | // Unrecoverable data loss or corruption. 84 | 'DATA_LOSS': FirestoreClientErrorCode.dataLoss, 85 | // Deadline expired before operation could complete. 86 | 'DEADLINE_EXCEEDED': FirestoreClientErrorCode.deadlineExceeded, 87 | // Operation was rejected because the system is not in a state required for the operation's execution. 88 | 'FAILED_PRECONDITION': FirestoreClientErrorCode.failedPrecondition, 89 | // Internal errors. 90 | 'INTERNAL': FirestoreClientErrorCode.internal, 91 | // Client specified an invalid argument. 92 | 'INVALID_ARGUMENT': FirestoreClientErrorCode.invalidArgument, 93 | // Some requested document was not found. 94 | 'NOT_FOUND': FirestoreClientErrorCode.notFound, 95 | // The operation completed successfully. 96 | 'OK': FirestoreClientErrorCode.ok, 97 | // Operation was attempted past the valid range. 98 | 'OUT_OF_RANGE': FirestoreClientErrorCode.outOfRange, 99 | // The caller does not have permission to execute the specified operation. 100 | 'PERMISSION_DENIED': FirestoreClientErrorCode.permissionDenied, 101 | // Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space. 102 | 'RESOURCE_EXHAUSTED': FirestoreClientErrorCode.resourceExhausted, 103 | // The request does not have valid authentication credentials for the operation. 104 | 'UNAUTHENTICATED': FirestoreClientErrorCode.unauthenticated, 105 | // The service is currently unavailable. 106 | 'UNAVAILABLE': FirestoreClientErrorCode.unavailable, 107 | // Operation is not implemented or not supported/enabled. 108 | 'UNIMPLEMENTED': FirestoreClientErrorCode.unimplemented, 109 | // Unknown error or an error from a different error domain. 110 | 'UNKNOWN': FirestoreClientErrorCode.unknown, 111 | }; 112 | 113 | /// Firestore client error codes and their default messages. 114 | enum FirestoreClientErrorCode { 115 | aborted( 116 | statusCode: StatusCode.aborted, 117 | code: 'aborted', 118 | message: 119 | 'The operation was aborted, typically due to a concurrency issue like transaction aborts, etc.', 120 | ), 121 | alreadyExists( 122 | statusCode: StatusCode.alreadyExists, 123 | code: 'already-exists', 124 | message: 'Some document that we attempted to create already exists.', 125 | ), 126 | cancelled( 127 | statusCode: StatusCode.cancelled, 128 | code: 'cancelled', 129 | message: 'The operation was cancelled (typically by the caller).', 130 | ), 131 | dataLoss( 132 | statusCode: StatusCode.dataLoss, 133 | code: 'data-loss', 134 | message: 'Unrecoverable data loss or corruption.', 135 | ), 136 | deadlineExceeded( 137 | statusCode: StatusCode.deadlineExceeded, 138 | code: 'deadline_exceeded', 139 | message: 'Deadline expired before operation could complete.', 140 | ), 141 | failedPrecondition( 142 | statusCode: StatusCode.failedPrecondition, 143 | code: 'failed_precondition', 144 | message: 145 | "Operation was rejected because the system is not in a state required for the operation's execution.", 146 | ), 147 | internal( 148 | statusCode: StatusCode.internal, 149 | code: 'internal', 150 | message: 'Internal errors.', 151 | ), 152 | invalidArgument( 153 | statusCode: StatusCode.invalidArgument, 154 | code: 'invalid_argument', 155 | message: 'Client specified an invalid argument.', 156 | ), 157 | notFound( 158 | statusCode: StatusCode.notFound, 159 | code: 'not_found', 160 | message: 'Some requested document was not found.', 161 | ), 162 | ok( 163 | statusCode: StatusCode.ok, 164 | code: 'ok', 165 | message: 'The operation completed successfully.', 166 | ), 167 | outOfRange( 168 | statusCode: StatusCode.outOfRange, 169 | code: 'out_of_range', 170 | message: 'Operation was attempted past the valid range.', 171 | ), 172 | permissionDenied( 173 | statusCode: StatusCode.permissionDenied, 174 | code: 'permission_denied', 175 | message: 176 | 'The caller does not have permission to execute the specified operation.', 177 | ), 178 | resourceExhausted( 179 | statusCode: StatusCode.resourceExhausted, 180 | code: 'resource_exhausted', 181 | message: 182 | 'Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space.', 183 | ), 184 | unauthenticated( 185 | statusCode: StatusCode.unauthenticated, 186 | code: 'unauthenticated', 187 | message: 188 | 'The request does not have valid authentication credentials for the operation.', 189 | ), 190 | unavailable( 191 | statusCode: StatusCode.unavailable, 192 | code: 'unavailable', 193 | message: 'The service is currently unavailable.', 194 | ), 195 | unimplemented( 196 | statusCode: StatusCode.unimplemented, 197 | code: 'unimplemented', 198 | message: 'Operation is not implemented or not supported/enabled.', 199 | ), 200 | unknown( 201 | statusCode: StatusCode.unknown, 202 | code: 'unknown', 203 | message: 'Unknown error or an error from a different error domain.', 204 | ); 205 | 206 | const FirestoreClientErrorCode({ 207 | required this.statusCode, 208 | required this.code, 209 | required this.message, 210 | }); 211 | 212 | final StatusCode statusCode; 213 | final String code; 214 | final String message; 215 | } 216 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/geo_point.dart: -------------------------------------------------------------------------------- 1 | part of 'firestore.dart'; 2 | 3 | /// An immutable object representing a geographic location in Firestore. The 4 | /// location is represented as a latitude/longitude pair. 5 | @immutable 6 | final class GeoPoint implements _Serializable { 7 | GeoPoint({ 8 | required this.latitude, 9 | required this.longitude, 10 | }) { 11 | if (latitude.isNaN) { 12 | throw ArgumentError.value( 13 | latitude, 14 | 'latitude', 15 | 'Value for argument "latitude" is not a valid number', 16 | ); 17 | } 18 | if (longitude.isNaN) { 19 | throw ArgumentError.value( 20 | longitude, 21 | 'longitude', 22 | 'Value for argument "longitude" is not a valid number', 23 | ); 24 | } 25 | 26 | if (latitude < -90 || latitude > 90) { 27 | throw ArgumentError.value( 28 | latitude, 29 | 'latitude', 30 | 'Latitude must be in the range of [-90, 90]', 31 | ); 32 | } 33 | if (longitude < -180 || longitude > 180) { 34 | throw ArgumentError.value( 35 | longitude, 36 | 'longitude', 37 | 'Longitude must be in the range of [-180, 180]', 38 | ); 39 | } 40 | } 41 | 42 | /// Converts a google.type.LatLng proto to its GeoPoint representation. 43 | factory GeoPoint._fromProto(firestore1.LatLng latLng) { 44 | return GeoPoint( 45 | latitude: latLng.latitude ?? 0, 46 | longitude: latLng.longitude ?? 0, 47 | ); 48 | } 49 | 50 | /// The latitude as a number between -90 and 90. 51 | final double latitude; 52 | 53 | /// The longitude as a number between -180 and 180. 54 | final double longitude; 55 | 56 | @override 57 | firestore1.Value _toProto() { 58 | return firestore1.Value( 59 | geoPointValue: firestore1.LatLng( 60 | latitude: latitude, 61 | longitude: longitude, 62 | ), 63 | ); 64 | } 65 | 66 | @override 67 | bool operator ==(Object other) { 68 | return other is GeoPoint && 69 | other.latitude == latitude && 70 | other.longitude == longitude; 71 | } 72 | 73 | @override 74 | int get hashCode => Object.hash(latitude, longitude); 75 | } 76 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/serializer.dart: -------------------------------------------------------------------------------- 1 | part of 'firestore.dart'; 2 | 3 | /// A type representing the raw Firestore document data. 4 | typedef DocumentData = Map; 5 | 6 | @internal 7 | typedef ApiMapValue = Map; 8 | 9 | abstract base class _Serializable { 10 | firestore1.Value _toProto(); 11 | } 12 | 13 | class _Serializer { 14 | _Serializer(this.firestore); 15 | 16 | final Firestore firestore; 17 | 18 | Object _createInteger(String n) { 19 | if (firestore._settings.useBigInt ?? false) { 20 | return BigInt.parse(n); 21 | } else { 22 | return int.parse(n); 23 | } 24 | } 25 | 26 | /// Encodes a Dart object into the Firestore 'Fields' representation. 27 | firestore1.MapValue encodeFields(DocumentData obj) { 28 | return firestore1.MapValue( 29 | fields: obj.map((key, value) { 30 | return MapEntry(key, encodeValue(value)); 31 | }).whereValueNotNull(), 32 | ); 33 | } 34 | 35 | /// Encodes a Dart value into the Firestore 'Value' representation. 36 | firestore1.Value? encodeValue(Object? value) { 37 | switch (value) { 38 | case _FieldTransform(): 39 | return null; 40 | 41 | case String(): 42 | return firestore1.Value(stringValue: value); 43 | 44 | case bool(): 45 | return firestore1.Value(booleanValue: value); 46 | 47 | case int(): 48 | case BigInt(): 49 | return firestore1.Value(integerValue: value.toString()); 50 | 51 | case double(): 52 | return firestore1.Value(doubleValue: value); 53 | 54 | case DateTime(): 55 | final timestamp = Timestamp.fromDate(value); 56 | return timestamp._toProto(); 57 | 58 | case null: 59 | return firestore1.Value( 60 | nullValue: 'NULL_VALUE', 61 | ); 62 | 63 | case _Serializable(): 64 | return value._toProto(); 65 | 66 | case List(): 67 | return firestore1.Value( 68 | arrayValue: firestore1.ArrayValue( 69 | values: value.map(encodeValue).nonNulls.toList(), 70 | ), 71 | ); 72 | 73 | case Map(): 74 | if (value.isEmpty) { 75 | return firestore1.Value( 76 | mapValue: firestore1.MapValue(fields: {}), 77 | ); 78 | } 79 | 80 | final fields = encodeFields(Map.from(value)); 81 | if (fields.fields!.isEmpty) return null; 82 | 83 | return firestore1.Value(mapValue: fields); 84 | 85 | default: 86 | throw ArgumentError.value( 87 | value, 88 | 'value', 89 | 'Unsupported field value: ${value.runtimeType}', 90 | ); 91 | } 92 | } 93 | 94 | /// Decodes a single Firestore 'Value' Protobuf. 95 | Object? decodeValue(Object? proto) { 96 | if (proto is! firestore1.Value) { 97 | throw ArgumentError.value( 98 | proto, 99 | 'proto', 100 | 'Cannot decode type from Firestore Value: ${proto.runtimeType}', 101 | ); 102 | } 103 | _assertValidProtobufValue(proto); 104 | 105 | switch (proto) { 106 | case firestore1.Value(:final stringValue?): 107 | return stringValue; 108 | case firestore1.Value(:final booleanValue?): 109 | return booleanValue; 110 | case firestore1.Value(:final integerValue?): 111 | return _createInteger(integerValue); 112 | case firestore1.Value(:final doubleValue?): 113 | return doubleValue; 114 | case firestore1.Value(:final timestampValue?): 115 | return Timestamp._fromString(timestampValue); 116 | case firestore1.Value(:final referenceValue?): 117 | final reosucePath = _QualifiedResourcePath.fromSlashSeparatedString( 118 | referenceValue, 119 | ); 120 | return firestore.doc(reosucePath.relativeName); 121 | case firestore1.Value(:final arrayValue?): 122 | final values = arrayValue.values; 123 | return [ 124 | if (values != null) 125 | for (final value in values) decodeValue(value), 126 | ]; 127 | case firestore1.Value(nullValue: != null): 128 | return null; 129 | case firestore1.Value(:final mapValue?): 130 | final fields = mapValue.fields; 131 | return { 132 | if (fields != null) 133 | for (final entry in fields.entries) 134 | entry.key: decodeValue(entry.value), 135 | }; 136 | case firestore1.Value(:final geoPointValue?): 137 | return GeoPoint._fromProto(geoPointValue); 138 | 139 | default: 140 | throw ArgumentError.value( 141 | proto, 142 | 'proto', 143 | 'Cannot decode type from Firestore Value: ${proto.runtimeType}', 144 | ); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/status_code.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | @internal 4 | enum StatusCode { 5 | ok(0), 6 | cancelled(1), 7 | unknown(2), 8 | invalidArgument(3), 9 | deadlineExceeded(4), 10 | notFound(5), 11 | alreadyExists(6), 12 | permissionDenied(7), 13 | resourceExhausted(8), 14 | failedPrecondition(9), 15 | aborted(10), 16 | outOfRange(11), 17 | unimplemented(12), 18 | internal(13), 19 | unavailable(14), 20 | dataLoss(15), 21 | unauthenticated(16); 22 | 23 | const StatusCode(this.value); 24 | 25 | // Imported from https://github.com/googleapis/nodejs-firestore/blob/fba4949be5be8b26720f0fefcf176e549829e382/dev/src/v1/firestore_client_config.json 26 | static const nonIdempotentRetryCodes = []; 27 | static const idempotentRetryCodes = [ 28 | StatusCode.deadlineExceeded, 29 | StatusCode.unavailable, 30 | ]; 31 | 32 | static const deadlineExceededResourceExhaustedInternalUnavailable = 33 | [ 34 | StatusCode.deadlineExceeded, 35 | StatusCode.resourceExhausted, 36 | StatusCode.internal, 37 | StatusCode.unavailable, 38 | ]; 39 | 40 | static const resourceExhaustedUnavailable = [ 41 | StatusCode.resourceExhausted, 42 | StatusCode.unavailable, 43 | ]; 44 | 45 | static const resourceExhaustedAbortedUnavailable = [ 46 | StatusCode.resourceExhausted, 47 | StatusCode.aborted, 48 | StatusCode.unavailable, 49 | ]; 50 | 51 | static const commitRetryCodes = resourceExhaustedUnavailable; 52 | 53 | static const batchGetRetryCodes = [ 54 | StatusCode.deadlineExceeded, 55 | StatusCode.resourceExhausted, 56 | StatusCode.internal, 57 | StatusCode.unavailable, 58 | ]; 59 | 60 | final int value; 61 | } 62 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/timestamp.dart: -------------------------------------------------------------------------------- 1 | part of 'firestore.dart'; 2 | 3 | /// Encode seconds+nanoseconds to a Google Firestore timestamp string. 4 | String _toGoogleDateTime({required int seconds, required int nanoseconds}) { 5 | final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000, isUtc: true); 6 | var formattedDate = DateFormat('yyyy-MM-ddTHH:mm:ss').format(date); 7 | 8 | if (nanoseconds > 0) { 9 | final nanoString = 10 | nanoseconds.toString().padLeft(9, '0'); // Ensure it has 9 digits 11 | formattedDate = '$formattedDate.$nanoString'; 12 | } 13 | 14 | return '${formattedDate}Z'; 15 | } 16 | 17 | /// A Timestamp represents a point in time independent of any time zone or calendar, 18 | /// represented as seconds and fractions of seconds at nanosecond resolution in UTC 19 | /// Epoch time. It is encoded using the Proleptic Gregorian Calendar which extends 20 | /// the Gregorian calendar backwards to year one. It is encoded assuming all minutes 21 | /// are 60 seconds long, i.e. leap seconds are "smeared" so that no leap second table 22 | /// is needed for interpretation. Range is from 0001-01-01T00:00:00Z to 23 | /// 9999-12-31T23:59:59.999999999Z. By restricting to that range, we ensure that we 24 | /// can convert to and from RFC 3339 date strings. 25 | /// 26 | /// For more information, see [the reference timestamp definition](https://github.com/google/protobuf/blob/master/src/google/protobuf/timestamp.proto) 27 | @immutable 28 | final class Timestamp implements _Serializable { 29 | Timestamp({required this.seconds, required this.nanoseconds}) { 30 | const minSeconds = -62135596800; 31 | const maxSeconds = 253402300799; 32 | 33 | if (seconds < minSeconds || seconds > maxSeconds) { 34 | throw ArgumentError.value( 35 | seconds, 36 | 'seconds', 37 | 'must be between $minSeconds and $maxSeconds.', 38 | ); 39 | } 40 | 41 | const maxNanoSeconds = 999999999; 42 | if (nanoseconds < 0 || nanoseconds > maxNanoSeconds) { 43 | throw ArgumentError.value( 44 | nanoseconds, 45 | 'nanoseconds', 46 | 'must be between 0 and $maxNanoSeconds.', 47 | ); 48 | } 49 | } 50 | 51 | /// Creates a new timestamp with the current date, with millisecond precision. 52 | /// 53 | /// ```dart 54 | /// final documentRef = firestore.doc('col/doc'); 55 | /// 56 | /// documentRef.set({'updateTime': Timestamp.now()}); 57 | /// ``` 58 | /// Returns a new `Timestamp` representing the current date. 59 | factory Timestamp.now() => Timestamp.fromDate(DateTime.now()); 60 | 61 | /// Creates a new timestamp from the given date. 62 | /// 63 | /// ```dart 64 | /// final documentRef = firestore.doc('col/doc'); 65 | /// 66 | /// final date = Date.parse('01 Jan 2000 00:00:00 GMT'); 67 | /// documentRef.set({ 'startTime': Timestamp.fromDate(date) }); 68 | /// 69 | /// ``` 70 | /// 71 | /// - [date]: The date to initialize the `Timestamp` from. 72 | /// 73 | /// Returns a new [Timestamp] representing the same point in time 74 | /// as the given date. 75 | factory Timestamp.fromDate(DateTime date) { 76 | return Timestamp.fromMicros(date.microsecondsSinceEpoch); 77 | } 78 | 79 | /// Creates a new timestamp from the given number of milliseconds. 80 | /// 81 | /// ```dart 82 | /// final documentRef = firestore.doc('col/doc'); 83 | /// 84 | /// documentRef.set({ 'startTime': Timestamp.fromMillis(42) }); 85 | /// ``` 86 | /// 87 | /// - [milliseconds]: Number of milliseconds since Unix epoch 88 | /// 1970-01-01T00:00:00Z. 89 | /// 90 | /// Returns a new [Timestamp] representing the same point in time 91 | /// as the given number of milliseconds. 92 | factory Timestamp.fromMillis(int milliseconds) { 93 | final seconds = (milliseconds / 1000).floor(); 94 | final nanos = (milliseconds - seconds * 1000) * _msToNanos; 95 | 96 | return Timestamp(seconds: seconds, nanoseconds: nanos); 97 | } 98 | 99 | /// Creates a new timestamp from the given number of microseconds. 100 | /// 101 | /// ```dart 102 | /// final documentRef = firestore.doc('col/doc'); 103 | /// 104 | /// documentRef.set({ 'startTime': Timestamp.fromMicros(42) }); 105 | /// ``` 106 | /// 107 | /// - [microseconds]: Number of microseconds since Unix epoch 108 | /// 1970-01-01T00:00:00Z. 109 | /// 110 | /// Returns a new [Timestamp] representing the same point in time 111 | /// as the given number of microseconds. 112 | factory Timestamp.fromMicros(int microseconds) { 113 | final seconds = (microseconds / 1000 / 1000).floor(); 114 | final nanos = (microseconds - seconds * 1000 * 1000) * _usToNanos; 115 | 116 | return Timestamp(seconds: seconds, nanoseconds: nanos); 117 | } 118 | 119 | factory Timestamp._fromString(String timestampValue) { 120 | final date = DateTime.parse(timestampValue); 121 | var nanos = 0; 122 | 123 | if (timestampValue.length > 20) { 124 | final nanoString = timestampValue.substring( 125 | 20, 126 | timestampValue.length - 1, 127 | ); 128 | final trailingZeroes = 9 - nanoString.length; 129 | nanos = int.parse(nanoString) * (math.pow(10, trailingZeroes).toInt()); 130 | } 131 | 132 | if (nanos.isNaN || date.second.isNaN) { 133 | throw ArgumentError.value( 134 | timestampValue, 135 | 'timestampValue', 136 | 'Specify a valid ISO 8601 timestamp.', 137 | ); 138 | } 139 | 140 | return Timestamp( 141 | seconds: date.millisecondsSinceEpoch ~/ 1000, 142 | nanoseconds: nanos, 143 | ); 144 | } 145 | 146 | static const _msToNanos = 1000000; 147 | static const _usToNanos = 1000; 148 | 149 | final int seconds; 150 | final int nanoseconds; 151 | 152 | @override 153 | firestore1.Value _toProto() { 154 | return firestore1.Value( 155 | timestampValue: _toGoogleDateTime( 156 | seconds: seconds, 157 | nanoseconds: nanoseconds, 158 | ), 159 | ); 160 | } 161 | 162 | @override 163 | bool operator ==(Object other) { 164 | return other is Timestamp && 165 | seconds == other.seconds && 166 | nanoseconds == other.nanoseconds; 167 | } 168 | 169 | @override 170 | int get hashCode => Object.hash(seconds, nanoseconds); 171 | 172 | @override 173 | String toString() { 174 | return 'Timestamp(seconds=$seconds, nanoseconds=$nanoseconds)'; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/types.dart: -------------------------------------------------------------------------------- 1 | part of 'firestore.dart'; 2 | 3 | typedef UpdateMap = Map; 4 | 5 | typedef FromFirestore = T Function( 6 | QueryDocumentSnapshot value, 7 | ); 8 | typedef ToFirestore = DocumentData Function(T value); 9 | 10 | DocumentData _jsonFromFirestore(QueryDocumentSnapshot value) { 11 | return value.data(); 12 | } 13 | 14 | DocumentData _jsonToFirestore(DocumentData value) => value; 15 | 16 | const _FirestoreDataConverter _jsonConverter = ( 17 | fromFirestore: _jsonFromFirestore, 18 | toFirestore: _jsonToFirestore, 19 | ); 20 | 21 | typedef _FirestoreDataConverter = ({ 22 | FromFirestore fromFirestore, 23 | ToFirestore toFirestore, 24 | }); 25 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:meta/meta.dart'; 5 | 6 | @internal 7 | extension MapWhereValue on Map { 8 | Map whereValueNotNull() { 9 | return Map.fromEntries( 10 | entries 11 | .where((e) => e.value != null) 12 | // ignore: null_check_on_nullable_type_parameter 13 | .map((e) => MapEntry(e.key, e.value!)), 14 | ); 15 | } 16 | } 17 | 18 | @internal 19 | Uint8List randomBytes(int length) { 20 | final rnd = Random.secure(); 21 | return Uint8List.fromList( 22 | List.generate(length, (i) => rnd.nextInt(256)), 23 | ); 24 | } 25 | 26 | /// Generate a unique client-side identifier. 27 | /// 28 | /// Used for the creation of new documents. 29 | /// Returns a unique 20-character wide identifier. 30 | @internal 31 | String autoId() { 32 | const chars = 33 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 34 | var autoId = ''; 35 | while (autoId.length < 20) { 36 | final bytes = randomBytes(40); 37 | for (final b in bytes) { 38 | // Length of `chars` is 62. We only take bytes between 0 and 62*4-1 39 | // (both inclusive). The value is then evenly mapped to indices of `char` 40 | // via a modulo operation. 41 | const maxValue = 62 * 4 - 1; 42 | if (autoId.length < 20 && b <= maxValue) { 43 | autoId += chars[b % 62]; 44 | } 45 | } 46 | } 47 | return autoId; 48 | } 49 | 50 | /// Generate a short and semi-random client-side identifier. 51 | /// 52 | /// Used for the creation of request tags. 53 | @internal 54 | String requestTag() => autoId().substring(0, 5); 55 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/google_cloud_firestore/validate.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | /// Validates that 'value' is a host. 4 | @internal 5 | void validateHost( 6 | String value, { 7 | required String argName, 8 | }) { 9 | final urlString = 'http://$value/'; 10 | Uri parsed; 11 | try { 12 | parsed = Uri.parse(urlString); 13 | } catch (e) { 14 | throw ArgumentError.value(value, argName, 'Must be a valid host'); 15 | } 16 | 17 | if (parsed.query.isNotEmpty || 18 | parsed.path != '/' || 19 | parsed.userName.isNotEmpty) { 20 | throw ArgumentError.value(value, argName, 'Must be a valid host'); 21 | } 22 | } 23 | 24 | extension on Uri { 25 | String get userName => userInfo.split(':').first; 26 | } 27 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/messaging.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:googleapis/fcm/v1.dart' as fmc1; 5 | import 'package:http/http.dart'; 6 | import 'package:meta/meta.dart'; 7 | 8 | import 'app.dart'; 9 | 10 | part 'messaging/fmc_exception.dart'; 11 | part 'messaging/messaging_api.dart'; 12 | part 'messaging/messaging_api_request_internal.dart'; 13 | 14 | const _fmcMaxBatchSize = 500; 15 | 16 | // const _fcmTopicManagementHost = 'iid.googleapis.com'; 17 | // const _fcmTopicManagementAddPath = '/iid/v1:batchAdd'; 18 | // const _fcmTopicManagementRemovePath = '/iid/v1:batchRemove'; 19 | 20 | /// An interface for interacting with the Firebase Cloud Messaging service. 21 | class Messaging { 22 | /// An interface for interacting with the Firebase Cloud Messaging service. 23 | Messaging( 24 | this.firebase, { 25 | @internal FirebaseMessagingRequestHandler? requestHandler, 26 | }) : _requestHandler = 27 | requestHandler ?? FirebaseMessagingRequestHandler(firebase); 28 | 29 | /// The app associated with this Messaging instance. 30 | final FirebaseAdminApp firebase; 31 | 32 | final FirebaseMessagingRequestHandler _requestHandler; 33 | 34 | String get _parent => 'projects/${firebase.projectId}'; 35 | 36 | /// Sends the given message via FCM. 37 | /// 38 | /// - [message] - The message payload. 39 | /// - [dryRun] - Whether to send the message in the dry-run 40 | /// (validation only) mode. 41 | /// 42 | /// Returns a unique message ID string after the message has been successfully 43 | /// handed off to the FCM service for delivery. 44 | Future send(Message message, {bool? dryRun}) { 45 | return _requestHandler.v1( 46 | (client) async { 47 | final response = await client.projects.messages.send( 48 | fmc1.SendMessageRequest( 49 | message: message._toProto(), 50 | validateOnly: dryRun, 51 | ), 52 | _parent, 53 | ); 54 | 55 | final name = response.name; 56 | if (name == null) { 57 | throw FirebaseMessagingAdminException( 58 | MessagingClientErrorCode.internalError, 59 | 'No name in response', 60 | ); 61 | } 62 | 63 | return name; 64 | }, 65 | ); 66 | } 67 | 68 | /// Sends each message in the given array via Firebase Cloud Messaging. 69 | /// 70 | // TODO once we have Messaging.sendAll, add the following: 71 | // Unlike [Messaging.sendAll], this method makes a single RPC call for each message 72 | // in the given array. 73 | /// 74 | /// The responses list obtained from the return value corresponds to the order of `messages`. 75 | /// An error from this method or a `BatchResponse` with all failures indicates a total failure, 76 | /// meaning that none of the messages in the list could be sent. Partial failures or no 77 | /// failures are only indicated by a `BatchResponse` return value. 78 | /// 79 | /// - [messages]: A non-empty array containing up to 500 messages. 80 | /// - [dryRun]: Whether to send the messages in the dry-run 81 | /// (validation only) mode. 82 | Future sendEach(List messages, {bool? dryRun}) { 83 | return _requestHandler.v1( 84 | (client) async { 85 | if (messages.isEmpty) { 86 | throw FirebaseMessagingAdminException( 87 | MessagingClientErrorCode.invalidArgument, 88 | 'messages must be a non-empty array', 89 | ); 90 | } 91 | if (messages.length > _fmcMaxBatchSize) { 92 | throw FirebaseMessagingAdminException( 93 | MessagingClientErrorCode.invalidArgument, 94 | 'messages list must not contain more than $_fmcMaxBatchSize items', 95 | ); 96 | } 97 | 98 | final responses = await Future.wait( 99 | messages.map((message) async { 100 | final response = client.projects.messages.send( 101 | fmc1.SendMessageRequest( 102 | message: message._toProto(), 103 | validateOnly: dryRun, 104 | ), 105 | _parent, 106 | ); 107 | 108 | return response.then( 109 | (value) { 110 | return SendResponse._(success: true, messageId: value.name); 111 | }, 112 | // ignore: avoid_types_on_closure_parameters 113 | onError: (Object? error) { 114 | return SendResponse._( 115 | success: false, 116 | error: error is FirebaseMessagingAdminException 117 | ? error 118 | : FirebaseMessagingAdminException( 119 | MessagingClientErrorCode.internalError, 120 | error.toString(), 121 | ), 122 | ); 123 | }, 124 | ); 125 | }), 126 | ); 127 | 128 | final successCount = responses.where((r) => r.success).length; 129 | 130 | return BatchResponse._( 131 | responses: responses, 132 | successCount: successCount, 133 | failureCount: responses.length - successCount, 134 | ); 135 | }, 136 | ); 137 | } 138 | 139 | /// Sends the given multicast message to all the FCM registration tokens 140 | /// specified in it. 141 | /// 142 | /// This method uses the [Messaging.sendEach] API under the hood to send the given 143 | /// message to all the target recipients. The responses list obtained from the 144 | /// return value corresponds to the order of tokens in the `MulticastMessage`. 145 | /// An error from this method or a `BatchResponse` with all failures indicates a total 146 | /// failure, meaning that the messages in the list could be sent. Partial failures or 147 | /// failures are only indicated by a `BatchResponse` return value. 148 | /// 149 | /// - [message]: A multicast message containing up to 500 tokens. 150 | /// - [dryRun]: Whether to send the message in the dry-run 151 | /// (validation only) mode. 152 | Future sendEachForMulticast( 153 | MulticastMessage message, { 154 | bool? dryRun, 155 | }) { 156 | return sendEach( 157 | message.tokens 158 | .map( 159 | (token) => TokenMessage( 160 | token: token, 161 | data: message.data, 162 | notification: message.notification, 163 | android: message.android, 164 | apns: message.apns, 165 | fcmOptions: message.fcmOptions, 166 | webpush: message.webpush, 167 | ), 168 | ) 169 | .toList(), 170 | dryRun: dryRun, 171 | ); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/messaging/messaging_api_request_internal.dart: -------------------------------------------------------------------------------- 1 | part of '../messaging.dart'; 2 | 3 | final _legacyFirebaseMessagingHeaders = { 4 | // TODO send version 5 | 'X-Firebase-Client': 'fire-admin-node/12.0.0', 6 | 'access_token_auth': 'true', 7 | }; 8 | 9 | @internal 10 | class FirebaseMessagingRequestHandler { 11 | FirebaseMessagingRequestHandler(this.firebase); 12 | 13 | final FirebaseAdminApp firebase; 14 | 15 | Future _run( 16 | Future Function(Client client) fn, 17 | ) { 18 | return _fmcGuard(() => firebase.client.then(fn)); 19 | } 20 | 21 | Future _fmcGuard( 22 | FutureOr Function() fn, 23 | ) async { 24 | try { 25 | final value = fn(); 26 | 27 | if (value is T) return value; 28 | 29 | return value.catchError(_handleException); 30 | } catch (error, stackTrace) { 31 | _handleException(error, stackTrace); 32 | } 33 | } 34 | 35 | Future v1( 36 | Future Function(fmc1.FirebaseCloudMessagingApi client) fn, 37 | ) { 38 | return _run((client) => fn(fmc1.FirebaseCloudMessagingApi(client))); 39 | } 40 | 41 | /// Invokes the request handler with the provided request data. 42 | Future invokeRequestHandler({ 43 | required String host, 44 | required String path, 45 | Object? requestData, 46 | }) async { 47 | try { 48 | final client = await firebase.client; 49 | 50 | final response = await client.post( 51 | Uri.https(host, path), 52 | body: jsonEncode(requestData), 53 | headers: { 54 | ..._legacyFirebaseMessagingHeaders, 55 | 'content-type': 'application/json', 56 | }, 57 | ); 58 | 59 | // Send non-JSON responses to the catch() below where they will be treated as errors. 60 | if (!response.isJson) { 61 | throw _HttpException(response); 62 | } 63 | 64 | final json = jsonDecode(response.body); 65 | 66 | // Check for backend errors in the response. 67 | final errorCode = _getErrorCode(json); 68 | if (errorCode != null) { 69 | throw _HttpException(response); 70 | } 71 | 72 | return json; 73 | } on _HttpException catch (error, stackTrace) { 74 | Error.throwWithStackTrace( 75 | _createFirebaseError( 76 | body: error.response.body, 77 | statusCode: error.response.statusCode, 78 | isJson: error.response.isJson, 79 | ), 80 | stackTrace, 81 | ); 82 | } 83 | } 84 | } 85 | 86 | String? _getErrorCode(Object? response) { 87 | if (response is! Map || !response.containsKey('error')) return null; 88 | 89 | final error = response['error']; 90 | if (error is String) return error; 91 | 92 | error as Map; 93 | 94 | final details = error['details']; 95 | if (details is List) { 96 | const fcmErrorType = 'type.googleapis.com/google.firebase.fcm.v1.FcmError'; 97 | for (final element in details) { 98 | if (element is Map && element['@type'] == fcmErrorType) { 99 | return element['errorCode'] as String?; 100 | } 101 | } 102 | } 103 | 104 | if (error.containsKey('status')) { 105 | return error['status'] as String?; 106 | } 107 | 108 | return error['message'] as String?; 109 | } 110 | 111 | /// Extracts error message from the given response object. 112 | String? _getErrorMessage(Object? response) { 113 | switch (response) { 114 | case {'error': {'message': final String? message}}: 115 | return message; 116 | } 117 | 118 | return null; 119 | } 120 | 121 | /// Creates a new FirebaseMessagingError by extracting the error code, message and other relevant 122 | /// details from an HTTP error response. 123 | FirebaseMessagingAdminException _createFirebaseError({ 124 | required String body, 125 | required int? statusCode, 126 | required bool isJson, 127 | }) { 128 | if (isJson) { 129 | // For JSON responses, map the server response to a client-side error. 130 | 131 | final json = jsonDecode(body); 132 | final errorCode = _getErrorCode(json)!; 133 | final errorMessage = _getErrorMessage(json); 134 | 135 | return FirebaseMessagingAdminException.fromServerError( 136 | serverErrorCode: errorCode, 137 | message: errorMessage, 138 | rawServerResponse: json, 139 | ); 140 | } 141 | 142 | // Non-JSON response 143 | MessagingClientErrorCode error; 144 | switch (statusCode) { 145 | case 400: 146 | error = MessagingClientErrorCode.invalidArgument; 147 | case 401: 148 | case 403: 149 | error = MessagingClientErrorCode.authenticationError; 150 | case 500: 151 | error = MessagingClientErrorCode.internalError; 152 | case 503: 153 | error = MessagingClientErrorCode.serverUnavailable; 154 | default: 155 | // Treat non-JSON responses with unexpected status codes as unknown errors. 156 | error = MessagingClientErrorCode.unknownError; 157 | } 158 | 159 | return FirebaseMessagingAdminException( 160 | error, 161 | '${error.message} Raw server response: "$body". Status code: ' 162 | '$statusCode.', 163 | ); 164 | } 165 | 166 | extension on Response { 167 | bool get isJson => 168 | headers['content-type']?.contains('application/json') ?? false; 169 | } 170 | 171 | class _HttpException implements Exception { 172 | _HttpException(this.response); 173 | 174 | final Response response; 175 | } 176 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/object_utils.dart: -------------------------------------------------------------------------------- 1 | extension ObjectUtils on T? { 2 | T orThrow(Never Function() thrower) => this ?? thrower(); 3 | 4 | R? let(R Function(T) block) { 5 | final that = this; 6 | return that == null ? null : block(that); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/security_rules/security_rules.dart: -------------------------------------------------------------------------------- 1 | import '../../dart_firebase_admin.dart'; 2 | import 'security_rules_api_internals.dart'; 3 | 4 | /// A source file containing some Firebase security rules. The content includes raw 5 | /// source code including text formatting, indentation and comments. 6 | class RulesFile { 7 | RulesFile({required this.name, required this.content}); 8 | 9 | final String name; 10 | final String content; 11 | } 12 | 13 | /// Required metadata associated with a ruleset. 14 | class RulesetMetadata { 15 | RulesetMetadata._from(RulesetResponse rs) 16 | : name = _stripProjectIdPrefix(rs.name), 17 | createTime = DateTime.parse(rs.createTime).toIso8601String(); 18 | 19 | /// Name of the [Ruleset] as a short string. This can be directly passed into APIs 20 | /// like [SecurityRules.getRuleset] and [SecurityRules.deleteRuleset]. 21 | final String name; 22 | 23 | /// Creation time of the [Ruleset] as a UTC timestamp string. 24 | final String createTime; 25 | } 26 | 27 | /// A page of ruleset metadata. 28 | class RulesetMetadataList { 29 | RulesetMetadataList._fromResponse(ListRulesetsResponse response) 30 | : rulesets = response.rulesets.map(RulesetMetadata._from).toList(), 31 | nextPageToken = response.nextPageToken; 32 | 33 | /// A batch of ruleset metadata. 34 | final List rulesets; 35 | 36 | /// The next page token if available. This is needed to retrieve the next batch. 37 | final String? nextPageToken; 38 | } 39 | 40 | /// A set of Firebase security rules. 41 | class Ruleset extends RulesetMetadata { 42 | Ruleset._fromResponse(super.rs) 43 | : source = rs.source.files, 44 | super._from(); 45 | 46 | final List source; 47 | } 48 | 49 | /// The Firebase `SecurityRules` service interface. 50 | class SecurityRules { 51 | SecurityRules(this.app); 52 | 53 | static const _cloudFirestore = 'cloud.firestore'; 54 | static const _firebaseStorage = 'firebase.storage'; 55 | 56 | final FirebaseAdminApp app; 57 | late final _client = SecurityRulesApiClient(app); 58 | 59 | /// Gets the [Ruleset] identified by the given 60 | /// name. The input name should be the short name string without the project ID 61 | /// prefix. For example, to retrieve the `projects/project-id/rulesets/my-ruleset`, 62 | /// pass the short name "my-ruleset". Rejects with a `not-found` error if the 63 | /// specified [Ruleset] cannot be found. 64 | /// 65 | /// [name] - Name of the [Ruleset] to retrieve. 66 | /// Returns a future that fulfills with the specified [Ruleset]. 67 | Future getRuleset(String name) async { 68 | final rulesetResponse = await _client.getRuleset(name); 69 | 70 | return Ruleset._fromResponse(rulesetResponse); 71 | } 72 | 73 | /// Gets the [Ruleset] currently applied to 74 | /// Cloud Firestore. Rejects with a `not-found` error if no ruleset is applied 75 | /// on Firestore. 76 | /// 77 | /// Returns a future that fulfills with the Firestore ruleset. 78 | Future getFirestoreRuleset() { 79 | return _getRulesetForRelease(_cloudFirestore); 80 | } 81 | 82 | /// Creates a new [Ruleset] from the given 83 | /// source, and applies it to Cloud Firestore. 84 | /// 85 | /// [source] - Rules source to apply. 86 | /// Returns a future that fulfills when the ruleset is created and released. 87 | Future releaseFirestoreRulesetFromSource(String source) async { 88 | final rulesFile = RulesFile(name: 'firestore.rules', content: source); 89 | final ruleset = await createRuleset(rulesFile); 90 | 91 | await releaseFirestoreRuleset(ruleset.name); 92 | 93 | return ruleset; 94 | } 95 | 96 | /// Applies the specified [Ruleset] ruleset 97 | /// to Cloud Firestore. 98 | /// 99 | /// [ruleset] - Name of the ruleset to apply. 100 | /// Returns a future that fulfills when the ruleset is released. 101 | Future releaseFirestoreRuleset(String ruleset) async { 102 | await _client.updateOrCreateRelease(_cloudFirestore, ruleset); 103 | } 104 | 105 | /// Gets the [Ruleset] currently applied to a 106 | /// Cloud Storage bucket. Rejects with a `not-found` error if no ruleset is applied 107 | /// on the bucket. 108 | /// 109 | /// Returns a future that fulfills with the Cloud Storage ruleset. 110 | Future getStorageRuleset(String bucket) async { 111 | final bucketName = bucket; 112 | final ruleset = 113 | await _getRulesetForRelease('$_firebaseStorage/$bucketName'); 114 | 115 | return ruleset; 116 | } 117 | 118 | /// Creates a new [Ruleset] from the given 119 | /// source, and applies it to a Cloud Storage bucket. 120 | /// 121 | /// [source] - Rules source to apply. 122 | /// Returns a future that fulfills when the ruleset is created and released. 123 | Future releaseStorageRulesetFromSource( 124 | String source, 125 | String bucket, 126 | ) async { 127 | final rulesFile = RulesFile(name: 'storage.rules', content: source); 128 | final ruleset = await createRuleset(rulesFile); 129 | 130 | await releaseStorageRuleset(ruleset.name, bucket); 131 | 132 | return ruleset; 133 | } 134 | 135 | /// Applies the specified [Ruleset] ruleset 136 | /// to a Cloud Storage bucket. 137 | /// 138 | /// [ruleset] - Name of the ruleset to apply or a [RulesetMetadata] object 139 | /// containing the name. 140 | /// Returns a future that fulfills when the ruleset is released. 141 | Future releaseStorageRuleset(String ruleset, String bucket) async { 142 | await _client.updateOrCreateRelease('$_firebaseStorage/$bucket', ruleset); 143 | } 144 | 145 | /// Creates a new [Ruleset] from the given [RulesFile]. 146 | /// 147 | /// [file] - Rules file to include in the new [Ruleset]. 148 | /// Returns a future that fulfills with the newly created [Ruleset]. 149 | Future createRuleset(RulesFile file) async { 150 | final ruleset = RulesetContent( 151 | source: RulesetSource( 152 | files: [file], 153 | ), 154 | ); 155 | 156 | final rulesetResponse = await _client.createRuleset(ruleset); 157 | return Ruleset._fromResponse(rulesetResponse); 158 | } 159 | 160 | /// Deletes the [Ruleset] identified by the given 161 | /// name. The input name should be the short name string without the project ID 162 | /// prefix. For example, to delete the `projects/project-id/rulesets/my-ruleset`, 163 | /// pass the short name "my-ruleset". Rejects with a `not-found` error if the 164 | /// specified [Ruleset] cannot be found. 165 | /// 166 | /// [name] - Name of the [Ruleset] to delete. 167 | /// Returns a future that fulfills when the [Ruleset] is deleted. 168 | Future deleteRuleset(String name) { 169 | return _client.deleteRuleset(name); 170 | } 171 | 172 | /// Retrieves a page of ruleset metadata. 173 | /// 174 | /// [pageSize] - The page size, 100 if undefined. This is also the maximum allowed 175 | /// limit. 176 | /// [nextPageToken] - The next page token. If not specified, returns rulesets 177 | /// starting without any offset. 178 | /// Returns a future that fulfills with a page of rulesets. 179 | Future listRulesetMetadata({ 180 | int pageSize = 100, 181 | String? nextPageToken, 182 | }) async { 183 | final response = await _client.listRulesets( 184 | pageSize: pageSize, 185 | pageToken: nextPageToken, 186 | ); 187 | return RulesetMetadataList._fromResponse(response); 188 | } 189 | 190 | Future _getRulesetForRelease(String releaseName) async { 191 | final release = await _client.getRelease(releaseName); 192 | final rulesetName = release.rulesetName; 193 | 194 | return getRuleset(_stripProjectIdPrefix(rulesetName)); 195 | } 196 | } 197 | 198 | String _stripProjectIdPrefix(String name) => name.split('/').last; 199 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/security_rules/security_rules_api_internals.dart: -------------------------------------------------------------------------------- 1 | import 'package:googleapis/firebaserules/v1.dart' as firebase_rules_v1; 2 | import 'package:meta/meta.dart'; 3 | 4 | import '../app.dart'; 5 | import 'security_rules.dart'; 6 | import 'security_rules_internals.dart'; 7 | 8 | class Release { 9 | Release._({ 10 | required this.name, 11 | required this.rulesetName, 12 | required this.createTime, 13 | required this.updateTime, 14 | }); 15 | 16 | final String name; 17 | final String rulesetName; 18 | final String? createTime; 19 | final String? updateTime; 20 | } 21 | 22 | class RulesetContent { 23 | RulesetContent({required this.source}); 24 | 25 | final RulesetSource source; 26 | } 27 | 28 | class RulesetSource { 29 | RulesetSource({required this.files}); 30 | 31 | factory RulesetSource._fromSource(firebase_rules_v1.Source source) { 32 | return RulesetSource( 33 | files: [ 34 | for (final file in source.files ?? []) 35 | RulesFile(name: file.name!, content: file.content!), 36 | ], 37 | ); 38 | } 39 | 40 | final List files; 41 | } 42 | 43 | class RulesetResponse extends RulesetContent { 44 | factory RulesetResponse._from(firebase_rules_v1.Ruleset response) { 45 | return RulesetResponse._( 46 | name: response.name!, 47 | createTime: response.createTime!, 48 | source: RulesetSource._fromSource( 49 | response.source ?? firebase_rules_v1.Source(), 50 | ), 51 | ); 52 | } 53 | RulesetResponse._({ 54 | required this.name, 55 | required this.createTime, 56 | required super.source, 57 | }); 58 | 59 | final String name; 60 | final String createTime; 61 | } 62 | 63 | class ListRulesetsResponse { 64 | ListRulesetsResponse._({ 65 | required this.rulesets, 66 | this.nextPageToken, 67 | }); 68 | 69 | final List rulesets; 70 | final String? nextPageToken; 71 | } 72 | 73 | @internal 74 | class SecurityRulesApiClient { 75 | SecurityRulesApiClient(this.app); 76 | 77 | final FirebaseAdminApp app; 78 | String? projectIdPrefix; 79 | 80 | Future _v1( 81 | Future Function(firebase_rules_v1.FirebaseRulesApi client) fn, 82 | ) async { 83 | try { 84 | return await fn(firebase_rules_v1.FirebaseRulesApi(await app.client)); 85 | } on FirebaseSecurityRulesException { 86 | rethrow; 87 | } on firebase_rules_v1.DetailedApiRequestError catch (e, stack) { 88 | switch (e.jsonResponse) { 89 | case {'error': {'status': final status}}: 90 | final code = _errorMapping[status]; 91 | if (code == null) break; 92 | 93 | Error.throwWithStackTrace( 94 | FirebaseSecurityRulesException(code, e.message), 95 | stack, 96 | ); 97 | } 98 | 99 | Error.throwWithStackTrace( 100 | FirebaseSecurityRulesException( 101 | FirebaseSecurityRulesErrorCode.unknownError, 102 | 'Unexpected error: $e', 103 | ), 104 | stack, 105 | ); 106 | } catch (e, stack) { 107 | Error.throwWithStackTrace( 108 | FirebaseSecurityRulesException( 109 | FirebaseSecurityRulesErrorCode.unknownError, 110 | 'Unexpected error: $e', 111 | ), 112 | stack, 113 | ); 114 | } 115 | } 116 | 117 | Future getRuleset(String name) { 118 | return _v1((api) async { 119 | final response = await api.projects.rulesets 120 | .get('projects/${app.projectId}/rulesets/$name'); 121 | 122 | return RulesetResponse._from(response); 123 | }); 124 | } 125 | 126 | Future createRuleset(RulesetContent ruleset) { 127 | firebase_rules_v1.Ruleset toApiRuleset() { 128 | return firebase_rules_v1.Ruleset( 129 | source: firebase_rules_v1.Source( 130 | files: ruleset.source.files 131 | .map( 132 | (file) => firebase_rules_v1.File( 133 | name: file.name, 134 | content: file.content, 135 | ), 136 | ) 137 | .toList(), 138 | ), 139 | ); 140 | } 141 | 142 | return _v1((api) async { 143 | final response = await api.projects.rulesets.create( 144 | toApiRuleset(), 145 | 'projects/${app.projectId}', 146 | ); 147 | 148 | return RulesetResponse._( 149 | name: response.name!, 150 | createTime: response.createTime!, 151 | source: RulesetSource._fromSource(response.source!), 152 | ); 153 | }); 154 | } 155 | 156 | Future updateOrCreateRelease(String name, String rulesetName) async { 157 | try { 158 | return await updateRelease(name, rulesetName); 159 | } on FirebaseSecurityRulesException catch (e) { 160 | if (e.code == 161 | 'security-rules/${FirebaseSecurityRulesErrorCode.notFound}') { 162 | return createRelease(name, rulesetName); 163 | } 164 | rethrow; 165 | } 166 | } 167 | 168 | Future deleteRuleset(String name) { 169 | return _v1((api) async { 170 | await api.projects.rulesets 171 | .delete('projects/${app.projectId}/rulesets/$name'); 172 | }); 173 | } 174 | 175 | Future listRulesets({ 176 | int pageSize = 100, 177 | String? pageToken, 178 | }) { 179 | return _v1((api) async { 180 | if (pageSize < 1 || pageSize > 100) { 181 | throw FirebaseSecurityRulesException( 182 | FirebaseSecurityRulesErrorCode.invalidArgument, 183 | 'Page size must be between 1 and 100.', 184 | ); 185 | } 186 | 187 | final response = await api.projects.rulesets.list( 188 | 'projects/${app.projectId}', 189 | pageSize: pageSize, 190 | pageToken: pageToken, 191 | ); 192 | 193 | return ListRulesetsResponse._( 194 | rulesets: response.rulesets!.map(RulesetResponse._from).toList(), 195 | nextPageToken: response.nextPageToken, 196 | ); 197 | }); 198 | } 199 | 200 | Future getRelease(String name) { 201 | return _v1((api) async { 202 | final response = await api.projects.releases 203 | .get('projects/${app.projectId}/releases/$name'); 204 | 205 | return Release._( 206 | name: response.name!, 207 | rulesetName: response.rulesetName!, 208 | createTime: response.createTime, 209 | updateTime: response.updateTime, 210 | ); 211 | }); 212 | } 213 | 214 | Future updateRelease(String name, String rulesetName) { 215 | return _v1((api) async { 216 | final response = await api.projects.releases.patch( 217 | firebase_rules_v1.UpdateReleaseRequest( 218 | release: firebase_rules_v1.Release( 219 | name: 'projects/${app.projectId}/releases/$name', 220 | rulesetName: 'projects/${app.projectId}/rulesets/$rulesetName', 221 | ), 222 | ), 223 | 'projects/${app.projectId}/releases/$name', 224 | ); 225 | 226 | return Release._( 227 | name: response.name!, 228 | rulesetName: response.rulesetName!, 229 | createTime: response.createTime, 230 | updateTime: response.updateTime, 231 | ); 232 | }); 233 | } 234 | 235 | Future createRelease(String name, String rulesetName) { 236 | return _v1((api) async { 237 | final response = await api.projects.releases.create( 238 | firebase_rules_v1.Release( 239 | name: 'projects/${app.projectId}/releases/$name', 240 | rulesetName: 'projects/${app.projectId}/rulesets/$rulesetName', 241 | ), 242 | 'projects/${app.projectId}', 243 | ); 244 | 245 | return Release._( 246 | name: response.name!, 247 | rulesetName: response.rulesetName!, 248 | createTime: response.createTime, 249 | updateTime: response.updateTime, 250 | ); 251 | }); 252 | } 253 | } 254 | 255 | const _errorMapping = { 256 | 'INVALID_ARGUMENT': FirebaseSecurityRulesErrorCode.invalidArgument, 257 | 'NOT_FOUND': FirebaseSecurityRulesErrorCode.notFound, 258 | 'RESOURCE_EXHAUSTED': FirebaseSecurityRulesErrorCode.resourceExhausted, 259 | 'UNAUTHENTICATED': FirebaseSecurityRulesErrorCode.authenticationError, 260 | 'UNKNOWN': FirebaseSecurityRulesErrorCode.unknownError, 261 | }; 262 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/security_rules/security_rules_internals.dart: -------------------------------------------------------------------------------- 1 | import '../app.dart'; 2 | 3 | enum FirebaseSecurityRulesErrorCode { 4 | alreadyExists('already-exists'), 5 | authenticationError('authentication-error'), 6 | internalError('internal-error'), 7 | invalidArgument('invalid-argument'), 8 | invalidServerResponse('invalid-server-response'), 9 | notFound('not-found'), 10 | resourceExhausted('resource-exhausted'), 11 | serviceUnavailable('service-unavailable'), 12 | unknownError('unknown-error'); 13 | 14 | const FirebaseSecurityRulesErrorCode(this.value); 15 | final String value; 16 | } 17 | 18 | class FirebaseSecurityRulesException extends FirebaseAdminException { 19 | FirebaseSecurityRulesException( 20 | FirebaseSecurityRulesErrorCode code, 21 | String? message, 22 | ) : super('security-rules', code.value, message); 23 | } 24 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/utils/crypto_signer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:asn1lib/asn1lib.dart'; 5 | import 'package:googleapis/iamcredentials/v1.dart' as iam_credentials_v1; 6 | import 'package:googleapis_auth/googleapis_auth.dart' as auth; 7 | import 'package:http/http.dart' as http; 8 | import 'package:meta/meta.dart'; 9 | import 'package:pem/pem.dart'; 10 | import 'package:pointycastle/export.dart' as pointy; 11 | 12 | import '../../dart_firebase_admin.dart'; 13 | 14 | Future _v1( 15 | FirebaseAdminApp app, 16 | Future Function(iam_credentials_v1.IAMCredentialsApi client) fn, 17 | ) async { 18 | try { 19 | return await fn( 20 | iam_credentials_v1.IAMCredentialsApi(await app.client), 21 | ); 22 | } on iam_credentials_v1.ApiRequestError catch (e) { 23 | throw CryptoSignerException( 24 | CryptoSignerErrorCode.serverError, 25 | e.message ?? 'Unknown error', 26 | ); 27 | } 28 | } 29 | 30 | @internal 31 | abstract class CryptoSigner { 32 | static CryptoSigner fromApp(FirebaseAdminApp app) { 33 | final credential = app.credential; 34 | final serviceAccountCredentials = credential.serviceAccountCredentials; 35 | if (serviceAccountCredentials != null) { 36 | return _ServiceAccountSigner(serviceAccountCredentials); 37 | } 38 | 39 | return _IAMSigner(app); 40 | } 41 | 42 | /// The name of the signing algorithm. 43 | String get algorithm; 44 | 45 | /// Cryptographically signs a buffer of data. 46 | Future sign(Uint8List buffer); 47 | 48 | /// Returns the ID of the service account used to sign tokens. 49 | Future getAccountId(); 50 | } 51 | 52 | class _IAMSigner implements CryptoSigner { 53 | _IAMSigner(this.app) : _serviceAccountId = app.credential.serviceAccountId; 54 | 55 | @override 56 | String get algorithm => 'RS256'; 57 | 58 | final FirebaseAdminApp app; 59 | String? _serviceAccountId; 60 | 61 | @override 62 | Future getAccountId() async { 63 | if (_serviceAccountId case final serviceAccountId? 64 | when serviceAccountId.isNotEmpty) { 65 | return serviceAccountId; 66 | } 67 | final response = await http.get( 68 | Uri.parse( 69 | 'http://metadata/computeMetadata/v1/instance/service-accounts/default/email', 70 | ), 71 | headers: { 72 | 'Metadata-Flavor': 'Google', 73 | }, 74 | ); 75 | 76 | if (response.statusCode != 200) { 77 | throw CryptoSignerException( 78 | CryptoSignerErrorCode.invalidCredential, 79 | 'Failed to determine service account. Make sure to initialize ' 80 | 'the SDK with a service account credential. Alternatively specify a service ' 81 | 'account with iam.serviceAccounts.signBlob permission. Original error: ${response.body}', 82 | ); 83 | } 84 | 85 | return _serviceAccountId = response.body; 86 | } 87 | 88 | @override 89 | Future sign(Uint8List buffer) async { 90 | final serviceAccount = await getAccountId(); 91 | 92 | final response = await _v1(app, (client) { 93 | return client.projects.serviceAccounts.signBlob( 94 | iam_credentials_v1.SignBlobRequest( 95 | payload: base64Encode(buffer), 96 | ), 97 | 'projects/-/serviceAccounts/$serviceAccount', 98 | ); 99 | }); 100 | 101 | // Response from IAM is base64 encoded. Decode it into a buffer and return. 102 | return base64Decode(response.signedBlob!); 103 | } 104 | } 105 | 106 | /// A CryptoSigner implementation that uses an explicitly specified service account private key to 107 | /// sign data. Performs all operations locally, and does not make any RPC calls. 108 | class _ServiceAccountSigner implements CryptoSigner { 109 | _ServiceAccountSigner(this.credential); 110 | 111 | final auth.ServiceAccountCredentials credential; 112 | 113 | @override 114 | String get algorithm => 'RS256'; 115 | 116 | @override 117 | Future getAccountId() async => credential.email; 118 | 119 | @override 120 | Future sign(Uint8List buffer) async { 121 | final signer = pointy.Signer('SHA-256/RSA'); 122 | final privateParams = pointy.PrivateKeyParameter( 123 | parseRSAPrivateKey(credential.privateKey), 124 | ); 125 | 126 | signer.init(true, privateParams); // `true` for signing mode 127 | 128 | final signature = signer.generateSignature(buffer) as pointy.RSASignature; 129 | 130 | return signature.bytes; 131 | 132 | // print(credential.privateKey); 133 | // final key = utf8.encode(credential.privateKey); 134 | // final hmac = Hmac(sha256, key); 135 | // final digest = hmac.convert(buffer); 136 | 137 | // return Uint8List.fromList(digest.bytes); 138 | } 139 | 140 | /// Parses a PEM private key into an `RSAPrivateKey` 141 | pointy.RSAPrivateKey parseRSAPrivateKey(String pemStr) { 142 | final pem = PemCodec(PemLabel.privateKey).decode(pemStr); 143 | 144 | var asn1Parser = ASN1Parser(Uint8List.fromList(pem)); 145 | final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; 146 | final privateKey = topLevelSeq.elements[2]; 147 | 148 | asn1Parser = ASN1Parser(privateKey.contentBytes()); 149 | final pkSeq = asn1Parser.nextObject() as ASN1Sequence; 150 | 151 | final modulus = pkSeq.elements[1] as ASN1Integer; 152 | final privateExponent = pkSeq.elements[3] as ASN1Integer; 153 | final p = pkSeq.elements[4] as ASN1Integer; 154 | final q = pkSeq.elements[5] as ASN1Integer; 155 | 156 | return pointy.RSAPrivateKey( 157 | modulus.valueAsBigInteger, 158 | privateExponent.valueAsBigInteger, 159 | p.valueAsBigInteger, 160 | q.valueAsBigInteger, 161 | ); 162 | 163 | // final keyBytes = PemCodec(PemLabel.privateKey).decode(pemStr); 164 | // // final base64Key = pem 165 | // // .replaceAll("-----BEGIN PRIVATE KEY-----", "") 166 | // // .replaceAll("-----END PRIVATE KEY-----", "") 167 | // // .replaceAll("\n", "") 168 | // // .replaceAll("\r", ""); 169 | 170 | // // final keyBytes = base64Decode(base64Key); 171 | // final asn1Parser = ASN1Parser(Uint8List.fromList(keyBytes)); 172 | // final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; 173 | // final keySeq = topLevelSeq.elements![2] as ASN1Sequence; 174 | 175 | // final modulus = (keySeq.elements![0] as ASN1Integer).integer; 176 | // final privateExponent = (keySeq.elements![3] as ASN1Integer).integer; 177 | 178 | // return RSAPrivateKey(modulus!, privateExponent!, null, null); 179 | } 180 | } 181 | 182 | @internal 183 | class CryptoSignerException implements Exception { 184 | CryptoSignerException(this.code, this.message); 185 | 186 | final String code; 187 | final String message; 188 | 189 | @override 190 | String toString() => 'CryptoSignerException($code, $message)'; 191 | } 192 | 193 | /// Crypto Signer error codes and their default messages. 194 | @internal 195 | class CryptoSignerErrorCode { 196 | static const invalidArgument = 'invalid-argument'; 197 | static const internalError = 'internal-error'; 198 | static const invalidCredential = 'invalid-credential'; 199 | static const serverError = 'server-error'; 200 | } 201 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/utils/error.dart: -------------------------------------------------------------------------------- 1 | class ErrorInfo { 2 | ErrorInfo({required this.code, required this.message}); 3 | 4 | final String code; 5 | final String message; 6 | } 7 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/utils/index.dart: -------------------------------------------------------------------------------- 1 | class ParsedResource { 2 | ParsedResource({ 3 | this.projectId, 4 | this.locationId, 5 | required this.resourceId, 6 | }); 7 | 8 | /// Parses the top level resources of a given resource name. 9 | /// Supports both full and partial resources names, example: 10 | /// `locations/{location}/functions/{functionName}`, 11 | /// `projects/{project}/locations/{location}/functions/{functionName}`, or {functionName} 12 | /// Does not support deeply nested resource names. 13 | /// 14 | /// [resourceName] - The resource name string. 15 | /// [resourceIdKey] - The key of the resource name to be parsed. 16 | /// Returns a parsed resource name object. 17 | factory ParsedResource.parse(String resourceName, String resourceIdKey) { 18 | if (!resourceName.contains('/')) { 19 | return ParsedResource(resourceId: resourceName); 20 | } 21 | final channelNameRegex = RegExp( 22 | '^(projects/([^/]+)/)?locations/([^/]+)/$resourceIdKey/([^/]+)\$', 23 | ); 24 | final match = channelNameRegex.firstMatch(resourceName); 25 | if (match == null) { 26 | throw const FormatException('Invalid resource name format.'); 27 | } 28 | 29 | final projectId = match[2]; 30 | final locationId = match[3]; 31 | final resourceId = match[4]; 32 | 33 | return ParsedResource( 34 | projectId: projectId, 35 | locationId: locationId, 36 | resourceId: resourceId!, 37 | ); 38 | } 39 | 40 | String? projectId; 41 | String? locationId; 42 | String resourceId; 43 | } 44 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/utils/jwt.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; 4 | import 'package:http/http.dart' as http; 5 | import 'package:jose/jose.dart'; 6 | import 'package:meta/meta.dart'; 7 | 8 | const algorithmRS256 = 'RS256'; 9 | 10 | /// Class for verifying unsigned (emulator) JWTs. 11 | class EmulatorSignatureVerifier implements SignatureVerifier { 12 | @override 13 | Future verify(String token) async { 14 | // Signature checks skipped for emulator; no need to fetch public keys. 15 | 16 | try { 17 | verifyJwtSignature( 18 | token, 19 | SecretKey(''), 20 | ); 21 | } on JWTInvalidException catch (e) { 22 | // Emulator tokens have "alg": "none" 23 | if (e.message == 'unknown algorithm') return; 24 | if (e.message == 'invalid signature') return; 25 | rethrow; 26 | } 27 | } 28 | } 29 | 30 | @internal 31 | class DecodedToken { 32 | DecodedToken({required this.header, required this.payload}); 33 | 34 | final Map header; 35 | final Map payload; 36 | } 37 | 38 | abstract class SignatureVerifier { 39 | Future verify(String token); 40 | } 41 | 42 | abstract class KeyFetcher { 43 | Future fetchPublicKeys(); 44 | } 45 | 46 | class UrlKeyFetcher implements KeyFetcher { 47 | UrlKeyFetcher(this.clientCert); 48 | 49 | final Uri clientCert; 50 | 51 | JsonWebKeyStore? _publicKeys; 52 | late DateTime _publicKeysExpireAt; 53 | 54 | @override 55 | Future fetchPublicKeys() async { 56 | if (_shouldRefresh()) return refresh(); 57 | return _publicKeys!; 58 | } 59 | 60 | bool _shouldRefresh() { 61 | if (_publicKeys == null) return true; 62 | return _publicKeysExpireAt.isBefore(DateTime.now()); 63 | } 64 | 65 | Future refresh() async { 66 | final response = await http.get(clientCert); 67 | final json = jsonDecode(response.body) as Map; 68 | final error = json['error']; 69 | if (error != null) { 70 | var errorMessage = 'Error fetching public keys for Google certs: $error'; 71 | final description = json['error_description']; 72 | if (description != null) { 73 | errorMessage += ' ($description)'; 74 | } 75 | throw Exception(errorMessage); 76 | } 77 | 78 | // reset expire at from previous set of keys. 79 | _publicKeysExpireAt = DateTime(0); 80 | final cacheControl = response.headers['cache-control']; 81 | if (cacheControl != null) { 82 | final parts = cacheControl.split(','); 83 | for (final part in parts) { 84 | final subParts = part.trim().split('='); 85 | if (subParts[0] == 'max-age') { 86 | final maxAge = int.parse(subParts[1]); 87 | // Is "seconds" correct? 88 | _publicKeysExpireAt = DateTime.now().add(Duration(seconds: maxAge)); 89 | } 90 | } 91 | } 92 | 93 | final store = _publicKeys = JsonWebKeyStore(); 94 | 95 | for (final entry in json.entries) { 96 | final key = JsonWebKey.fromPem(entry.value! as String, keyId: entry.key); 97 | store.addKey(key); 98 | } 99 | 100 | return store; 101 | } 102 | } 103 | 104 | class JwksFetcher implements KeyFetcher { 105 | JwksFetcher(this.jwksUrl); 106 | final Uri jwksUrl; 107 | JsonWebKeyStore? _publicKeys; 108 | int _publicKeysExpireAt = 0; 109 | static const int hourInMilliseconds = 6 * 60 * 60 * 1000; // 6 hours 110 | 111 | @override 112 | Future fetchPublicKeys() async { 113 | if (_shouldRefresh) return refresh(); 114 | 115 | return _publicKeys!; 116 | } 117 | 118 | bool get _shouldRefresh { 119 | return _publicKeys == null || 120 | _publicKeysExpireAt <= DateTime.now().millisecondsSinceEpoch; 121 | } 122 | 123 | Future refresh() async { 124 | final response = await http.get(jwksUrl); 125 | if (response.statusCode != 200) { 126 | throw Exception('Failed to fetch JWKS'); 127 | } 128 | 129 | final jwks = jsonDecode(response.body) as Map; 130 | final keys = JsonWebKeySet.fromJson(jwks).keys; 131 | 132 | // Reset expire time 133 | _publicKeysExpireAt = 0; 134 | 135 | // Extract signing keys 136 | final store = _publicKeys = JsonWebKeyStore(); 137 | keys.forEach(store.addKey); 138 | 139 | // Set new expiration time 140 | _publicKeysExpireAt = 141 | DateTime.now().millisecondsSinceEpoch + hourInMilliseconds; 142 | 143 | return store; 144 | } 145 | } 146 | 147 | class PublicKeySignatureVerifier implements SignatureVerifier { 148 | PublicKeySignatureVerifier(this.keyFetcher); 149 | 150 | PublicKeySignatureVerifier.withCertificateUrl(Uri clientCert) 151 | : this(UrlKeyFetcher(clientCert)); 152 | 153 | factory PublicKeySignatureVerifier.withJwksUrl(Uri jwksUrl) { 154 | return PublicKeySignatureVerifier(JwksFetcher(jwksUrl)); 155 | } 156 | 157 | final KeyFetcher keyFetcher; 158 | 159 | /// Verifies a JWT token. 160 | /// 161 | /// This verifies the token's signature. The signing key is selected using the 162 | /// 'kid' claim in the token's header. 163 | /// The token's expiration is also verified. 164 | @override 165 | Future verify(String token) async { 166 | try { 167 | final jwt = JWT.decode(token); 168 | final kid = jwt.header?['kid'] as String?; 169 | 170 | if (kid == null) { 171 | throw JwtException( 172 | JwtErrorCode.noKidInHeader, 173 | 'no-kid-in-header-error', 174 | ); 175 | } 176 | 177 | final store = await keyFetcher.fetchPublicKeys(); 178 | 179 | try { 180 | await JsonWebToken.decodeAndVerify(token, store); 181 | } catch (e, stackTrace) { 182 | Error.throwWithStackTrace( 183 | JwtException( 184 | JwtErrorCode.invalidSignature, 185 | 'Error while verifying signature of Firebase ID token: $e', 186 | ), 187 | stackTrace, 188 | ); 189 | } 190 | 191 | // At this point most JWTException's should have been caught in 192 | // verifyJwtSignature, but we could still get some from JWT.decode above 193 | } on JWTException catch (e) { 194 | throw JwtException( 195 | JwtErrorCode.unknown, 196 | e is JWTUndefinedException ? e.message : '${e.runtimeType}: e.message', 197 | ); 198 | } 199 | } 200 | } 201 | 202 | sealed class SecretOrPublicKey {} 203 | 204 | /// Decodes general purpose Firebase JWTs. 205 | /// 206 | /// [jwtToken] - JWT token to be decoded. 207 | /// 208 | /// Returns a decoded token containing the header and payload. 209 | Future decodeJwt(String jwtToken) async { 210 | final fullDecodedToken = JWT.decode(jwtToken); 211 | 212 | return DecodedToken( 213 | header: fullDecodedToken.header ?? {}, 214 | payload: Map.from(fullDecodedToken.payload as Map), 215 | ); 216 | } 217 | 218 | @internal 219 | void verifyJwtSignature( 220 | String token, 221 | JWTKey key, { 222 | Duration? issueAt, 223 | Audience? audience, 224 | String? subject, 225 | String? issuer, 226 | String? jwtId, 227 | }) { 228 | try { 229 | JWT.verify( 230 | token, 231 | key, 232 | issueAt: issueAt, 233 | audience: audience, 234 | subject: subject, 235 | issuer: issuer, 236 | jwtId: jwtId, 237 | ); 238 | } on JWTExpiredException catch (e, stackTrace) { 239 | Error.throwWithStackTrace( 240 | JwtException( 241 | JwtErrorCode.tokenExpired, 242 | 'The provided token has expired. Get a fresh token from your ' 243 | 'client app and try again.', 244 | ), 245 | stackTrace, 246 | ); 247 | } 248 | } 249 | 250 | /// Jwt error code structure. 251 | class JwtException implements Exception { 252 | JwtException(this.code, this.message); 253 | 254 | final JwtErrorCode code; 255 | final String message; 256 | } 257 | 258 | /// JWT error codes. 259 | enum JwtErrorCode { 260 | invalidArgument('invalid-argument'), 261 | invalidCredential('invalid-credential'), 262 | tokenExpired('token-expired'), 263 | invalidSignature('invalid-token'), 264 | noMatchingKid('no-matching-kid-error'), 265 | noKidInHeader('no-kid-error'), 266 | keyFetchError('key-fetch-error'), 267 | unknown('unknown'); 268 | 269 | const JwtErrorCode(this.value); 270 | 271 | final String value; 272 | } 273 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/utils/utils.dart: -------------------------------------------------------------------------------- 1 | /// Generates the update mask for the provided object. 2 | /// Note this will ignore the last key with value undefined. 3 | List generateUpdateMask( 4 | Object? obj, { 5 | List terminalPaths = const [], 6 | String root = '', 7 | }) { 8 | if (obj is! Map) return []; 9 | 10 | final updateMask = []; 11 | for (final key in obj.keys) { 12 | final nextPath = root.isEmpty ? '$root.$key' : '$key'; 13 | // We hit maximum path. 14 | // Consider switching to Set if the list grows too large. 15 | if (terminalPaths.contains(nextPath)) { 16 | // Add key and stop traversing this branch. 17 | updateMask.add('$key'); 18 | } else { 19 | final maskList = generateUpdateMask( 20 | obj[key], 21 | terminalPaths: terminalPaths, 22 | root: nextPath, 23 | ); 24 | if (maskList.isNotEmpty) { 25 | for (final mask in maskList) { 26 | updateMask.add('$key.$mask'); 27 | } 28 | } else { 29 | updateMask.add('$key'); 30 | } 31 | } 32 | } 33 | return updateMask; 34 | } 35 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/lib/src/utils/validator.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../auth.dart'; 4 | 5 | /// Validates that a string is a valid phone number. 6 | @internal 7 | bool isPhoneNumber(String phoneNumber) { 8 | // Phone number validation is very lax here. Backend will enforce E.164 9 | // spec compliance and will normalize accordingly. 10 | // The phone number string must be non-empty and starts with a plus sign. 11 | final re1 = RegExp(r'^\+'); 12 | // The phone number string must contain at least one alphanumeric character. 13 | final re2 = RegExp(r'[\da-zA-Z]+'); 14 | return re1.hasMatch(phoneNumber) && re2.hasMatch(phoneNumber); 15 | } 16 | 17 | /// Verifies that a string is a valid phone number. Throws otherwise. 18 | @internal 19 | void assertIsPhoneNumber(String phoneNumber) { 20 | if (!isPhoneNumber(phoneNumber)) { 21 | throw FirebaseAuthAdminException(AuthClientErrorCode.invalidPhoneNumber); 22 | } 23 | } 24 | 25 | /// Validates that a string is a valid email. 26 | @internal 27 | bool isEmail(String email) { 28 | // There must at least one character before the @ symbol and another after. 29 | final re = RegExp(r'^[^@]+@[^@]+$'); 30 | return re.hasMatch(email); 31 | } 32 | 33 | /// Verifies that a string is a valid email. Throws otherwise. 34 | @internal 35 | void assertIsEmail(String email) { 36 | if (!isEmail(email)) { 37 | throw FirebaseAuthAdminException(AuthClientErrorCode.invalidEmail); 38 | } 39 | } 40 | 41 | /// Validates that a string is a valid Firebase Auth uid. 42 | @internal 43 | bool isUid(String uid) => uid.isNotEmpty && uid.length <= 128; 44 | 45 | /// Verifies that a string is a valid Firebase Auth uid. Throws otherwise. 46 | @internal 47 | void assertIsUid(String uid) { 48 | if (!isUid(uid)) { 49 | throw FirebaseAuthAdminException(AuthClientErrorCode.invalidUid); 50 | } 51 | } 52 | 53 | /// Validates that the provided topic is a valid FCM topic name. 54 | bool isTopic(Object? topic) { 55 | if (topic is! String) return false; 56 | 57 | final validTopicRegExp = RegExp( 58 | r'^(\/topics\/)?(private\/)?[a-zA-Z0-9-_.~%]+$', 59 | ); 60 | return validTopicRegExp.hasMatch(topic); 61 | } 62 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_firebase_admin 2 | description: A Firebase Admin SDK implementation for Dart. 3 | version: 0.4.1 4 | homepage: "https://github.com/invertase/dart_firebase_admin" 5 | repository: "https://github.com/invertase/dart_firebase_admin" 6 | 7 | environment: 8 | sdk: ">=3.2.0 <4.0.0" 9 | 10 | dependencies: 11 | asn1lib: ^1.6.0 12 | collection: ^1.18.0 13 | dart_jsonwebtoken: ^3.0.0 14 | freezed_annotation: ^3.0.0 15 | googleapis: ^13.2.0 16 | googleapis_auth: ^1.3.0 17 | googleapis_beta: ^9.0.0 18 | http: ^1.0.0 19 | intl: ^0.20.0 20 | jose: ^0.3.4 21 | meta: ^1.9.1 22 | pem: ^2.0.5 23 | pointycastle: ^3.7.0 24 | 25 | dev_dependencies: 26 | build_runner: ^2.4.7 27 | file: ^7.0.0 28 | freezed: ^3.0.0 29 | mocktail: ^1.0.1 30 | path: ^1.9.1 31 | test: ^1.24.4 32 | uuid: ^4.0.0 33 | 34 | false_secrets: 35 | - /test/auth/jwt_test.dart 36 | - /test/client/get_id_token.js -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: ../../../analysis_options.yaml 2 | linter: 3 | rules: 4 | # Disabling inside tests for the sake of type testing 5 | omit_local_variable_types: false 6 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/app_check/app_check_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dart_firebase_admin/app_check.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import '../google_cloud_firestore/util/helpers.dart'; 7 | import '../mock.dart'; 8 | 9 | void main() { 10 | late AppCheck appCheck; 11 | 12 | setUpAll(registerFallbacks); 13 | 14 | setUp(() { 15 | final sdk = createApp(useEmulator: false); 16 | appCheck = AppCheck(sdk); 17 | }); 18 | 19 | final hasGoogleEnv = 20 | Platform.environment['GOOGLE_APPLICATION_CREDENTIALS'] != null; 21 | 22 | group('AppCheck', () { 23 | test( 24 | skip: hasGoogleEnv ? false : 'Requires GOOGLE_APPLICATION_CREDENTIALS', 25 | 'e2e', () async { 26 | final token = await appCheck 27 | .createToken('1:559949546715:android:13025aec6cc3243d0ab8fe'); 28 | 29 | await appCheck.verifyToken(token.token); 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/auth/auth_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:dart_firebase_admin/auth.dart'; 5 | import 'package:path/path.dart' as p; 6 | import 'package:test/test.dart'; 7 | 8 | import '../google_cloud_firestore/util/helpers.dart'; 9 | 10 | Future run( 11 | String executable, 12 | List arguments, { 13 | String? workDir, 14 | }) async { 15 | final process = await Process.run( 16 | executable, 17 | arguments, 18 | stdoutEncoding: utf8, 19 | workingDirectory: workDir, 20 | ); 21 | 22 | if (process.exitCode != 0) { 23 | throw Exception(process.stderr); 24 | } 25 | 26 | return process; 27 | } 28 | 29 | Future npmInstall({ 30 | String? workDir, 31 | }) async => 32 | run('npm', ['install'], workDir: workDir); 33 | 34 | /// Run test/client/get_id_token.js 35 | Future getIdToken() async { 36 | final path = p.join( 37 | Directory.current.path, 38 | 'test', 39 | 'client', 40 | ); 41 | 42 | await npmInstall(workDir: path); 43 | 44 | final process = await run( 45 | 'node', 46 | ['get_id_token.js'], 47 | workDir: path, 48 | ); 49 | 50 | return (process.stdout as String).trim(); 51 | } 52 | 53 | void main() { 54 | group('FirebaseAuth', () { 55 | group('verifyIdToken', () { 56 | test('in prod', () async { 57 | final app = createApp(useEmulator: false); 58 | final auth = Auth(app); 59 | 60 | final token = await getIdToken(); 61 | final decodedToken = await auth.verifyIdToken(token); 62 | 63 | expect(decodedToken.aud, 'dart-firebase-admin'); 64 | expect(decodedToken.uid, 'TmpgnnHo3JRjzQZjgBaYzQDyyZi2'); 65 | expect(decodedToken.sub, 'TmpgnnHo3JRjzQZjgBaYzQDyyZi2'); 66 | expect(decodedToken.email, 'foo@google.com'); 67 | expect(decodedToken.emailVerified, false); 68 | expect(decodedToken.phoneNumber, isNull); 69 | expect(decodedToken.firebase.identities, { 70 | 'email': ['foo@google.com'], 71 | }); 72 | expect(decodedToken.firebase.signInProvider, 'password'); 73 | }); 74 | }); 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/auth/integration_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dart_firebase_admin/auth.dart'; 4 | import 'package:http/http.dart'; 5 | import 'package:mocktail/mocktail.dart'; 6 | import 'package:test/test.dart'; 7 | import 'package:uuid/uuid.dart'; 8 | 9 | import '../google_cloud_firestore/util/helpers.dart'; 10 | import '../mock.dart'; 11 | 12 | const _uid = Uuid(); 13 | 14 | void main() { 15 | late Auth auth; 16 | 17 | setUp(() { 18 | final sdk = createApp(tearDown: () => cleanup(auth)); 19 | sdk.useEmulator(); 20 | auth = Auth(sdk); 21 | }); 22 | 23 | setUpAll(registerFallbacks); 24 | 25 | group('Error handling', () { 26 | for (final MapEntry(key: messagingError, value: code) 27 | in authServerToClientCode.entries) { 28 | test('converts $messagingError error codes', () async { 29 | final clientMock = ClientMock(); 30 | when(() => clientMock.send(any())).thenAnswer( 31 | (_) => Future.value( 32 | StreamedResponse( 33 | Stream.value( 34 | utf8.encode( 35 | jsonEncode({ 36 | 'error': {'message': messagingError}, 37 | }), 38 | ), 39 | ), 40 | 400, 41 | headers: { 42 | 'content-type': 'application/json', 43 | }, 44 | ), 45 | ), 46 | ); 47 | 48 | final app = createApp(client: clientMock); 49 | final handler = Auth(app); 50 | 51 | await expectLater( 52 | () => handler.getUser('123'), 53 | throwsA( 54 | isA() 55 | .having((e) => e.errorCode, 'errorCode', code) 56 | .having((e) => e.code, 'code', 'auth/${code.code}'), 57 | ), 58 | ); 59 | }); 60 | } 61 | }); 62 | 63 | group('createUser', () { 64 | test('supports no specified uid', () async { 65 | final user = await auth.createUser( 66 | CreateRequest(email: 'example@gmail.com'), 67 | ); 68 | 69 | expect(user.uid, isNotEmpty); 70 | expect(user.email, 'example@gmail.com'); 71 | }); 72 | 73 | test('supports specifying uid', () async { 74 | final user = await auth.createUser( 75 | CreateRequest( 76 | email: 'example@gmail.com', 77 | uid: '42', 78 | ), 79 | ); 80 | 81 | expect(user.uid, '42'); 82 | expect(user.email, 'example@gmail.com'); 83 | }); 84 | 85 | test('supports users with enrolled second factors', () async { 86 | const phoneNumber = '+16505550002'; 87 | 88 | final user = await auth.createUser( 89 | CreateRequest( 90 | email: 'example@gmail.com', 91 | multiFactor: MultiFactorCreateSettings( 92 | enrolledFactors: [ 93 | CreatePhoneMultiFactorInfoRequest( 94 | displayName: 'home phone', 95 | phoneNumber: phoneNumber, 96 | ), 97 | ], 98 | ), 99 | ), 100 | ); 101 | 102 | expect(user.email, 'example@gmail.com'); 103 | expect(user.multiFactor?.enrolledFactors, hasLength(1)); 104 | expect( 105 | user.multiFactor?.enrolledFactors 106 | .cast() 107 | .map((e) => (e.phoneNumber, e.displayName)), 108 | [(phoneNumber, 'home phone')], 109 | ); 110 | }); 111 | 112 | test('Fails when uid is already in use', () async { 113 | final user = await auth.createUser( 114 | CreateRequest(email: 'example@gmail.com'), 115 | ); 116 | 117 | final user2 = auth.createUser( 118 | CreateRequest( 119 | uid: user.uid, 120 | email: 'user2@gmail.com', 121 | ), 122 | ); 123 | 124 | expect( 125 | user2, 126 | throwsA( 127 | isA().having( 128 | (e) => e.errorCode, 129 | 'errorCode', 130 | AuthClientErrorCode.uidAlreadyExists, 131 | ), 132 | ), 133 | ); 134 | }); 135 | }); 136 | 137 | test('getUserByEmail', () async { 138 | final user = await auth.createUser( 139 | CreateRequest(email: 'example@gmail.com'), 140 | ); 141 | 142 | final user2 = await auth.getUserByEmail(user.email!); 143 | 144 | expect(user2.uid, user.uid); 145 | expect(user2.email, user.email); 146 | }); 147 | 148 | test('getUserByPhoneNumber', () async { 149 | const phoneNumber = '+16505550002'; 150 | final user = await auth.createUser( 151 | CreateRequest(phoneNumber: phoneNumber), 152 | ); 153 | 154 | final user2 = await auth.getUserByPhoneNumber(user.phoneNumber!); 155 | 156 | expect(user2.uid, user.uid); 157 | expect(user2.phoneNumber, user.phoneNumber); 158 | }); 159 | 160 | group('getUserByProviderUid', () { 161 | test('works', () async { 162 | final importUser = UserImportRecord( 163 | uid: 'import_${_uid.v4()}', 164 | email: 'user@example.com', 165 | phoneNumber: '+15555550000', 166 | providerData: [ 167 | UserProviderRequest( 168 | displayName: 'User Name', 169 | email: 'user@example.com', 170 | phoneNumber: '+15555550000', 171 | photoURL: 'http://example.com/user', 172 | providerId: 'google.com', 173 | uid: 'google_uid', 174 | ), 175 | ], 176 | ); 177 | 178 | await auth.importUsers( 179 | [importUser], 180 | ); 181 | 182 | final user = await auth.getUserByProviderUid( 183 | providerId: 'google.com', 184 | uid: 'google_uid', 185 | ); 186 | 187 | expect(user.uid, importUser.uid); 188 | }); 189 | }); 190 | 191 | group('updateUser', () { 192 | test('supports updating email', () async { 193 | final user = await auth.createUser( 194 | CreateRequest( 195 | email: 'testuser@example.com', 196 | ), 197 | ); 198 | 199 | final updatedUser = await auth.updateUser( 200 | user.uid, 201 | UpdateRequest( 202 | email: 'updateduser@example.com', 203 | ), 204 | ); 205 | 206 | expect(updatedUser.email, equals('updateduser@example.com')); 207 | 208 | final user2 = await auth.getUserByEmail(updatedUser.email!); 209 | expect(user2.uid, equals(user.uid)); 210 | }); 211 | }); 212 | } 213 | 214 | Future cleanup(Auth auth) async { 215 | if (!auth.app.isUsingEmulator) { 216 | throw Exception('Cannot cleanup non-emulator app'); 217 | } 218 | 219 | final users = await auth.listUsers(); 220 | await Future.wait([ 221 | for (final user in users.users) auth.deleteUser(user.uid), 222 | ]); 223 | } 224 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/auth/jwt_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_firebase_admin/src/utils/jwt.dart'; 2 | import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; 3 | import 'package:jose/jose.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group('PublicKeySignatureVerifier', () { 8 | final privateKey = RSAPrivateKey(''' 9 | -----BEGIN PRIVATE KEY----- 10 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj 11 | MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu 12 | NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ 13 | qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg 14 | p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR 15 | ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi 16 | VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV 17 | laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8 18 | sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H 19 | mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY 20 | dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw 21 | ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ 22 | DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T 23 | N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t 24 | 0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv 25 | t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU 26 | AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk 27 | 48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL 28 | DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK 29 | xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA 30 | mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh 31 | 2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz 32 | et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr 33 | VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD 34 | TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc 35 | dn/RsYEONbwQSjIfMPkvxF+8HQ== 36 | -----END PRIVATE KEY----- 37 | '''); 38 | final keyFetcher = _TestKeyFetcher(); 39 | final payload = { 40 | 'a': '1', 41 | 'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000, 42 | }; 43 | 44 | test('valid kid should pass', () async { 45 | final jwt = JWT( 46 | payload, 47 | header: {'kid': 'key1'}, 48 | ); 49 | final token = jwt.sign( 50 | privateKey, 51 | algorithm: JWTAlgorithm.RS256, 52 | ); 53 | await PublicKeySignatureVerifier(keyFetcher).verify(token); 54 | }); 55 | test('no kid should throw', () async { 56 | final jwt = JWT(payload); 57 | final token = jwt.sign( 58 | privateKey, 59 | algorithm: JWTAlgorithm.RS256, 60 | ); 61 | await expectLater( 62 | PublicKeySignatureVerifier(keyFetcher).verify(token), 63 | throwsA(isA()), 64 | ); 65 | }); 66 | test('invalid kid should throw', () async { 67 | final jwt = JWT( 68 | payload, 69 | header: {'kid': 'key2'}, 70 | ); 71 | final token = jwt.sign( 72 | privateKey, 73 | algorithm: JWTAlgorithm.RS256, 74 | ); 75 | await expectLater( 76 | PublicKeySignatureVerifier(keyFetcher).verify(token), 77 | throwsA(isA()), 78 | ); 79 | }); 80 | }); 81 | } 82 | 83 | class _TestKeyFetcher implements KeyFetcher { 84 | @override 85 | Future fetchPublicKeys() async { 86 | final store = JsonWebKeyStore(); 87 | 88 | const key = ''' 89 | -----BEGIN PUBLIC KEY----- 90 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo 91 | 4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u 92 | +qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh 93 | kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ 94 | 0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg 95 | cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc 96 | mwIDAQAB 97 | -----END PUBLIC KEY----- 98 | '''; 99 | 100 | store.addKey(JsonWebKey.fromPem(key, keyId: 'key1')); 101 | 102 | return store; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/auth/token_verifier_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_firebase_admin/src/auth.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('DecodedIdToken', () { 6 | test('.fromMap', () async { 7 | final idToken = DecodedIdToken.fromMap( 8 | { 9 | 'aud': 'mock-aud', 10 | 'auth_time': 1, 11 | 'email': 'mock-email', 12 | 'email_verified': true, 13 | 'exp': 1, 14 | 'firebase': { 15 | 'identities': { 16 | 'email': 'mock-email', 17 | }, 18 | 'sign_in_provider': 'mock-sign-in-provider', 19 | 'sign_in_second_factor': 'mock-sign-in-second-factor', 20 | 'second_factor_identifier': 'mock-second-factor-identifier', 21 | 'tenant': 'mock-tenant', 22 | }, 23 | 'iat': 1, 24 | 'iss': 'mock-iss', 25 | 'phone_number': 'mock-phone-number', 26 | 'picture': 'mock-picture', 27 | 'sub': 'mock-sub', 28 | }, 29 | ); 30 | expect(idToken.aud, 'mock-aud'); 31 | expect(idToken.authTime, DateTime.fromMillisecondsSinceEpoch(1000)); 32 | expect(idToken.email, 'mock-email'); 33 | expect(idToken.emailVerified, true); 34 | expect(idToken.exp, 1); 35 | expect(idToken.firebase.identities, {'email': 'mock-email'}); 36 | expect(idToken.firebase.signInProvider, 'mock-sign-in-provider'); 37 | expect(idToken.firebase.signInSecondFactor, 'mock-sign-in-second-factor'); 38 | expect( 39 | idToken.firebase.secondFactorIdentifier, 40 | 'mock-second-factor-identifier', 41 | ); 42 | expect(idToken.firebase.tenant, 'mock-tenant'); 43 | expect(idToken.iat, 1); 44 | expect(idToken.iss, 'mock-iss'); 45 | expect(idToken.phoneNumber, 'mock-phone-number'); 46 | expect(idToken.picture, 'mock-picture'); 47 | expect(idToken.sub, 'mock-sub'); 48 | expect(idToken.uid, 'mock-sub'); 49 | }); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/auth/user_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_firebase_admin/auth.dart'; 2 | import 'package:dart_firebase_admin/src/auth.dart' show UserMetadataToJson; 3 | import 'package:googleapis/identitytoolkit/v1.dart' as auth1; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group('UserMetadata', () { 8 | test('_toJson', () { 9 | final now = DateTime.now().toUtc(); 10 | 11 | final metadata = UserMetadata.fromResponse( 12 | auth1.GoogleCloudIdentitytoolkitV1UserInfo( 13 | createdAt: '0', 14 | lastLoginAt: '0', 15 | lastRefreshAt: now.toIso8601String(), 16 | ), 17 | ); 18 | 19 | final json = metadata.toJson(); 20 | expect( 21 | json, 22 | { 23 | 'lastSignInTime': '0', 24 | 'creationTime': '0', 25 | 'lastRefreshTime': now.toIso8601String(), 26 | }, 27 | ); 28 | 29 | final recoded = UserMetadata.fromResponse( 30 | auth1.GoogleCloudIdentitytoolkitV1UserInfo( 31 | createdAt: json['creationTime']! as String, 32 | lastLoginAt: json['lastSignInTime']! as String, 33 | lastRefreshAt: json['lastRefreshTime']! as String, 34 | ), 35 | ); 36 | 37 | expect(recoded.creationTime, metadata.creationTime); 38 | expect(recoded.lastSignInTime, metadata.lastSignInTime); 39 | expect(recoded.lastRefreshTime, metadata.lastRefreshTime); 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/client/get_id_token.js: -------------------------------------------------------------------------------- 1 | const { initializeApp } = require("firebase/app"); 2 | const { 3 | getAuth, 4 | signInWithEmailAndPassword, 5 | signOut, 6 | } = require("firebase/auth"); 7 | 8 | // Your web app's Firebase configuration 9 | const firebaseConfig = { 10 | apiKey: "AIzaSyCyxPmn7XCrAgnW2AnDjr9VWWXJ1AX-ouQ", 11 | authDomain: "dart-firebase-admin.firebaseapp.com", 12 | databaseURL: 13 | "https://dart-firebase-admin-default-rtdb.europe-west1.firebasedatabase.app", 14 | projectId: "dart-firebase-admin", 15 | storageBucket: "dart-firebase-admin.firebasestorage.app", 16 | messagingSenderId: "559949546715", 17 | appId: "1:559949546715:web:86bc35cdf9e2633c0ab8fe", 18 | }; 19 | 20 | const firebase = initializeApp(firebaseConfig); 21 | const auth = getAuth(firebase); 22 | 23 | async function main() { 24 | try { 25 | auth.setPersistence("NONE"); 26 | 27 | const user = await signInWithEmailAndPassword( 28 | auth, 29 | "foo@google.com", 30 | "123456" 31 | ); 32 | 33 | const token = await user.user.getIdToken(true); 34 | console.log(token); 35 | } finally { 36 | await signOut(auth); 37 | } 38 | } 39 | main(); 40 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin_playground_client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "firebase": "^11.3.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/credential_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:dart_firebase_admin/src/app.dart'; 6 | import 'package:file/memory.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | const _fakeRSAKey = 10 | '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCUD3KKtJk6JEDA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n4h3z8UdjAgMBAAECggEAR5HmBO2CygufLxLzbZ/jwN7Yitf0v/nT8LRjDs1WFux9\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nPPZaRPjBWvdqg4QttSSBKGm5FnhFPrpEFvOjznNBoQKBgQDJpRvDTIkNnpYhi/ni\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ndLSYULRW1DBgakQd09NRvPBoQwKBgQC7+KGhoXw5Kvr7qnQu+x0Gb+8u8CHT0qCG\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nvpTRZN3CYQKBgFBc/DaWnxyNcpoGFl4lkBy/G9Q2hPf5KRsqS0CDL7BXCpL0lCyz\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nOcltaAFaTptzmARfj0Q2d7eEzemABr9JHdyCdY0RXgJe96zHijXOTiXPAoGAfe+C\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\npEmuauUytUaZ16G8/T8qh/ndPcqslwHQqsmtWYECgYEAwpvpZvvh7LXH5/OeLRjs\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nKhg2WH+bggdnYug+oRFauQs=\n-----END PRIVATE KEY-----'; 11 | 12 | void main() { 13 | group(Credential, () { 14 | test('fromServiceAccountParams', () { 15 | expect( 16 | () => Credential.fromServiceAccountParams( 17 | clientId: 'id', 18 | privateKey: _fakeRSAKey, 19 | email: 'email', 20 | ), 21 | returnsNormally, 22 | ); 23 | }); 24 | 25 | group('fromServiceAccount', () { 26 | test('throws if file is missing', () { 27 | final fs = MemoryFileSystem.test(); 28 | 29 | expect( 30 | () => Credential.fromServiceAccount(fs.file('service-account.json')), 31 | throwsA(isA()), 32 | ); 33 | }); 34 | 35 | test('throws if file cannot be parsed', () { 36 | final fs = MemoryFileSystem.test(); 37 | fs.file('service-account.json').writeAsStringSync('invalid'); 38 | 39 | expect( 40 | () => Credential.fromServiceAccount(fs.file('service-account.json')), 41 | throwsFormatException, 42 | ); 43 | }); 44 | 45 | test('throws if file is not correctly formatted', () { 46 | final fs = MemoryFileSystem.test(); 47 | fs.file('service-account.json').writeAsStringSync('{}'); 48 | 49 | expect( 50 | () => Credential.fromServiceAccount(fs.file('service-account.json')), 51 | throwsArgumentError, 52 | ); 53 | }); 54 | 55 | test('completes if file exists and is correctly formatted', () { 56 | final fs = MemoryFileSystem.test(); 57 | fs.file('service-account.json').writeAsStringSync(''' 58 | { 59 | "type": "service_account", 60 | "client_id": "id", 61 | "private_key": ${jsonEncode(_fakeRSAKey)}, 62 | "client_email": "email" 63 | } 64 | '''); 65 | 66 | // Should not throw. 67 | Credential.fromServiceAccount(fs.file('service-account.json')); 68 | }); 69 | }); 70 | 71 | group('fromApplicationDefaultCredentials', () { 72 | test( 73 | 'completes if `GOOGLE_APPLICATION_CREDENTIALS` environment-variable is valid service account JSON', 74 | () { 75 | final dir = Directory.current.createTempSync(); 76 | addTearDown(() => dir.deleteSync(recursive: true)); 77 | final file = File('${dir.path}/service-account.json'); 78 | file.writeAsStringSync(''' 79 | { 80 | "type": "service_account", 81 | "client_id": "id", 82 | "private_key": ${jsonEncode(_fakeRSAKey)}, 83 | "client_email": "foo@bar.com" 84 | } 85 | '''); 86 | 87 | final fakeServiceAccount = { 88 | 'GOOGLE_APPLICATION_CREDENTIALS': file.path, 89 | }; 90 | final credential = runZoned( 91 | Credential.fromApplicationDefaultCredentials, 92 | zoneValues: {envSymbol: fakeServiceAccount}, 93 | ); 94 | expect(credential.serviceAccountCredentials, isNotNull); 95 | 96 | // Verify if service account is actually being used 97 | expect( 98 | credential.serviceAccountCredentials!.email, 99 | 'foo@bar.com', 100 | ); 101 | }); 102 | 103 | test( 104 | 'does nothing if `GOOGLE_APPLICATION_CREDENTIALS` environment-variable is not valid service account JSON', 105 | () { 106 | final credential = runZoned( 107 | Credential.fromApplicationDefaultCredentials, 108 | zoneValues: { 109 | envSymbol: {'GOOGLE_APPLICATION_CREDENTIALS': ''}, 110 | }, 111 | ); 112 | expect(credential.serviceAccountCredentials, isNull); 113 | }); 114 | }); 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/firebase_admin_app_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dart_firebase_admin/src/app.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group(FirebaseAdminApp, () { 8 | test('initializeApp() creates a new FirebaseAdminApp', () { 9 | final app = FirebaseAdminApp.initializeApp( 10 | 'dart-firebase-admin', 11 | Credential.fromApplicationDefaultCredentials(), 12 | ); 13 | 14 | expect(app, isA()); 15 | expect(app.authApiHost, Uri.https('identitytoolkit.googleapis.com', '/')); 16 | expect( 17 | app.firestoreApiHost, 18 | Uri.https('firestore.googleapis.com', '/'), 19 | ); 20 | }); 21 | 22 | test('useEmulator() sets the apiHost to the emulator', () { 23 | final app = FirebaseAdminApp.initializeApp( 24 | 'dart-firebase-admin', 25 | Credential.fromApplicationDefaultCredentials(), 26 | ); 27 | 28 | app.useEmulator(); 29 | 30 | expect( 31 | app.authApiHost, 32 | Uri.http('127.0.0.1:9099', 'identitytoolkit.googleapis.com/'), 33 | ); 34 | expect( 35 | app.firestoreApiHost, 36 | Uri.http('127.0.0.1:8080', '/'), 37 | ); 38 | }); 39 | 40 | test( 41 | 'useEmulator() uses environment variables to set apiHost to the emulator', 42 | () async { 43 | const firebaseAuthEmulatorHost = '127.0.0.1:9000'; 44 | const firestoreEmulatorHost = '127.0.0.1:8000'; 45 | final testEnv = { 46 | 'FIREBASE_AUTH_EMULATOR_HOST': firebaseAuthEmulatorHost, 47 | 'FIRESTORE_EMULATOR_HOST': firestoreEmulatorHost, 48 | }; 49 | 50 | await runZoned( 51 | zoneValues: {envSymbol: testEnv}, 52 | () async { 53 | final app = FirebaseAdminApp.initializeApp( 54 | 'dart-firebase-admin', 55 | Credential.fromApplicationDefaultCredentials(), 56 | ); 57 | 58 | app.useEmulator(); 59 | 60 | expect( 61 | app.authApiHost, 62 | Uri.http( 63 | firebaseAuthEmulatorHost, 64 | 'identitytoolkit.googleapis.com/', 65 | ), 66 | ); 67 | expect( 68 | app.firestoreApiHost, 69 | Uri.http(firestoreEmulatorHost, '/'), 70 | ); 71 | }, 72 | ); 73 | }); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/google_cloud_firestore/collection_group_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_firebase_admin/firestore.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'util/helpers.dart'; 5 | 6 | void main() { 7 | group('collectionGroup', () { 8 | late Firestore firestore; 9 | 10 | setUp(() async => firestore = await createFirestore()); 11 | 12 | test('throws if collectionId contains "/"', () { 13 | expect( 14 | () => firestore.collectionGroup('my-group/docA'), 15 | throwsA( 16 | isA().having( 17 | (e) => e.message, 18 | 'message', 19 | 'Invalid collectionId "my-group/docA". ' 20 | 'Collection IDs must not contain "/".', 21 | ), 22 | ), 23 | ); 24 | }); 25 | 26 | test('supports withConverter', () async { 27 | await Future.wait([ 28 | firestore.doc('with-converter-group/docA').set({'value': 42}), 29 | firestore.doc('abc/def/with-converter-group/docB').set({'value': 13}), 30 | firestore.doc('abc/def/with-converter-group/docC').set({'value': 10}), 31 | ]); 32 | 33 | final group = 34 | firestore.collectionGroup('with-converter-group').withConverter( 35 | fromFirestore: (firestore) => firestore.data()['value']! as num, 36 | toFirestore: (value) => {'value': value}, 37 | ); 38 | 39 | final query = group.where('value', WhereFilter.greaterThan, 12); 40 | final snapshot = await query.get(); 41 | 42 | expect(snapshot.docs, hasLength(2)); 43 | expect(snapshot.docs[0].data(), 13); 44 | expect(snapshot.docs[1].data(), 42); 45 | }); 46 | 47 | test('defaults to JSON decoding', () async { 48 | await Future.wait([ 49 | firestore.doc('group/docA').set({'value': 42}), 50 | firestore.doc('abc/def/group/docB').set({'value': 13}), 51 | firestore.doc('abc/def/group/docC').set({'value': 10}), 52 | ]); 53 | 54 | final group = firestore.collectionGroup('group'); 55 | 56 | final query = group.where('value', WhereFilter.greaterThan, 12); 57 | final snapshot = await query.get(); 58 | 59 | expect(snapshot.docs, hasLength(2)); 60 | expect(snapshot.docs[0].data(), {'value': 13}); 61 | expect(snapshot.docs[1].data(), {'value': 42}); 62 | }); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/google_cloud_firestore/collection_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_firebase_admin/firestore.dart'; 2 | import 'package:test/test.dart' hide throwsArgumentError; 3 | 4 | import 'util/helpers.dart'; 5 | 6 | void main() { 7 | group('Collection interface', () { 8 | late Firestore firestore; 9 | 10 | setUp(() async => firestore = await createFirestore()); 11 | 12 | test('supports + in collection name', () async { 13 | final a = firestore 14 | .collection('/collection+a/lF1kvtRAYMqmdInT7iJK/subcollection'); 15 | 16 | expect(a.path, 'collection+a/lF1kvtRAYMqmdInT7iJK/subcollection'); 17 | 18 | await a.add({'foo': 'bar'}); 19 | 20 | final results = await a.get(); 21 | 22 | expect(results.docs.length, 1); 23 | expect(results.docs.first.data(), {'foo': 'bar'}); 24 | }); 25 | 26 | test('has doc() method', () { 27 | final collection = firestore.collection('colId'); 28 | 29 | expect(collection.id, 'colId'); 30 | expect(collection.path, 'colId'); 31 | 32 | final documentRef = collection.doc('docId'); 33 | 34 | expect(documentRef, isA>()); 35 | expect(documentRef.id, 'docId'); 36 | expect(documentRef.path, 'colId/docId'); 37 | 38 | expect( 39 | () => collection.doc(''), 40 | throwsArgumentError(message: 'Must be a non-empty string'), 41 | ); 42 | expect( 43 | () => collection.doc('doc/coll'), 44 | throwsArgumentError( 45 | message: 46 | 'Value for argument "documentPath" must point to a document, ' 47 | 'but was "doc/coll". ' 48 | 'Your path does not contain an even number of components.', 49 | ), 50 | ); 51 | 52 | expect( 53 | collection.doc('docId/colId/docId'), 54 | isA>(), 55 | ); 56 | }); 57 | 58 | test('has parent getter', () { 59 | final collection = firestore.collection('col1/doc/col2'); 60 | expect(collection.path, 'col1/doc/col2'); 61 | 62 | final document = collection.parent; 63 | expect(document!.path, 'col1/doc'); 64 | }); 65 | 66 | test('parent returns null for root', () { 67 | final collection = firestore.collection('col1'); 68 | 69 | expect(collection.parent, isNull); 70 | }); 71 | 72 | test('supports auto-generated ids', () { 73 | final collection = firestore.collection('col1'); 74 | 75 | final document = collection.doc(); 76 | expect(document.id, hasLength(20)); 77 | }); 78 | 79 | test('has add() method', () async { 80 | final collection = firestore.collection('addCollection'); 81 | 82 | final documentRef = await collection.add({'foo': 'bar'}); 83 | 84 | expect(documentRef, isA>()); 85 | expect(documentRef.id, hasLength(20)); 86 | expect(documentRef.path, 'addCollection/${documentRef.id}'); 87 | 88 | final documentSnapshot = await documentRef.get(); 89 | 90 | expect(documentSnapshot.exists, isTrue); 91 | expect(documentSnapshot.data(), {'foo': 'bar'}); 92 | }); 93 | 94 | test('has list() method', () async { 95 | final collection = firestore.collection('listCollection'); 96 | 97 | final a = collection.doc('a'); 98 | await a.set({'foo': 'bar'}); 99 | 100 | final b = collection.doc('b'); 101 | await b.set({'baz': 'quaz'}); 102 | 103 | final documents = await collection.listDocuments(); 104 | 105 | expect(documents, unorderedEquals([a, b])); 106 | }); 107 | 108 | test('override equal', () async { 109 | final coll1 = firestore.collection('coll1'); 110 | final coll1Equals = firestore.collection('coll1'); 111 | final coll2 = firestore.collection('coll2'); 112 | 113 | expect(coll1, coll1Equals); 114 | expect(coll1, isNot(coll2)); 115 | }); 116 | 117 | test('override hashCode', () async { 118 | final coll1 = firestore.collection('coll1'); 119 | final coll1Equals = firestore.collection('coll1'); 120 | final coll2 = firestore.collection('coll2'); 121 | 122 | expect(coll1.hashCode, coll1Equals.hashCode); 123 | expect(coll1.hashCode, isNot(coll2.hashCode)); 124 | }); 125 | 126 | test('for CollectionReference.withConverter().doc()', () async { 127 | final collection = firestore.collection('withConverterColDoc'); 128 | 129 | final rawDoc = collection.doc('doc'); 130 | 131 | final docRef = collection 132 | .withConverter( 133 | fromFirestore: (snapshot) => snapshot.data()['value']! as int, 134 | toFirestore: (value) => {'value': value}, 135 | ) 136 | .doc('doc'); 137 | 138 | expect(docRef, isA>()); 139 | expect(docRef.id, 'doc'); 140 | expect(docRef.path, 'withConverterColDoc/doc'); 141 | 142 | await docRef.set(42); 143 | 144 | final rawDocSnapshot = await rawDoc.get(); 145 | expect(rawDocSnapshot.data(), {'value': 42}); 146 | 147 | final docSnapshot = await docRef.get(); 148 | expect(docSnapshot.data(), 42); 149 | }); 150 | 151 | test('for CollectionReference.withConverter().add()', () async { 152 | final collection = 153 | firestore.collection('withConverterColAdd').withConverter( 154 | fromFirestore: (snapshot) => snapshot.data()['value']! as int, 155 | toFirestore: (value) => {'value': value}, 156 | ); 157 | 158 | expect(collection, isA>()); 159 | 160 | final docRef = await collection.add(42); 161 | 162 | expect(docRef, isA>()); 163 | expect(docRef.id, hasLength(20)); 164 | expect(docRef.path, 'withConverterColAdd/${docRef.id}'); 165 | 166 | final docSnapshot = await docRef.get(); 167 | expect(docSnapshot.data(), 42); 168 | }); 169 | 170 | test('drops the converter when calling CollectionReference.parent()', 171 | () { 172 | final collection = firestore 173 | .collection('withConverterColParent/doc/child') 174 | .withConverter( 175 | fromFirestore: (snapshot) => snapshot.data()['value']! as int, 176 | toFirestore: (value) => {'value': value}, 177 | ); 178 | 179 | expect(collection, isA>()); 180 | 181 | final DocumentReference? parent = collection.parent; 182 | 183 | expect(parent!.path, 'withConverterColParent/doc'); 184 | }); 185 | }); 186 | } 187 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/google_cloud_firestore/firestore_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_firebase_admin/firestore.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'util/helpers.dart'; 5 | 6 | void main() { 7 | group('Firestore', () { 8 | late Firestore firestore; 9 | 10 | setUp(() async => firestore = await createFirestore()); 11 | 12 | test('listCollections', () async { 13 | final a = firestore.collection('a'); 14 | final b = firestore.collection('b'); 15 | 16 | await a.doc('1').set({'a': 1}); 17 | await b.doc('2').set({'b': 2}); 18 | 19 | final collections = await firestore.listCollections(); 20 | 21 | expect(collections, containsAll([a, b])); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/google_cloud_firestore/timestamp_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_firebase_admin/firestore.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('Timestamp', () { 6 | test('constructor', () { 7 | final now = DateTime.now().toUtc(); 8 | final seconds = now.millisecondsSinceEpoch ~/ 1000; 9 | final nanoseconds = 10 | (now.microsecondsSinceEpoch - seconds * 1000 * 1000) * 1000; 11 | 12 | expect( 13 | Timestamp(seconds: seconds, nanoseconds: nanoseconds), 14 | Timestamp.fromDate(now), 15 | ); 16 | }); 17 | 18 | test('fromDate constructor', () { 19 | final now = DateTime.now().toUtc(); 20 | final timestamp = Timestamp.fromDate(now); 21 | 22 | expect(timestamp.seconds, now.millisecondsSinceEpoch ~/ 1000); 23 | }); 24 | 25 | test('fromMillis constructor', () { 26 | final now = DateTime.now().toUtc(); 27 | final timestamp = Timestamp.fromMillis(now.millisecondsSinceEpoch); 28 | 29 | expect(timestamp.seconds, now.millisecondsSinceEpoch ~/ 1000); 30 | expect( 31 | timestamp.nanoseconds, 32 | (now.millisecondsSinceEpoch % 1000) * (1000 * 1000), 33 | ); 34 | }); 35 | 36 | test('fromMicros constructor', () { 37 | final now = DateTime.now().toUtc(); 38 | final timestamp = Timestamp.fromMicros(now.microsecondsSinceEpoch); 39 | 40 | expect(timestamp.seconds, now.microsecondsSinceEpoch ~/ (1000 * 1000)); 41 | expect( 42 | timestamp.nanoseconds, 43 | (now.microsecondsSinceEpoch % (1000 * 1000)) * 1000, 44 | ); 45 | }); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/google_cloud_firestore/util/helpers.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dart_firebase_admin/firestore.dart'; 4 | import 'package:dart_firebase_admin/src/app.dart'; 5 | import 'package:http/http.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | const projectId = 'dart-firebase-admin'; 9 | 10 | FirebaseAdminApp createApp({ 11 | FutureOr Function()? tearDown, 12 | Client? client, 13 | bool useEmulator = true, 14 | }) { 15 | final credential = Credential.fromApplicationDefaultCredentials(); 16 | final app = FirebaseAdminApp.initializeApp( 17 | projectId, 18 | credential, 19 | client: client, 20 | ); 21 | if (useEmulator) app.useEmulator(); 22 | 23 | addTearDown(() async { 24 | if (tearDown != null) { 25 | await tearDown(); 26 | } 27 | await app.close(); 28 | }); 29 | 30 | return app; 31 | } 32 | 33 | Future _recursivelyDeleteAllDocuments(Firestore firestore) async { 34 | Future handleCollection(CollectionReference collection) async { 35 | final docs = await collection.listDocuments(); 36 | 37 | for (final doc in docs) { 38 | await doc.delete(); 39 | 40 | final subcollections = await doc.listCollections(); 41 | for (final subcollection in subcollections) { 42 | await handleCollection(subcollection); 43 | } 44 | } 45 | } 46 | 47 | final collections = await firestore.listCollections(); 48 | for (final collection in collections) { 49 | await handleCollection(collection); 50 | } 51 | } 52 | 53 | Future createFirestore({ 54 | Settings? settings, 55 | bool useEmulator = true, 56 | }) async { 57 | final firestore = Firestore( 58 | createApp(useEmulator: useEmulator), 59 | settings: settings, 60 | ); 61 | 62 | addTearDown(() => _recursivelyDeleteAllDocuments(firestore)); 63 | 64 | return firestore; 65 | } 66 | 67 | Matcher isArgumentError({String? message}) { 68 | var matcher = isA(); 69 | if (message != null) { 70 | matcher = matcher.having((e) => e.message, 'message', message); 71 | } 72 | 73 | return matcher; 74 | } 75 | 76 | Matcher throwsArgumentError({String? message}) { 77 | return throwsA(isArgumentError(message: message)); 78 | } 79 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_firebase_admin/dart_firebase_admin.dart'; 2 | import 'package:googleapis/fcm/v1.dart'; 3 | import 'package:http/http.dart'; 4 | import 'package:mocktail/mocktail.dart'; 5 | 6 | void registerFallbacks() { 7 | registerFallbackValue(_SendMessageRequestFake()); 8 | registerFallbackValue(Uri()); 9 | registerFallbackValue(Request('post', Uri())); 10 | } 11 | 12 | class FirebaseAdminMock extends Mock implements FirebaseAdminApp {} 13 | 14 | class ClientMock extends Mock implements Client {} 15 | 16 | class _SendMessageRequestFake extends Fake implements SendMessageRequest {} 17 | -------------------------------------------------------------------------------- /packages/dart_firebase_admin/test/security_rules/security_rules_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_firebase_admin/security_rules.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import '../google_cloud_firestore/util/helpers.dart'; 5 | import '../mock.dart'; 6 | 7 | void main() { 8 | late SecurityRules securityRules; 9 | 10 | setUpAll(registerFallbacks); 11 | 12 | setUp(() async { 13 | final sdk = createApp(useEmulator: false); 14 | securityRules = SecurityRules(sdk); 15 | }); 16 | 17 | const simpleFirestoreContent = 18 | 'service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if false; } } }'; 19 | 20 | group('SecurityRules', () { 21 | test('ruleset e2e', () async { 22 | final ruleset = await securityRules.createRuleset( 23 | RulesFile( 24 | name: 'firestore.rules', 25 | content: simpleFirestoreContent, 26 | ), 27 | ); 28 | 29 | final ruleset2 = await securityRules.getRuleset(ruleset.name); 30 | expect(ruleset2.name, ruleset.name); 31 | expect(ruleset2.createTime, isNotEmpty); 32 | expect(ruleset2.source.single.name, 'firestore.rules'); 33 | expect(ruleset2.source.single.content, simpleFirestoreContent); 34 | 35 | await securityRules.deleteRuleset(ruleset.name); 36 | 37 | expect( 38 | securityRules.getRuleset(ruleset.name), 39 | throwsA( 40 | isA() 41 | .having((e) => e.code, 'code', 'security-rules/not-found'), 42 | ), 43 | ); 44 | }); 45 | 46 | test('listRulesetMetadata', () async { 47 | final ruleset = await securityRules.createRuleset( 48 | RulesFile( 49 | name: 'firestore.rules', 50 | content: simpleFirestoreContent, 51 | ), 52 | ); 53 | final ruleset2 = await securityRules.createRuleset( 54 | RulesFile( 55 | name: 'firestore.rules', 56 | content: '/* hello */ $simpleFirestoreContent', 57 | ), 58 | ); 59 | 60 | final metadata = await securityRules.listRulesetMetadata(pageSize: 1); 61 | 62 | expect(metadata.rulesets.length, 1); 63 | expect(metadata.nextPageToken, isNotNull); 64 | expect(metadata.rulesets.single.name, ruleset2.name); 65 | 66 | final metadata2 = await securityRules.listRulesetMetadata( 67 | pageSize: 1, 68 | nextPageToken: metadata.nextPageToken, 69 | ); 70 | 71 | expect(metadata2.rulesets.length, 1); 72 | expect(metadata2.rulesets.single.name, isNot(ruleset2.name)); 73 | expect(metadata2.rulesets.single.name, ruleset.name); 74 | }); 75 | 76 | test('firestore release flow', () async { 77 | final ruleset = await securityRules.createRuleset( 78 | RulesFile( 79 | name: 'firestore.rules', 80 | content: simpleFirestoreContent, 81 | ), 82 | ); 83 | 84 | final before = await securityRules.getFirestoreRuleset(); 85 | 86 | expect(before.name, isNot(ruleset.name)); 87 | 88 | await securityRules.releaseFirestoreRuleset(ruleset.name); 89 | 90 | final after = await securityRules.getFirestoreRuleset(); 91 | expect(after.name, ruleset.name); 92 | }); 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_firebase_admin_workspace 2 | publish_to: none 3 | 4 | environment: 5 | sdk: '>=3.0.0 <4.0.0' 6 | dev_dependencies: 7 | melos: ^6.1.0 8 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Fast fail the script on failures. 4 | set -e 5 | 6 | dart pub global activate coverage 7 | 8 | firebase emulators:exec --project dart-firebase-admin --only firestore,auth "dart test --concurrency=1 --coverage=coverage" 9 | 10 | format_coverage --lcov --in=coverage --out=coverage.lcov --packages=.dart_tool/package_config.json --report-on=lib --------------------------------------------------------------------------------