├── .github ├── issue_template.md ├── pull_request_template.md ├── resources │ └── integ-service-account.json.gpg ├── scripts │ ├── generate_changelog.sh │ ├── publish_preflight_check.sh │ └── run_all_tests.sh └── workflows │ ├── ci.yml │ ├── nightly.yml │ └── release.yml ├── .gitignore ├── .opensource └── project.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── appcheck ├── appcheck.go └── appcheck_test.go ├── auth ├── auth.go ├── auth_appengine.go ├── auth_std.go ├── auth_test.go ├── email_action_links.go ├── email_action_links_test.go ├── export_users.go ├── hash │ ├── hash.go │ └── hash_test.go ├── import_users.go ├── multi_factor_config_mgt.go ├── multi_factor_config_mgt_test.go ├── project_config_mgt.go ├── project_config_mgt_test.go ├── provider_config.go ├── provider_config_test.go ├── tenant_mgt.go ├── tenant_mgt_test.go ├── token_generator.go ├── token_generator_test.go ├── token_verifier.go ├── token_verifier_test.go ├── user_mgt.go └── user_mgt_test.go ├── db ├── auth_override_test.go ├── db.go ├── db_test.go ├── query.go ├── query_test.go ├── ref.go └── ref_test.go ├── errorutils └── errorutils.go ├── firebase.go ├── firebase_test.go ├── go.mod ├── go.sum ├── iid ├── iid.go └── iid_test.go ├── integration ├── auth │ ├── auth_test.go │ ├── project_config_mgt_test.go │ ├── provider_config_test.go │ ├── tenant_mgt_test.go │ └── user_mgt_test.go ├── db │ ├── db_test.go │ └── query_test.go ├── firestore │ └── firestore_test.go ├── iid │ └── iid_test.go ├── internal │ └── internal.go ├── messaging │ └── messaging_test.go └── storage │ └── storage_test.go ├── internal ├── errors.go ├── errors_test.go ├── http_client.go ├── http_client_test.go ├── internal.go └── json_http_client_test.go ├── messaging ├── messaging.go ├── messaging_batch.go ├── messaging_batch_test.go ├── messaging_test.go ├── messaging_utils.go ├── topic_mgt.go └── topic_mgt_test.go ├── remoteconfig ├── condition_evaluator.go ├── condition_evaluator_test.go ├── remoteconfig.go ├── remoteconfig_test.go ├── server_config.go ├── server_config_test.go ├── server_template.go ├── server_template_test.go └── server_template_types.go ├── snippets ├── auth.go ├── db.go ├── init.go ├── messaging.go └── storage.go ├── storage ├── storage.go └── storage_test.go └── testdata ├── appcheck_pk.pem ├── dinosaurs.json ├── dinosaurs_index.json ├── firebase_config.json ├── firebase_config_empty.json ├── firebase_config_invalid.json ├── firebase_config_invalid_key.json ├── firebase_config_partial.json ├── get_disabled_user.json ├── get_user.json ├── invalid_service_account.json ├── list_users.json ├── mock.jwks.json ├── plain_text.txt ├── public_certs.json ├── refresh_token.json └── service_account.json /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### [READ] Step 1: Are you in the right place? 2 | 3 | * For issues or feature requests related to __the code in this repository__ 4 | file a GitHub issue. 5 | * If this is a __feature request__ make sure the issue title starts with "FR:". 6 | * For general technical questions, post a question on [StackOverflow](http://stackoverflow.com/) 7 | with the firebase tag. 8 | * For general Firebase discussion, use the [firebase-talk](https://groups.google.com/forum/#!forum/firebase-talk) 9 | google group. 10 | * For help troubleshooting your application that does not fall under one 11 | of the above categories, reach out to the personalized 12 | [Firebase support channel](https://firebase.google.com/support/). 13 | 14 | ### [REQUIRED] Step 2: Describe your environment 15 | 16 | * Operating System version: _____ 17 | * Firebase SDK version: _____ 18 | * Library version: _____ 19 | * Firebase Product: _____ (auth, database, storage, etc) 20 | 21 | ### [REQUIRED] Step 3: Describe the problem 22 | 23 | #### Steps to reproduce: 24 | 25 | What happened? How can we make the problem occur? 26 | This could be a description, log/console output, etc. 27 | 28 | #### Relevant Code: 29 | 30 | ``` 31 | // TODO(you): code here to reproduce the problem 32 | ``` 33 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Hey there! So you want to contribute to a Firebase SDK? 2 | Before you file this pull request, please read these guidelines: 3 | 4 | ### Discussion 5 | 6 | * Read the contribution guidelines (CONTRIBUTING.md). 7 | * If this has been discussed in an issue, make sure to link to the issue here. 8 | If not, go file an issue about this **before creating a pull request** to discuss. 9 | 10 | ### Testing 11 | 12 | * Make sure all existing tests in the repository pass after your change. 13 | * If you fixed a bug or added a feature, add a new test to cover your code. 14 | 15 | ### API Changes 16 | 17 | * At this time we cannot accept changes that affect the public API. If you'd like to help 18 | us make Firebase APIs better, please propose your change in an issue so that we 19 | can discuss it together. 20 | -------------------------------------------------------------------------------- /.github/resources/integ-service-account.json.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/firebase-admin-go/d515faf47673ae79005d4b0abceca74716a5ac92/.github/resources/integ-service-account.json.gpg -------------------------------------------------------------------------------- /.github/scripts/generate_changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | set -u 19 | 20 | function printChangelog() { 21 | local TITLE=$1 22 | shift 23 | # Skip the sentinel value. 24 | local ENTRIES=("${@:2}") 25 | if [ ${#ENTRIES[@]} -ne 0 ]; then 26 | echo "### ${TITLE}" 27 | echo "" 28 | for ((i = 0; i < ${#ENTRIES[@]}; i++)) 29 | do 30 | echo "* ${ENTRIES[$i]}" 31 | done 32 | echo "" 33 | fi 34 | } 35 | 36 | if [[ -z "${GITHUB_SHA}" ]]; then 37 | GITHUB_SHA="HEAD" 38 | fi 39 | 40 | LAST_TAG=`git describe --tags $(git rev-list --tags --max-count=1) 2> /dev/null` || true 41 | if [[ -z "${LAST_TAG}" ]]; then 42 | echo "[INFO] No tags found. Including all commits up to ${GITHUB_SHA}." 43 | VERSION_RANGE="${GITHUB_SHA}" 44 | else 45 | echo "[INFO] Last release tag: ${LAST_TAG}." 46 | COMMIT_SHA=`git show-ref -s ${LAST_TAG}` 47 | echo "[INFO] Last release commit: ${COMMIT_SHA}." 48 | VERSION_RANGE="${COMMIT_SHA}..${GITHUB_SHA}" 49 | echo "[INFO] Including all commits in the range ${VERSION_RANGE}." 50 | fi 51 | 52 | echo "" 53 | 54 | # Older versions of Bash (< 4.4) treat empty arrays as unbound variables, which triggers 55 | # errors when referencing them. Therefore we initialize each of these arrays with an empty 56 | # sentinel value, and later skip them. 57 | CHANGES=("") 58 | FIXES=("") 59 | FEATS=("") 60 | MISC=("") 61 | 62 | while read -r line 63 | do 64 | COMMIT_MSG=`echo ${line} | cut -d ' ' -f 2-` 65 | if [[ $COMMIT_MSG =~ ^change(\(.*\))?: ]]; then 66 | CHANGES+=("$COMMIT_MSG") 67 | elif [[ $COMMIT_MSG =~ ^fix(\(.*\))?: ]]; then 68 | FIXES+=("$COMMIT_MSG") 69 | elif [[ $COMMIT_MSG =~ ^feat(\(.*\))?: ]]; then 70 | FEATS+=("$COMMIT_MSG") 71 | else 72 | MISC+=("${COMMIT_MSG}") 73 | fi 74 | done < <(git log ${VERSION_RANGE} --oneline) 75 | 76 | printChangelog "Breaking Changes" "${CHANGES[@]}" 77 | printChangelog "New Features" "${FEATS[@]}" 78 | printChangelog "Bug Fixes" "${FIXES[@]}" 79 | printChangelog "Miscellaneous" "${MISC[@]}" 80 | -------------------------------------------------------------------------------- /.github/scripts/publish_preflight_check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | set -e 19 | set -u 20 | 21 | function echo_info() { 22 | local MESSAGE=$1 23 | echo "[INFO] ${MESSAGE}" 24 | } 25 | 26 | function echo_warn() { 27 | local MESSAGE=$1 28 | echo "[WARN] ${MESSAGE}" 29 | } 30 | 31 | function terminate() { 32 | echo "" 33 | echo_warn "--------------------------------------------" 34 | echo_warn "PREFLIGHT FAILED" 35 | echo_warn "--------------------------------------------" 36 | exit 1 37 | } 38 | 39 | 40 | echo_info "Starting publish preflight check..." 41 | echo_info "Git revision : ${GITHUB_SHA}" 42 | echo_info "Git ref : ${GITHUB_REF}" 43 | echo_info "Workflow triggered by : ${GITHUB_ACTOR}" 44 | echo_info "GitHub event : ${GITHUB_EVENT_NAME}" 45 | 46 | 47 | echo_info "" 48 | echo_info "--------------------------------------------" 49 | echo_info "Extracting release version" 50 | echo_info "--------------------------------------------" 51 | echo_info "" 52 | 53 | echo_info "Loading version from: firebase.go" 54 | 55 | readonly RELEASE_VERSION=`grep "const Version" firebase.go | awk '{print $4}' | tr -d \"` || true 56 | if [[ -z "${RELEASE_VERSION}" ]]; then 57 | echo_warn "Failed to extract release version from: firebase.go" 58 | terminate 59 | fi 60 | 61 | if [[ ! "${RELEASE_VERSION}" =~ ^([0-9]*)\.([0-9]*)\.([0-9]*)$ ]]; then 62 | echo_warn "Malformed release version string: ${RELEASE_VERSION}. Exiting." 63 | terminate 64 | fi 65 | 66 | echo_info "Extracted release version: ${RELEASE_VERSION}" 67 | echo "version=v${RELEASE_VERSION}" >> $GITHUB_OUTPUT 68 | 69 | 70 | echo_info "" 71 | echo_info "--------------------------------------------" 72 | echo_info "Checking release tag" 73 | echo_info "--------------------------------------------" 74 | echo_info "" 75 | 76 | echo_info "---< git fetch --depth=1 origin +refs/tags/*:refs/tags/* >---" 77 | git fetch --depth=1 origin +refs/tags/*:refs/tags/* 78 | echo "" 79 | 80 | readonly EXISTING_TAG=`git rev-parse -q --verify "refs/tags/v${RELEASE_VERSION}"` || true 81 | if [[ -n "${EXISTING_TAG}" ]]; then 82 | echo_warn "Tag v${RELEASE_VERSION} already exists. Exiting." 83 | echo_warn "If the tag was created in a previous unsuccessful attempt, delete it and try again." 84 | echo_warn " $ git tag -d v${RELEASE_VERSION}" 85 | echo_warn " $ git push --delete origin v${RELEASE_VERSION}" 86 | 87 | readonly RELEASE_URL="https://github.com/firebase/firebase-admin-go/releases/tag/v${RELEASE_VERSION}" 88 | echo_warn "Delete any corresponding releases at ${RELEASE_URL}." 89 | terminate 90 | fi 91 | 92 | echo_info "Tag v${RELEASE_VERSION} does not exist." 93 | 94 | 95 | echo_info "" 96 | echo_info "--------------------------------------------" 97 | echo_info "Generating changelog" 98 | echo_info "--------------------------------------------" 99 | echo_info "" 100 | 101 | echo_info "---< git fetch origin dev --prune --unshallow >---" 102 | git fetch origin dev --prune --unshallow 103 | echo "" 104 | 105 | echo_info "Generating changelog from history..." 106 | readonly CURRENT_DIR=$(dirname "$0") 107 | readonly CHANGELOG=`${CURRENT_DIR}/generate_changelog.sh` 108 | echo "$CHANGELOG" 109 | 110 | # Parse and preformat the text to handle multi-line output. 111 | # See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#example-of-a-multiline-string 112 | # and https://github.com/github/docs/issues/21529#issue-1418590935 113 | FILTERED_CHANGELOG=`echo "$CHANGELOG" | grep -v "\\[INFO\\]"` 114 | FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\''/'"'}" 115 | echo "changelog<> $GITHUB_OUTPUT 116 | echo -e "$FILTERED_CHANGELOG" >> $GITHUB_OUTPUT 117 | echo "CHANGELOGEOF" >> $GITHUB_OUTPUT 118 | 119 | echo "" 120 | echo_info "--------------------------------------------" 121 | echo_info "PREFLIGHT SUCCESSFUL" 122 | echo_info "--------------------------------------------" 123 | -------------------------------------------------------------------------------- /.github/scripts/run_all_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | set -u 19 | 20 | gpg --quiet --batch --yes --decrypt --passphrase="${FIREBASE_SERVICE_ACCT_KEY}" \ 21 | --output testdata/integration_cert.json .github/resources/integ-service-account.json.gpg 22 | 23 | echo "${FIREBASE_API_KEY}" > testdata/integration_apikey.txt 24 | 25 | go test -v -race ./... 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: pull_request 3 | jobs: 4 | 5 | module: 6 | name: Module build 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | go: ['1.23', '1.24'] 12 | 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Go ${{ matrix.go }} 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: ${{ matrix.go }} 21 | 22 | - name: Install golint 23 | run: go install golang.org/x/lint/golint@latest 24 | 25 | - name: Run Linter 26 | run: | 27 | golint -set_exit_status ./... 28 | 29 | - name: Run Unit Tests 30 | if: success() || failure() 31 | run: go test -v -race -test.short ./... 32 | 33 | - name: Run Formatter 34 | run: | 35 | if [[ ! -z "$(gofmt -l -s .)" ]]; then 36 | echo "Go code is not formatted:" 37 | gofmt -d -s . 38 | exit 1 39 | fi 40 | 41 | - name: Run Static Analyzer 42 | run: go vet -v ./... 43 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Nightly Builds 16 | 17 | on: 18 | # Runs every day at 06:30 AM (PT) and 08:30 PM (PT) / 04:30 AM (UTC) and 02:30 PM (UTC) 19 | # or on 'firebase_nightly_build' repository dispatch event. 20 | schedule: 21 | - cron: "30 4,14 * * *" 22 | repository_dispatch: 23 | types: [firebase_nightly_build] 24 | 25 | jobs: 26 | nightly: 27 | 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - name: Check out code 32 | uses: actions/checkout@v4 33 | with: 34 | ref: ${{ github.event.client_payload.ref || github.ref }} 35 | 36 | - name: Set up Go 37 | uses: actions/setup-go@v5 38 | with: 39 | go-version: '1.23' 40 | 41 | - name: Install golint 42 | run: go install golang.org/x/lint/golint@latest 43 | 44 | - name: Run Linter 45 | run: | 46 | golint -set_exit_status ./... 47 | 48 | - name: Run Tests 49 | run: ./.github/scripts/run_all_tests.sh 50 | env: 51 | FIREBASE_SERVICE_ACCT_KEY: ${{ secrets.FIREBASE_SERVICE_ACCT_KEY }} 52 | FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} 53 | 54 | - name: Send email on failure 55 | if: failure() 56 | uses: firebase/firebase-admin-node/.github/actions/send-email@master 57 | with: 58 | api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }} 59 | domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }} 60 | from: 'GitHub ' 61 | to: ${{ secrets.FIREBASE_ADMIN_GITHUB_EMAIL }} 62 | subject: 'Nightly build ${{github.run_id}} of ${{github.repository}} failed!' 63 | html: > 64 | Nightly workflow ${{github.run_id}} failed on: ${{github.repository}} 65 |

Navigate to the 66 | failed workflow. 67 | continue-on-error: true 68 | 69 | - name: Send email on cancelled 70 | if: cancelled() 71 | uses: firebase/firebase-admin-node/.github/actions/send-email@master 72 | with: 73 | api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }} 74 | domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }} 75 | from: 'GitHub ' 76 | to: ${{ secrets.FIREBASE_ADMIN_GITHUB_EMAIL }} 77 | subject: 'Nightly build ${{github.run_id}} of ${{github.repository}} cancelled!' 78 | html: > 79 | Nightly workflow ${{github.run_id}} cancelled on: ${{github.repository}} 80 |

Navigate to the 81 | cancelled workflow. 82 | continue-on-error: true 83 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Release Candidate 16 | 17 | on: 18 | # Only run the workflow when a PR is updated or when a developer explicitly requests 19 | # a build by sending a 'firebase_build' event. 20 | pull_request: 21 | types: [opened, synchronize, closed] 22 | 23 | repository_dispatch: 24 | types: 25 | - firebase_build 26 | 27 | jobs: 28 | stage_release: 29 | # To publish a release, merge the release PR with the label 'release:publish'. 30 | # To stage a release without publishing it, send a 'firebase_build' event or apply 31 | # the 'release:stage' label to a PR. 32 | if: github.event.action == 'firebase_build' || 33 | contains(github.event.pull_request.labels.*.name, 'release:stage') || 34 | (github.event.pull_request.merged && 35 | contains(github.event.pull_request.labels.*.name, 'release:publish')) 36 | 37 | runs-on: ubuntu-latest 38 | 39 | # When manually triggering the build, the requester can specify a target branch or a tag 40 | # via the 'ref' client parameter. 41 | steps: 42 | - name: Check out code 43 | uses: actions/checkout@v4 44 | with: 45 | ref: ${{ github.event.client_payload.ref || github.ref }} 46 | 47 | - name: Set up Go 48 | uses: actions/setup-go@v5 49 | with: 50 | go-version: '1.23' 51 | 52 | - name: Install golint 53 | run: go install golang.org/x/lint/golint@latest 54 | 55 | - name: Run Linter 56 | run: | 57 | golint -set_exit_status ./... 58 | 59 | - name: Run Tests 60 | run: ./.github/scripts/run_all_tests.sh 61 | env: 62 | FIREBASE_SERVICE_ACCT_KEY: ${{ secrets.FIREBASE_SERVICE_ACCT_KEY }} 63 | FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} 64 | 65 | publish_release: 66 | needs: stage_release 67 | 68 | # Check whether the release should be published. We publish only when the trigger PR is 69 | # 1. merged 70 | # 2. to the dev branch 71 | # 3. with the label 'release:publish', and 72 | # 4. the title prefix '[chore] Release '. 73 | if: github.event.pull_request.merged && 74 | github.ref == 'refs/heads/dev' && 75 | contains(github.event.pull_request.labels.*.name, 'release:publish') && 76 | startsWith(github.event.pull_request.title, '[chore] Release ') 77 | 78 | runs-on: ubuntu-latest 79 | permissions: 80 | contents: write 81 | 82 | steps: 83 | - name: Checkout source for publish 84 | uses: actions/checkout@v4 85 | with: 86 | persist-credentials: false 87 | 88 | - name: Publish preflight check 89 | id: preflight 90 | run: ./.github/scripts/publish_preflight_check.sh 91 | 92 | # We authorize this step with an access token that has write access to the master branch. 93 | - name: Merge to master 94 | uses: actions/github-script@v7 95 | with: 96 | github-token: ${{ secrets.FIREBASE_GITHUB_TOKEN }} 97 | script: | 98 | github.rest.repos.merge({ 99 | owner: context.repo.owner, 100 | repo: context.repo.repo, 101 | base: 'master', 102 | head: 'dev' 103 | }) 104 | 105 | # See: https://cli.github.com/manual/gh_release_create 106 | - name: Create release tag 107 | env: 108 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 109 | run: gh release create ${{ steps.preflight.outputs.version }} 110 | --title "Firebase Admin Go SDK ${{ steps.preflight.outputs.version }}" 111 | --notes '${{ steps.preflight.outputs.changelog }}' 112 | --target "master" 113 | 114 | # Post to Twitter if explicitly opted-in by adding the label 'release:tweet'. 115 | - name: Post to Twitter 116 | if: success() && 117 | contains(github.event.pull_request.labels.*.name, 'release:tweet') 118 | uses: firebase/firebase-admin-node/.github/actions/send-tweet@master 119 | with: 120 | status: > 121 | ${{ steps.preflight.outputs.version }} of @Firebase Admin Go SDK is available. 122 | https://github.com/firebase/firebase-admin-go/releases/tag/${{ steps.preflight.outputs.version }} 123 | consumer-key: ${{ secrets.FIREBASE_TWITTER_CONSUMER_KEY }} 124 | consumer-secret: ${{ secrets.FIREBASE_TWITTER_CONSUMER_SECRET }} 125 | access-token: ${{ secrets.FIREBASE_TWITTER_ACCESS_TOKEN }} 126 | access-token-secret: ${{ secrets.FIREBASE_TWITTER_ACCESS_TOKEN_SECRET }} 127 | continue-on-error: true 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | testdata/integration_* 2 | .vscode/* 3 | *~ 4 | \#*\# 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.opensource/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Firebase Admin SDK - Go", 3 | "platforms": [ 4 | "Go", 5 | "Admin" 6 | ], 7 | "content": "README.md", 8 | "pages": [], 9 | "related": [ 10 | "firebase/firebase-admin-java", 11 | "firebase/firebase-admin-node", 12 | "firebase/firebase-admin-python" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/firebase/firebase-admin-go/workflows/Continuous%20Integration/badge.svg?branch=dev)](https://github.com/firebase/firebase-admin-go/actions) 2 | [![GoDoc](https://godoc.org/firebase.google.com/go?status.svg)](https://godoc.org/firebase.google.com/go) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/firebase/firebase-admin-go)](https://goreportcard.com/report/github.com/firebase/firebase-admin-go) 4 | 5 | # Firebase Admin Go SDK 6 | 7 | ## Table of Contents 8 | 9 | * [Overview](#overview) 10 | * [Installation](#installation) 11 | * [Contributing](#contributing) 12 | * [Documentation](#documentation) 13 | * [License and Terms](#license-and-terms) 14 | 15 | ## Overview 16 | 17 | [Firebase](https://firebase.google.com) provides the tools and infrastructure 18 | you need to develop apps, grow your user base, and earn money. The Firebase 19 | Admin Go SDK enables access to Firebase services from privileged environments 20 | (such as servers or cloud) in Go. Currently this SDK provides 21 | Firebase custom authentication support. 22 | 23 | For more information, visit the 24 | [Firebase Admin SDK setup guide](https://firebase.google.com/docs/admin/setup/). 25 | 26 | 27 | ## Installation 28 | 29 | The Firebase Admin Go SDK can be installed using the `go get` utility: 30 | 31 | ``` 32 | # Install the latest version: 33 | go get firebase.google.com/go/v4@latest 34 | 35 | # Or install a specific version: 36 | go get firebase.google.com/go/v4@4.x.x 37 | ``` 38 | 39 | ## Contributing 40 | 41 | Please refer to the [CONTRIBUTING page](./CONTRIBUTING.md) for more information 42 | about how you can contribute to this project. We welcome bug reports, feature 43 | requests, code review feedback, and also pull requests. 44 | 45 | ## Supported Go Versions 46 | 47 | The Admin Go SDK is compatible with the two most-recent major Go releases. 48 | We currently support Go v1.23 and 1.24. 49 | [Continuous integration](https://github.com/firebase/firebase-admin-go/actions) system 50 | tests the code on Go v1.23 and v1.24. 51 | 52 | ## Documentation 53 | 54 | * [Setup Guide](https://firebase.google.com/docs/admin/setup/) 55 | * [Authentication Guide](https://firebase.google.com/docs/auth/admin/) 56 | * [Cloud Firestore](https://firebase.google.com/docs/firestore/) 57 | * [Cloud Messaging Guide](https://firebase.google.com/docs/cloud-messaging/admin/) 58 | * [Storage Guide](https://firebase.google.com/docs/storage/admin/start) 59 | * [API Reference](https://godoc.org/firebase.google.com/go) 60 | * [Release Notes](https://firebase.google.com/support/release-notes/admin/go) 61 | 62 | 63 | ## License and Terms 64 | 65 | Firebase Admin Go SDK is licensed under the 66 | [Apache License, version 2.0](http://www.apache.org/licenses/LICENSE-2.0). 67 | 68 | Your use of Firebase is governed by the 69 | [Terms of Service for Firebase Services](https://firebase.google.com/terms/). 70 | -------------------------------------------------------------------------------- /appcheck/appcheck.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package appcheck provides functionality for verifying App Check tokens. 16 | package appcheck 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "strings" 22 | "time" 23 | 24 | "github.com/MicahParks/keyfunc" 25 | "github.com/golang-jwt/jwt/v4" 26 | 27 | "firebase.google.com/go/v4/internal" 28 | ) 29 | 30 | // JWKSUrl is the URL of the JWKS used to verify App Check tokens. 31 | var JWKSUrl = "https://firebaseappcheck.googleapis.com/v1beta/jwks" 32 | 33 | const appCheckIssuer = "https://firebaseappcheck.googleapis.com/" 34 | 35 | var ( 36 | // ErrIncorrectAlgorithm is returned when the token is signed with a non-RSA256 algorithm. 37 | ErrIncorrectAlgorithm = errors.New("token has incorrect algorithm") 38 | // ErrTokenType is returned when the token is not a JWT. 39 | ErrTokenType = errors.New("token has incorrect type") 40 | // ErrTokenClaims is returned when the token claims cannot be decoded. 41 | ErrTokenClaims = errors.New("token has incorrect claims") 42 | // ErrTokenAudience is returned when the token audience does not match the current project. 43 | ErrTokenAudience = errors.New("token has incorrect audience") 44 | // ErrTokenIssuer is returned when the token issuer does not match Firebase's App Check service. 45 | ErrTokenIssuer = errors.New("token has incorrect issuer") 46 | // ErrTokenSubject is returned when the token subject is empty or missing. 47 | ErrTokenSubject = errors.New("token has empty or missing subject") 48 | ) 49 | 50 | // DecodedAppCheckToken represents a verified App Check token. 51 | // 52 | // DecodedAppCheckToken provides typed accessors to the common JWT fields such as Audience (aud) 53 | // and ExpiresAt (exp). Additionally it provides an AppID field, which indicates the application ID to which this 54 | // token belongs. Any additional JWT claims can be accessed via the Claims map of DecodedAppCheckToken. 55 | type DecodedAppCheckToken struct { 56 | Issuer string 57 | Subject string 58 | Audience []string 59 | ExpiresAt time.Time 60 | IssuedAt time.Time 61 | AppID string 62 | Claims map[string]interface{} 63 | } 64 | 65 | // Client is the interface for the Firebase App Check service. 66 | type Client struct { 67 | projectID string 68 | jwks *keyfunc.JWKS 69 | } 70 | 71 | // NewClient creates a new instance of the Firebase App Check Client. 72 | // 73 | // This function can only be invoked from within the SDK. Client applications should access the 74 | // the App Check service through firebase.App. 75 | func NewClient(ctx context.Context, conf *internal.AppCheckConfig) (*Client, error) { 76 | // TODO: Add support for overriding the HTTP client using the App one. 77 | jwks, err := keyfunc.Get(JWKSUrl, keyfunc.Options{ 78 | Ctx: ctx, 79 | RefreshInterval: 6 * time.Hour, 80 | }) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return &Client{ 86 | projectID: conf.ProjectID, 87 | jwks: jwks, 88 | }, nil 89 | } 90 | 91 | // VerifyToken verifies the given App Check token. 92 | // 93 | // VerifyToken considers an App Check token string to be valid if all the following conditions are met: 94 | // - The token string is a valid RS256 JWT. 95 | // - The JWT contains valid issuer (iss) and audience (aud) claims that match the issuerPrefix 96 | // and projectID of the tokenVerifier. 97 | // - The JWT contains a valid subject (sub) claim. 98 | // - The JWT is not expired, and it has been issued some time in the past. 99 | // - The JWT is signed by a Firebase App Check backend server as determined by the keySource. 100 | // 101 | // If any of the above conditions are not met, an error is returned. Otherwise a pointer to a 102 | // decoded App Check token is returned. 103 | func (c *Client) VerifyToken(token string) (*DecodedAppCheckToken, error) { 104 | // References for checks: 105 | // https://firebase.googleblog.com/2021/10/protecting-backends-with-app-check.html 106 | // https://github.com/firebase/firebase-admin-node/blob/master/src/app-check/token-verifier.ts#L106 107 | 108 | // The standard JWT parser also validates the expiration of the token 109 | // so we do not need dedicated code for that. 110 | decodedToken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { 111 | if t.Header["alg"] != "RS256" { 112 | return nil, ErrIncorrectAlgorithm 113 | } 114 | if t.Header["typ"] != "JWT" { 115 | return nil, ErrTokenType 116 | } 117 | return c.jwks.Keyfunc(t) 118 | }) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | claims, ok := decodedToken.Claims.(jwt.MapClaims) 124 | if !ok { 125 | return nil, ErrTokenClaims 126 | } 127 | 128 | rawAud := claims["aud"].([]interface{}) 129 | aud := []string{} 130 | for _, v := range rawAud { 131 | aud = append(aud, v.(string)) 132 | } 133 | 134 | if !contains(aud, "projects/"+c.projectID) { 135 | return nil, ErrTokenAudience 136 | } 137 | 138 | // We check the prefix to make sure this token was issued 139 | // by the Firebase App Check service, but we do not check the 140 | // Project Number suffix because the Golang SDK only has project ID. 141 | // 142 | // This is consistent with the Firebase Admin Node SDK. 143 | if !strings.HasPrefix(claims["iss"].(string), appCheckIssuer) { 144 | return nil, ErrTokenIssuer 145 | } 146 | 147 | if val, ok := claims["sub"].(string); !ok || val == "" { 148 | return nil, ErrTokenSubject 149 | } 150 | 151 | appCheckToken := DecodedAppCheckToken{ 152 | Issuer: claims["iss"].(string), 153 | Subject: claims["sub"].(string), 154 | Audience: aud, 155 | ExpiresAt: time.Unix(int64(claims["exp"].(float64)), 0), 156 | IssuedAt: time.Unix(int64(claims["iat"].(float64)), 0), 157 | AppID: claims["sub"].(string), 158 | } 159 | 160 | // Remove all the claims we've already parsed. 161 | for _, usedClaim := range []string{"iss", "sub", "aud", "exp", "iat", "sub"} { 162 | delete(claims, usedClaim) 163 | } 164 | appCheckToken.Claims = claims 165 | 166 | return &appCheckToken, nil 167 | } 168 | 169 | func contains(s []string, str string) bool { 170 | for _, v := range s { 171 | if v == str { 172 | return true 173 | } 174 | } 175 | return false 176 | } 177 | -------------------------------------------------------------------------------- /auth/auth_appengine.go: -------------------------------------------------------------------------------- 1 | //go:build appengine 2 | // +build appengine 3 | 4 | // Copyright 2017 Google Inc. All Rights Reserved. 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | 18 | package auth 19 | 20 | import ( 21 | "context" 22 | 23 | "firebase.google.com/go/v4/internal" 24 | "google.golang.org/appengine/v2" 25 | ) 26 | 27 | type aeSigner struct{} 28 | 29 | func newCryptoSigner(ctx context.Context, conf *internal.AuthConfig) (cryptoSigner, error) { 30 | return aeSigner{}, nil 31 | } 32 | 33 | func (s aeSigner) Email(ctx context.Context) (string, error) { 34 | return appengine.ServiceAccount(ctx) 35 | } 36 | 37 | func (s aeSigner) Sign(ctx context.Context, b []byte) ([]byte, error) { 38 | _, sig, err := appengine.SignBytes(ctx, b) 39 | return sig, err 40 | } 41 | -------------------------------------------------------------------------------- /auth/auth_std.go: -------------------------------------------------------------------------------- 1 | //go:build !appengine 2 | // +build !appengine 3 | 4 | // Copyright 2017 Google Inc. All Rights Reserved. 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | 18 | package auth 19 | 20 | import ( 21 | "context" 22 | 23 | "firebase.google.com/go/v4/internal" 24 | ) 25 | 26 | func newCryptoSigner(ctx context.Context, conf *internal.AuthConfig) (cryptoSigner, error) { 27 | return newIAMSigner(ctx, conf) 28 | } 29 | -------------------------------------------------------------------------------- /auth/email_action_links.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package auth 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "net/url" 23 | ) 24 | 25 | // ActionCodeSettings specifies the required continue/state URL with optional Android and iOS settings. Used when 26 | // invoking the email action link generation APIs. 27 | type ActionCodeSettings struct { 28 | URL string `json:"continueUrl"` 29 | HandleCodeInApp bool `json:"canHandleCodeInApp"` 30 | IOSBundleID string `json:"iOSBundleId,omitempty"` 31 | AndroidPackageName string `json:"androidPackageName,omitempty"` 32 | AndroidMinimumVersion string `json:"androidMinimumVersion,omitempty"` 33 | AndroidInstallApp bool `json:"androidInstallApp,omitempty"` 34 | DynamicLinkDomain string `json:"dynamicLinkDomain,omitempty"` 35 | } 36 | 37 | func (settings *ActionCodeSettings) toMap() (map[string]interface{}, error) { 38 | if settings.URL == "" { 39 | return nil, errors.New("URL must not be empty") 40 | } 41 | 42 | url, err := url.Parse(settings.URL) 43 | if err != nil || url.Scheme == "" || url.Host == "" { 44 | return nil, fmt.Errorf("malformed url string: %q", settings.URL) 45 | } 46 | 47 | if settings.AndroidMinimumVersion != "" || settings.AndroidInstallApp { 48 | if settings.AndroidPackageName == "" { 49 | return nil, errors.New("Android package name is required when specifying other Android settings") 50 | } 51 | } 52 | 53 | b, err := json.Marshal(settings) 54 | if err != nil { 55 | return nil, err 56 | } 57 | var result map[string]interface{} 58 | if err := json.Unmarshal(b, &result); err != nil { 59 | return nil, err 60 | } 61 | return result, nil 62 | } 63 | 64 | type linkType string 65 | 66 | const ( 67 | emailLinkSignIn linkType = "EMAIL_SIGNIN" 68 | emailVerification linkType = "VERIFY_EMAIL" 69 | passwordReset linkType = "PASSWORD_RESET" 70 | ) 71 | 72 | // EmailVerificationLink generates the out-of-band email action link for email verification flows for the specified 73 | // email address. 74 | func (c *baseClient) EmailVerificationLink(ctx context.Context, email string) (string, error) { 75 | return c.EmailVerificationLinkWithSettings(ctx, email, nil) 76 | } 77 | 78 | // EmailVerificationLinkWithSettings generates the out-of-band email action link for email verification flows for the 79 | // specified email address, using the action code settings provided. 80 | func (c *baseClient) EmailVerificationLinkWithSettings( 81 | ctx context.Context, email string, settings *ActionCodeSettings) (string, error) { 82 | return c.generateEmailActionLink(ctx, emailVerification, email, settings) 83 | } 84 | 85 | // PasswordResetLink generates the out-of-band email action link for password reset flows for the specified email 86 | // address. 87 | func (c *baseClient) PasswordResetLink(ctx context.Context, email string) (string, error) { 88 | return c.PasswordResetLinkWithSettings(ctx, email, nil) 89 | } 90 | 91 | // PasswordResetLinkWithSettings generates the out-of-band email action link for password reset flows for the 92 | // specified email address, using the action code settings provided. 93 | func (c *baseClient) PasswordResetLinkWithSettings( 94 | ctx context.Context, email string, settings *ActionCodeSettings) (string, error) { 95 | return c.generateEmailActionLink(ctx, passwordReset, email, settings) 96 | } 97 | 98 | // EmailSignInLink generates the out-of-band email action link for email link sign-in flows, using the action 99 | // code settings provided. 100 | func (c *baseClient) EmailSignInLink( 101 | ctx context.Context, email string, settings *ActionCodeSettings) (string, error) { 102 | return c.generateEmailActionLink(ctx, emailLinkSignIn, email, settings) 103 | } 104 | 105 | func (c *baseClient) generateEmailActionLink( 106 | ctx context.Context, linkType linkType, email string, settings *ActionCodeSettings) (string, error) { 107 | 108 | if email == "" { 109 | return "", errors.New("email must not be empty") 110 | } 111 | 112 | if linkType == emailLinkSignIn && settings == nil { 113 | return "", errors.New("ActionCodeSettings must not be nil when generating sign-in links") 114 | } 115 | 116 | payload := map[string]interface{}{ 117 | "requestType": linkType, 118 | "email": email, 119 | "returnOobLink": true, 120 | } 121 | if settings != nil { 122 | settingsMap, err := settings.toMap() 123 | if err != nil { 124 | return "", err 125 | } 126 | for k, v := range settingsMap { 127 | payload[k] = v 128 | } 129 | } 130 | 131 | var result struct { 132 | OOBLink string `json:"oobLink"` 133 | } 134 | _, err := c.post(ctx, "/accounts:sendOobCode", payload, &result) 135 | return result.OOBLink, err 136 | } 137 | -------------------------------------------------------------------------------- /auth/export_users.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package auth 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "net/http" 21 | "net/url" 22 | "strconv" 23 | 24 | "firebase.google.com/go/v4/internal" 25 | "google.golang.org/api/iterator" 26 | ) 27 | 28 | const maxReturnedResults = 1000 29 | 30 | // Users returns an iterator over Users. 31 | // 32 | // If nextPageToken is empty, the iterator will start at the beginning. 33 | // If the nextPageToken is not empty, the iterator starts after the token. 34 | func (c *baseClient) Users(ctx context.Context, nextPageToken string) *UserIterator { 35 | it := &UserIterator{ 36 | ctx: ctx, 37 | client: c, 38 | } 39 | it.pageInfo, it.nextFunc = iterator.NewPageInfo( 40 | it.fetch, 41 | func() int { return len(it.users) }, 42 | func() interface{} { b := it.users; it.users = nil; return b }) 43 | it.pageInfo.MaxSize = maxReturnedResults 44 | it.pageInfo.Token = nextPageToken 45 | return it 46 | } 47 | 48 | // UserIterator is an iterator over Users. 49 | // 50 | // Also see: https://github.com/GoogleCloudPlatform/google-cloud-go/wiki/Iterator-Guidelines 51 | type UserIterator struct { 52 | client *baseClient 53 | ctx context.Context 54 | nextFunc func() error 55 | pageInfo *iterator.PageInfo 56 | users []*ExportedUserRecord 57 | } 58 | 59 | // PageInfo supports pagination. See the google.golang.org/api/iterator package for details. 60 | // Page size can be determined by the NewPager(...) function described there. 61 | func (it *UserIterator) PageInfo() *iterator.PageInfo { return it.pageInfo } 62 | 63 | // Next returns the next result. Its second return value is [iterator.Done] if 64 | // there are no more results. Once Next returns [iterator.Done], all subsequent 65 | // calls will return [iterator.Done]. 66 | func (it *UserIterator) Next() (*ExportedUserRecord, error) { 67 | if err := it.nextFunc(); err != nil { 68 | return nil, err 69 | } 70 | user := it.users[0] 71 | it.users = it.users[1:] 72 | return user, nil 73 | } 74 | 75 | func (it *UserIterator) fetch(pageSize int, pageToken string) (string, error) { 76 | query := make(url.Values) 77 | query.Set("maxResults", strconv.Itoa(pageSize)) 78 | if pageToken != "" { 79 | query.Set("nextPageToken", pageToken) 80 | } 81 | 82 | url, err := it.client.makeUserMgtURL(fmt.Sprintf("/accounts:batchGet?%s", query.Encode())) 83 | if err != nil { 84 | return "", err 85 | } 86 | 87 | req := &internal.Request{ 88 | Method: http.MethodGet, 89 | URL: url, 90 | } 91 | var parsed struct { 92 | Users []userQueryResponse `json:"users"` 93 | NextPageToken string `json:"nextPageToken"` 94 | } 95 | _, err = it.client.httpClient.DoAndUnmarshal(it.ctx, req, &parsed) 96 | if err != nil { 97 | return "", err 98 | } 99 | 100 | for _, u := range parsed.Users { 101 | eu, err := u.makeExportedUserRecord() 102 | if err != nil { 103 | return "", err 104 | } 105 | it.users = append(it.users, eu) 106 | } 107 | it.pageInfo.Token = parsed.NextPageToken 108 | return parsed.NextPageToken, nil 109 | } 110 | 111 | // ExportedUserRecord is the returned user value used when listing all the users. 112 | type ExportedUserRecord struct { 113 | *UserRecord 114 | PasswordHash string 115 | PasswordSalt string 116 | } 117 | -------------------------------------------------------------------------------- /auth/multi_factor_config_mgt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package auth 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | // ProviderConfig represents a multi-factor auth provider configuration. 22 | // Currently, only TOTP is supported. 23 | type ProviderConfig struct { 24 | // The state of multi-factor configuration, whether it's enabled or disabled. 25 | State MultiFactorConfigState `json:"state"` 26 | // TOTPProviderConfig holds the TOTP (time-based one-time password) configuration that is used in second factor authentication. 27 | TOTPProviderConfig *TOTPProviderConfig `json:"totpProviderConfig,omitempty"` 28 | } 29 | 30 | // TOTPProviderConfig represents configuration settings for TOTP second factor auth. 31 | type TOTPProviderConfig struct { 32 | // The number of adjacent intervals used by TOTP. 33 | AdjacentIntervals int `json:"adjacentIntervals,omitempty"` 34 | } 35 | 36 | // MultiFactorConfigState represents whether the multi-factor configuration is enabled or disabled. 37 | type MultiFactorConfigState string 38 | 39 | // These constants represent the possible values for the MultiFactorConfigState type. 40 | const ( 41 | Enabled MultiFactorConfigState = "ENABLED" 42 | Disabled MultiFactorConfigState = "DISABLED" 43 | ) 44 | 45 | // MultiFactorConfig represents a multi-factor configuration for a tenant or project. 46 | // This can be used to define whether multi-factor authentication is enabled or disabled and the list of second factor challenges that are supported. 47 | type MultiFactorConfig struct { 48 | // A slice of pointers to ProviderConfig structs, each outlining the specific second factor authorization method. 49 | ProviderConfigs []*ProviderConfig `json:"providerConfigs,omitempty"` 50 | } 51 | 52 | func (mfa *MultiFactorConfig) validate() error { 53 | if mfa == nil { 54 | return nil 55 | } 56 | if len(mfa.ProviderConfigs) == 0 { 57 | return fmt.Errorf("\"ProviderConfigs\" must be a non-empty array of type \"ProviderConfig\"s") 58 | } 59 | for _, providerConfig := range mfa.ProviderConfigs { 60 | if providerConfig == nil { 61 | return fmt.Errorf("\"ProviderConfigs\" must be a non-empty array of type \"ProviderConfig\"s") 62 | } 63 | if err := providerConfig.validate(); err != nil { 64 | return err 65 | } 66 | } 67 | return nil 68 | } 69 | 70 | func (pvc *ProviderConfig) validate() error { 71 | if pvc.State == "" && pvc.TOTPProviderConfig == nil { 72 | return fmt.Errorf("\"ProviderConfig\" must be defined") 73 | } 74 | state := string(pvc.State) 75 | if state != string(Enabled) && state != string(Disabled) { 76 | return fmt.Errorf("\"ProviderConfig.State\" must be 'Enabled' or 'Disabled'") 77 | } 78 | return pvc.TOTPProviderConfig.validate() 79 | } 80 | 81 | func (tpvc *TOTPProviderConfig) validate() error { 82 | if tpvc == nil { 83 | return fmt.Errorf("\"TOTPProviderConfig\" must be defined") 84 | } 85 | if !(tpvc.AdjacentIntervals >= 1 && tpvc.AdjacentIntervals <= 10) { 86 | return fmt.Errorf("\"AdjacentIntervals\" must be an integer between 1 and 10 (inclusive)") 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /auth/multi_factor_config_mgt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package auth 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestMultiFactorConfig(t *testing.T) { 22 | mfa := MultiFactorConfig{ 23 | ProviderConfigs: []*ProviderConfig{{ 24 | State: Disabled, 25 | TOTPProviderConfig: &TOTPProviderConfig{ 26 | AdjacentIntervals: 5, 27 | }, 28 | }}, 29 | } 30 | if err := mfa.validate(); err != nil { 31 | t.Errorf("MultiFactorConfig not valid") 32 | } 33 | } 34 | func TestMultiFactorConfigNoProviderConfigs(t *testing.T) { 35 | mfa := MultiFactorConfig{} 36 | want := "\"ProviderConfigs\" must be a non-empty array of type \"ProviderConfig\"s" 37 | if err := mfa.validate(); err.Error() != want { 38 | t.Errorf("MultiFactorConfig.validate(nil) = %v, want = %q", err, want) 39 | } 40 | } 41 | 42 | func TestMultiFactorConfigNilProviderConfigs(t *testing.T) { 43 | mfa := MultiFactorConfig{ 44 | ProviderConfigs: nil, 45 | } 46 | want := "\"ProviderConfigs\" must be a non-empty array of type \"ProviderConfig\"s" 47 | if err := mfa.validate(); err.Error() != want { 48 | t.Errorf("MultiFactorConfig.validate(nil) = %v, want = %q", err, want) 49 | } 50 | } 51 | 52 | func TestMultiFactorConfigNilProviderConfig(t *testing.T) { 53 | mfa := MultiFactorConfig{ 54 | ProviderConfigs: []*ProviderConfig{nil}, 55 | } 56 | want := "\"ProviderConfigs\" must be a non-empty array of type \"ProviderConfig\"s" 57 | if err := mfa.validate(); err.Error() != want { 58 | t.Errorf("MultiFactorConfig.validate(nil) = %v, want = %q", err, want) 59 | } 60 | } 61 | 62 | func TestMultiFactorConfigUndefinedProviderConfig(t *testing.T) { 63 | mfa := MultiFactorConfig{ 64 | ProviderConfigs: []*ProviderConfig{{}}, 65 | } 66 | want := "\"ProviderConfig\" must be defined" 67 | if err := mfa.validate(); err.Error() != want { 68 | t.Errorf("MultiFactorConfig.validate(nil) = %v, want = %q", err, want) 69 | } 70 | } 71 | 72 | func TestMultiFactorConfigInvalidProviderConfigState(t *testing.T) { 73 | mfa := MultiFactorConfig{ 74 | ProviderConfigs: []*ProviderConfig{{ 75 | State: "invalid", 76 | }}, 77 | } 78 | want := "\"ProviderConfig.State\" must be 'Enabled' or 'Disabled'" 79 | if err := mfa.validate(); err.Error() != want { 80 | t.Errorf("MultiFactorConfig.validate(nil) = %v, want = %q", err, want) 81 | } 82 | } 83 | 84 | func TestMultiFactorConfigNilTOTPProviderConfig(t *testing.T) { 85 | mfa := MultiFactorConfig{ 86 | ProviderConfigs: []*ProviderConfig{{ 87 | State: Disabled, 88 | TOTPProviderConfig: nil, 89 | }}, 90 | } 91 | want := "\"TOTPProviderConfig\" must be defined" 92 | if err := mfa.validate(); err.Error() != want { 93 | t.Errorf("MultiFactorConfig.validate(nil) = %v, want = %q", err, want) 94 | } 95 | } 96 | 97 | func TestMultiFactorConfigInvalidAdjacentIntervals(t *testing.T) { 98 | mfa := MultiFactorConfig{ 99 | ProviderConfigs: []*ProviderConfig{{ 100 | State: Disabled, 101 | TOTPProviderConfig: &TOTPProviderConfig{ 102 | AdjacentIntervals: 11, 103 | }, 104 | }}, 105 | } 106 | want := "\"AdjacentIntervals\" must be an integer between 1 and 10 (inclusive)" 107 | if err := mfa.validate(); err.Error() != want { 108 | t.Errorf("MultiFactorConfig.validate(nil) = %v, want = %q", err, want) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /auth/project_config_mgt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package auth 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "net/http" 22 | "strings" 23 | 24 | "firebase.google.com/go/v4/internal" 25 | ) 26 | 27 | // ProjectConfig represents the properties to update on the provided project config. 28 | type ProjectConfig struct { 29 | MultiFactorConfig *MultiFactorConfig `json:"mfa,omitEmpty"` 30 | } 31 | 32 | func (base *baseClient) GetProjectConfig(ctx context.Context) (*ProjectConfig, error) { 33 | req := &internal.Request{ 34 | Method: http.MethodGet, 35 | URL: "/config", 36 | } 37 | var result ProjectConfig 38 | if _, err := base.makeRequest(ctx, req, &result); err != nil { 39 | return nil, err 40 | } 41 | return &result, nil 42 | } 43 | 44 | func (base *baseClient) UpdateProjectConfig(ctx context.Context, projectConfig *ProjectConfigToUpdate) (*ProjectConfig, error) { 45 | if projectConfig == nil { 46 | return nil, errors.New("project config must not be nil") 47 | } 48 | if err := projectConfig.validate(); err != nil { 49 | return nil, err 50 | } 51 | mask := projectConfig.params.UpdateMask() 52 | if len(mask) == 0 { 53 | return nil, errors.New("no parameters specified in the update request") 54 | } 55 | req := &internal.Request{ 56 | Method: http.MethodPatch, 57 | URL: "/config", 58 | Body: internal.NewJSONEntity(projectConfig.params), 59 | Opts: []internal.HTTPOption{ 60 | internal.WithQueryParam("updateMask", strings.Join(mask, ",")), 61 | }, 62 | } 63 | var result ProjectConfig 64 | if _, err := base.makeRequest(ctx, req, &result); err != nil { 65 | return nil, err 66 | } 67 | return &result, nil 68 | } 69 | 70 | // ProjectConfigToUpdate represents the options used to update the current project. 71 | type ProjectConfigToUpdate struct { 72 | params nestedMap 73 | } 74 | 75 | const ( 76 | multiFactorConfigProjectKey = "mfa" 77 | ) 78 | 79 | // MultiFactorConfig configures the project's multi-factor settings 80 | func (pc *ProjectConfigToUpdate) MultiFactorConfig(multiFactorConfig MultiFactorConfig) *ProjectConfigToUpdate { 81 | return pc.set(multiFactorConfigProjectKey, multiFactorConfig) 82 | } 83 | 84 | func (pc *ProjectConfigToUpdate) set(key string, value interface{}) *ProjectConfigToUpdate { 85 | pc.ensureParams().Set(key, value) 86 | return pc 87 | } 88 | 89 | func (pc *ProjectConfigToUpdate) ensureParams() nestedMap { 90 | if pc.params == nil { 91 | pc.params = make(nestedMap) 92 | } 93 | return pc.params 94 | } 95 | 96 | func (pc *ProjectConfigToUpdate) validate() error { 97 | req := make(map[string]interface{}) 98 | for k, v := range pc.params { 99 | req[k] = v 100 | } 101 | val, ok := req[multiFactorConfigProjectKey] 102 | if ok { 103 | multiFactorConfig, ok := val.(MultiFactorConfig) 104 | if !ok { 105 | return fmt.Errorf("invalid type for MultiFactorConfig: %s", req[multiFactorConfigProjectKey]) 106 | } 107 | if err := multiFactorConfig.validate(); err != nil { 108 | return err 109 | } 110 | } 111 | return nil 112 | } 113 | -------------------------------------------------------------------------------- /auth/project_config_mgt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package auth 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | "fmt" 20 | "net/http" 21 | "reflect" 22 | "sort" 23 | "strings" 24 | "testing" 25 | 26 | "github.com/google/go-cmp/cmp" 27 | ) 28 | 29 | const projectConfigResponse = `{ 30 | "mfa": { 31 | "providerConfigs": [ 32 | { 33 | "state":"ENABLED", 34 | "totpProviderConfig":{ 35 | "adjacentIntervals":5 36 | } 37 | } 38 | ] 39 | } 40 | }` 41 | 42 | var testProjectConfig = &ProjectConfig{ 43 | MultiFactorConfig: &MultiFactorConfig{ 44 | ProviderConfigs: []*ProviderConfig{ 45 | { 46 | State: Enabled, 47 | TOTPProviderConfig: &TOTPProviderConfig{ 48 | AdjacentIntervals: 5, 49 | }, 50 | }, 51 | }, 52 | }, 53 | } 54 | 55 | func TestGetProjectConfig(t *testing.T) { 56 | s := echoServer([]byte(projectConfigResponse), t) 57 | defer s.Close() 58 | 59 | client := s.Client 60 | projectConfig, err := client.GetProjectConfig(context.Background()) 61 | 62 | if err != nil { 63 | t.Errorf("GetProjectConfig() = %v", err) 64 | } 65 | if !reflect.DeepEqual(projectConfig, testProjectConfig) { 66 | t.Errorf("GetProjectConfig() = %#v, want = %#v", projectConfig, testProjectConfig) 67 | } 68 | } 69 | 70 | func TestUpdateProjectConfig(t *testing.T) { 71 | s := echoServer([]byte(projectConfigResponse), t) 72 | defer s.Close() 73 | 74 | client := s.Client 75 | options := (&ProjectConfigToUpdate{}). 76 | MultiFactorConfig(*testProjectConfig.MultiFactorConfig) 77 | projectConfig, err := client.UpdateProjectConfig(context.Background(), options) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | if !reflect.DeepEqual(projectConfig, testProjectConfig) { 83 | t.Errorf("UpdateProjectConfig() = %#v; want = %#v", projectConfig, testProjectConfig) 84 | } 85 | wantBody := map[string]interface{}{ 86 | "mfa": map[string]interface{}{ 87 | "providerConfigs": []interface{}{ 88 | map[string]interface{}{ 89 | "state": "ENABLED", 90 | "totpProviderConfig": map[string]interface{}{ 91 | "adjacentIntervals": float64(5), 92 | }, 93 | }, 94 | }, 95 | }, 96 | } 97 | wantMask := []string{"mfa"} 98 | if err := checkUpdateProjectConfigRequest(s, wantBody, wantMask); err != nil { 99 | t.Fatal(err) 100 | } 101 | } 102 | 103 | func TestUpdateProjectNilOptions(t *testing.T) { 104 | base := &baseClient{} 105 | want := "project config must not be nil" 106 | if _, err := base.UpdateProjectConfig(context.Background(), nil); err == nil || err.Error() != want { 107 | t.Errorf("UpdateProject(nil) = %v, want = %q", err, want) 108 | } 109 | } 110 | 111 | func checkUpdateProjectConfigRequest(s *mockAuthServer, wantBody interface{}, wantMask []string) error { 112 | req := s.Req[0] 113 | if req.Method != http.MethodPatch { 114 | return fmt.Errorf("UpdateProjectConfig() Method = %q; want = %q", req.Method, http.MethodPatch) 115 | } 116 | 117 | wantURL := "/projects/mock-project-id/config" 118 | if req.URL.Path != wantURL { 119 | return fmt.Errorf("UpdateProjectConfig() URL = %q; want = %q", req.URL.Path, wantURL) 120 | } 121 | 122 | queryParam := req.URL.Query().Get("updateMask") 123 | mask := strings.Split(queryParam, ",") 124 | sort.Strings(mask) 125 | if !reflect.DeepEqual(mask, wantMask) { 126 | return fmt.Errorf("UpdateProjectConfig() Query = %#v; want = %#v", mask, wantMask) 127 | } 128 | 129 | var body map[string]interface{} 130 | if err := json.Unmarshal(s.Rbody, &body); err != nil { 131 | return err 132 | } 133 | 134 | if diff := cmp.Diff(body, wantBody); diff != "" { 135 | fmt.Printf("UpdateProjectConfig() diff = %s", diff) 136 | } 137 | 138 | if !reflect.DeepEqual(body, wantBody) { 139 | return fmt.Errorf("UpdateProjectConfig() Body = %#v; want = %#v", body, wantBody) 140 | } 141 | 142 | return nil 143 | } 144 | -------------------------------------------------------------------------------- /auth/token_verifier_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package auth 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "io" 22 | "io/ioutil" 23 | "net/http" 24 | "testing" 25 | "time" 26 | 27 | "firebase.google.com/go/v4/internal" 28 | ) 29 | 30 | func TestNewIDTokenVerifier(t *testing.T) { 31 | tv, err := newIDTokenVerifier(context.Background(), testProjectID) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | if tv.shortName != "ID token" { 36 | t.Errorf("tokenVerifier.shortName = %q; want = %q", tv.shortName, "ID token") 37 | } 38 | if tv.projectID != testProjectID { 39 | t.Errorf("tokenVerifier.projectID = %q; want = %q", tv.projectID, testProjectID) 40 | } 41 | if tv.issuerPrefix != idTokenIssuerPrefix { 42 | t.Errorf("tokenVerifier.issuerPrefix = %q; want = %q", tv.issuerPrefix, idTokenIssuerPrefix) 43 | } 44 | ks, ok := tv.keySource.(*httpKeySource) 45 | if !ok { 46 | t.Fatalf("tokenVerifier.keySource = %#v; want = httpKeySource", tv.keySource) 47 | } 48 | if ks.KeyURI != idTokenCertURL { 49 | t.Errorf("tokenVerifier.certURL = %q; want = %q", ks.KeyURI, idTokenCertURL) 50 | } 51 | } 52 | 53 | func TestHTTPKeySource(t *testing.T) { 54 | data, err := ioutil.ReadFile("../testdata/public_certs.json") 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | ks := newHTTPKeySource("http://mock.url", http.DefaultClient) 60 | if ks.HTTPClient == nil { 61 | t.Errorf("HTTPClient = nil; want = non-nil") 62 | } 63 | hc, rc := newTestHTTPClient(data) 64 | ks.HTTPClient = hc 65 | if err := verifyHTTPKeySource(ks, rc); err != nil { 66 | t.Fatal(err) 67 | } 68 | } 69 | 70 | func TestHTTPKeySourceWithClient(t *testing.T) { 71 | data, err := ioutil.ReadFile("../testdata/public_certs.json") 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | hc, rc := newTestHTTPClient(data) 77 | ks := newHTTPKeySource("http://mock.url", hc) 78 | if ks.HTTPClient != hc { 79 | t.Errorf("HTTPClient = %v; want = %v", ks.HTTPClient, hc) 80 | } 81 | if err := verifyHTTPKeySource(ks, rc); err != nil { 82 | t.Fatal(err) 83 | } 84 | } 85 | 86 | func TestHTTPKeySourceEmptyResponse(t *testing.T) { 87 | hc, _ := newTestHTTPClient([]byte("")) 88 | ks := newHTTPKeySource("http://mock.url", hc) 89 | if keys, err := ks.Keys(context.Background()); keys != nil || err == nil { 90 | t.Errorf("Keys() = (%v, %v); want = (nil, error)", keys, err) 91 | } 92 | } 93 | 94 | func TestHTTPKeySourceIncorrectResponse(t *testing.T) { 95 | hc, _ := newTestHTTPClient([]byte("{\"foo\": \"bar\"}")) 96 | ks := newHTTPKeySource("http://mock.url", hc) 97 | if keys, err := ks.Keys(context.Background()); keys != nil || err == nil { 98 | t.Errorf("Keys() = (%v, %v); want = (nil, error)", keys, err) 99 | } 100 | } 101 | 102 | func TestHTTPKeySourceHTTPError(t *testing.T) { 103 | rc := &mockReadCloser{ 104 | data: string(""), 105 | closeCount: 0, 106 | } 107 | client := &http.Client{ 108 | Transport: &mockHTTPResponse{ 109 | Response: http.Response{ 110 | Status: "503 Service Unavailable", 111 | StatusCode: http.StatusServiceUnavailable, 112 | Body: rc, 113 | }, 114 | Err: nil, 115 | }, 116 | } 117 | ks := newHTTPKeySource("http://mock.url", client) 118 | if keys, err := ks.Keys(context.Background()); keys != nil || err == nil { 119 | t.Errorf("Keys() = (%v, %v); want = (nil, error)", keys, err) 120 | } 121 | } 122 | 123 | func TestHTTPKeySourceTransportError(t *testing.T) { 124 | hc := &http.Client{ 125 | Transport: &mockHTTPResponse{ 126 | Err: errors.New("transport error"), 127 | }, 128 | } 129 | ks := newHTTPKeySource("http://mock.url", hc) 130 | if keys, err := ks.Keys(context.Background()); keys != nil || err == nil { 131 | t.Errorf("Keys() = (%v, %v); want = (nil, error)", keys, err) 132 | } 133 | } 134 | 135 | func TestFindMaxAge(t *testing.T) { 136 | cases := []struct { 137 | cc string 138 | want int64 139 | }{ 140 | {"max-age=100", 100}, 141 | {"public, max-age=100", 100}, 142 | {"public,max-age=100", 100}, 143 | {"public, max-age=100, must-revalidate, no-transform", 100}, 144 | {"", 0}, 145 | {"max-age 100", 0}, 146 | {"max-age: 100", 0}, 147 | {"max-age2=100", 0}, 148 | {"max-age=foo", 0}, 149 | {"private,", 0}, 150 | } 151 | for _, tc := range cases { 152 | resp := &http.Response{ 153 | Header: http.Header{"Cache-Control": {tc.cc}}, 154 | } 155 | age := findMaxAge(resp) 156 | if *age != (time.Duration(tc.want) * time.Second) { 157 | t.Errorf("findMaxAge(%q) = %v; want = %v", tc.cc, *age, tc.want) 158 | } 159 | } 160 | } 161 | 162 | func TestParsePublicKeys(t *testing.T) { 163 | b, err := ioutil.ReadFile("../testdata/public_certs.json") 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | keys, err := parsePublicKeys(b) 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | if len(keys) != 3 { 172 | t.Errorf("parsePublicKeys() = %d; want = %d", len(keys), 3) 173 | } 174 | } 175 | 176 | func TestParsePublicKeysError(t *testing.T) { 177 | cases := []string{ 178 | "", 179 | "not-json", 180 | } 181 | for _, tc := range cases { 182 | if keys, err := parsePublicKeys([]byte(tc)); keys != nil || err == nil { 183 | t.Errorf("parsePublicKeys(%q) = (%v, %v); want = (nil, err)", tc, keys, err) 184 | } 185 | } 186 | } 187 | 188 | type mockHTTPResponse struct { 189 | Response http.Response 190 | Err error 191 | } 192 | 193 | func (m *mockHTTPResponse) RoundTrip(*http.Request) (*http.Response, error) { 194 | return &m.Response, m.Err 195 | } 196 | 197 | type mockReadCloser struct { 198 | data string 199 | index int64 200 | closeCount int 201 | } 202 | 203 | func newTestHTTPClient(data []byte) (*http.Client, *mockReadCloser) { 204 | rc := &mockReadCloser{ 205 | data: string(data), 206 | closeCount: 0, 207 | } 208 | client := &http.Client{ 209 | Transport: &mockHTTPResponse{ 210 | Response: http.Response{ 211 | Status: "200 OK", 212 | StatusCode: http.StatusOK, 213 | Header: http.Header{ 214 | "Cache-Control": {"public, max-age=100"}, 215 | }, 216 | Body: rc, 217 | }, 218 | Err: nil, 219 | }, 220 | } 221 | return client, rc 222 | } 223 | 224 | func (r *mockReadCloser) Read(p []byte) (n int, err error) { 225 | if len(p) == 0 { 226 | return 0, nil 227 | } 228 | if r.index >= int64(len(r.data)) { 229 | return 0, io.EOF 230 | } 231 | n = copy(p, r.data[r.index:]) 232 | r.index += int64(n) 233 | return 234 | } 235 | 236 | func (r *mockReadCloser) Close() error { 237 | r.closeCount++ 238 | r.index = 0 239 | return nil 240 | } 241 | 242 | func verifyHTTPKeySource(ks *httpKeySource, rc *mockReadCloser) error { 243 | mc := &internal.MockClock{Timestamp: time.Unix(0, 0)} 244 | ks.Clock = mc 245 | 246 | exp := time.Unix(100, 0) 247 | for i := 0; i <= 100; i++ { 248 | keys, err := ks.Keys(context.Background()) 249 | if err != nil { 250 | return err 251 | } 252 | if len(keys) != 3 { 253 | return fmt.Errorf("Keys: %d; want: 3", len(keys)) 254 | } else if rc.closeCount != 1 { 255 | return fmt.Errorf("HTTP calls: %d; want: 1", rc.closeCount) 256 | } else if ks.ExpiryTime != exp { 257 | return fmt.Errorf("Expiry: %v; want: %v", ks.ExpiryTime, exp) 258 | } 259 | mc.Timestamp = mc.Timestamp.Add(time.Second) 260 | } 261 | 262 | mc.Timestamp = time.Unix(101, 0) 263 | keys, err := ks.Keys(context.Background()) 264 | if err != nil { 265 | return err 266 | } 267 | if len(keys) != 3 { 268 | return fmt.Errorf("Keys: %d; want: 3", len(keys)) 269 | } else if rc.closeCount != 2 { 270 | return fmt.Errorf("HTTP calls: %d; want: 2", rc.closeCount) 271 | } 272 | return nil 273 | } 274 | -------------------------------------------------------------------------------- /db/auth_override_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package db 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | ) 21 | 22 | func TestAuthOverrideGet(t *testing.T) { 23 | mock := &mockServer{Resp: "data"} 24 | srv := mock.Start(aoClient) 25 | defer srv.Close() 26 | 27 | ref := aoClient.NewRef("peter") 28 | var got string 29 | if err := ref.Get(context.Background(), &got); err != nil { 30 | t.Fatal(err) 31 | } 32 | if got != "data" { 33 | t.Errorf("Ref(AuthOverride).Get() = %q; want = %q", got, "data") 34 | } 35 | checkOnlyRequest(t, mock.Reqs, &testReq{ 36 | Method: "GET", 37 | Path: "/peter.json", 38 | Query: map[string]string{"auth_variable_override": testAuthOverrides}, 39 | }) 40 | } 41 | 42 | func TestAuthOverrideSet(t *testing.T) { 43 | mock := &mockServer{} 44 | srv := mock.Start(aoClient) 45 | defer srv.Close() 46 | 47 | ref := aoClient.NewRef("peter") 48 | want := map[string]interface{}{"name": "Peter Parker", "age": float64(17)} 49 | if err := ref.Set(context.Background(), want); err != nil { 50 | t.Fatal(err) 51 | } 52 | checkOnlyRequest(t, mock.Reqs, &testReq{ 53 | Method: "PUT", 54 | Body: serialize(want), 55 | Path: "/peter.json", 56 | Query: map[string]string{"auth_variable_override": testAuthOverrides, "print": "silent"}, 57 | }) 58 | } 59 | 60 | func TestAuthOverrideQuery(t *testing.T) { 61 | mock := &mockServer{Resp: "data"} 62 | srv := mock.Start(aoClient) 63 | defer srv.Close() 64 | 65 | ref := aoClient.NewRef("peter") 66 | var got string 67 | if err := ref.OrderByChild("foo").Get(context.Background(), &got); err != nil { 68 | t.Fatal(err) 69 | } 70 | if got != "data" { 71 | t.Errorf("Ref(AuthOverride).OrderByChild() = %q; want = %q", got, "data") 72 | } 73 | checkOnlyRequest(t, mock.Reqs, &testReq{ 74 | Method: "GET", 75 | Path: "/peter.json", 76 | Query: map[string]string{ 77 | "auth_variable_override": testAuthOverrides, 78 | "orderBy": "\"foo\"", 79 | }, 80 | }) 81 | } 82 | 83 | func TestAuthOverrideRangeQuery(t *testing.T) { 84 | mock := &mockServer{Resp: "data"} 85 | srv := mock.Start(aoClient) 86 | defer srv.Close() 87 | 88 | ref := aoClient.NewRef("peter") 89 | var got string 90 | if err := ref.OrderByChild("foo").StartAt(1).EndAt(10).Get(context.Background(), &got); err != nil { 91 | t.Fatal(err) 92 | } 93 | if got != "data" { 94 | t.Errorf("Ref(AuthOverride).OrderByChild() = %q; want = %q", got, "data") 95 | } 96 | checkOnlyRequest(t, mock.Reqs, &testReq{ 97 | Method: "GET", 98 | Path: "/peter.json", 99 | Query: map[string]string{ 100 | "auth_variable_override": testAuthOverrides, 101 | "orderBy": "\"foo\"", 102 | "startAt": "1", 103 | "endAt": "10", 104 | }, 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db contains functions for accessing the Firebase Realtime Database. 16 | package db 17 | 18 | import ( 19 | "context" 20 | "encoding/json" 21 | "errors" 22 | "fmt" 23 | "net/url" 24 | "os" 25 | "runtime" 26 | "strings" 27 | 28 | "firebase.google.com/go/v4/internal" 29 | "golang.org/x/oauth2" 30 | "google.golang.org/api/option" 31 | ) 32 | 33 | const userAgentFormat = "Firebase/HTTP/%s/%s/AdminGo" 34 | const invalidChars = "[].#$" 35 | const authVarOverride = "auth_variable_override" 36 | const emulatorDatabaseEnvVar = "FIREBASE_DATABASE_EMULATOR_HOST" 37 | const emulatorNamespaceParam = "ns" 38 | 39 | // errInvalidURL tells whether the given database url is invalid 40 | // It is invalid if it is malformed, or not of the format "host:port" 41 | var errInvalidURL = errors.New("invalid database url") 42 | 43 | var emulatorToken = &oauth2.Token{ 44 | AccessToken: "owner", 45 | } 46 | 47 | // Client is the interface for the Firebase Realtime Database service. 48 | type Client struct { 49 | hc *internal.HTTPClient 50 | dbURLConfig *dbURLConfig 51 | authOverride string 52 | } 53 | 54 | type dbURLConfig struct { 55 | // BaseURL can be either: 56 | // - a production url (https://foo-bar.firebaseio.com/) 57 | // - an emulator url (http://localhost:9000) 58 | BaseURL string 59 | 60 | // Namespace is used in for the emulator to specify the databaseName 61 | // To specify a namespace on your url, pass ns= (localhost:9000/?ns=foo-bar) 62 | Namespace string 63 | } 64 | 65 | // NewClient creates a new instance of the Firebase Database Client. 66 | // 67 | // This function can only be invoked from within the SDK. Client applications should access the 68 | // Database service through firebase.App. 69 | func NewClient(ctx context.Context, c *internal.DatabaseConfig) (*Client, error) { 70 | urlConfig, isEmulator, err := parseURLConfig(c.URL) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | var ao []byte 76 | if c.AuthOverride == nil || len(c.AuthOverride) > 0 { 77 | ao, err = json.Marshal(c.AuthOverride) 78 | if err != nil { 79 | return nil, err 80 | } 81 | } 82 | 83 | opts := append([]option.ClientOption{}, c.Opts...) 84 | if isEmulator { 85 | ts := oauth2.StaticTokenSource(emulatorToken) 86 | opts = append(opts, option.WithTokenSource(ts)) 87 | } 88 | ua := fmt.Sprintf(userAgentFormat, c.Version, runtime.Version()) 89 | opts = append(opts, option.WithUserAgent(ua)) 90 | hc, _, err := internal.NewHTTPClient(ctx, opts...) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | hc.CreateErrFn = handleRTDBError 96 | return &Client{ 97 | hc: hc, 98 | dbURLConfig: urlConfig, 99 | authOverride: string(ao), 100 | }, nil 101 | } 102 | 103 | // NewRef returns a new database reference representing the node at the specified path. 104 | func (c *Client) NewRef(path string) *Ref { 105 | segs := parsePath(path) 106 | key := "" 107 | if len(segs) > 0 { 108 | key = segs[len(segs)-1] 109 | } 110 | 111 | return &Ref{ 112 | Key: key, 113 | Path: "/" + strings.Join(segs, "/"), 114 | client: c, 115 | segs: segs, 116 | } 117 | } 118 | 119 | func (c *Client) sendAndUnmarshal( 120 | ctx context.Context, req *internal.Request, v interface{}) (*internal.Response, error) { 121 | if strings.ContainsAny(req.URL, invalidChars) { 122 | return nil, fmt.Errorf("invalid path with illegal characters: %q", req.URL) 123 | } 124 | 125 | req.URL = fmt.Sprintf("%s%s.json", c.dbURLConfig.BaseURL, req.URL) 126 | if c.authOverride != "" { 127 | req.Opts = append(req.Opts, internal.WithQueryParam(authVarOverride, c.authOverride)) 128 | } 129 | if c.dbURLConfig.Namespace != "" { 130 | req.Opts = append(req.Opts, internal.WithQueryParam(emulatorNamespaceParam, c.dbURLConfig.Namespace)) 131 | } 132 | 133 | return c.hc.DoAndUnmarshal(ctx, req, v) 134 | } 135 | 136 | func parsePath(path string) []string { 137 | var segs []string 138 | for _, s := range strings.Split(path, "/") { 139 | if s != "" { 140 | segs = append(segs, s) 141 | } 142 | } 143 | return segs 144 | } 145 | 146 | func handleRTDBError(resp *internal.Response) error { 147 | err := internal.NewFirebaseError(resp) 148 | var p struct { 149 | Error string `json:"error"` 150 | } 151 | json.Unmarshal(resp.Body, &p) 152 | if p.Error != "" { 153 | err.String = fmt.Sprintf("http error status: %d; reason: %s", resp.Status, p.Error) 154 | } 155 | 156 | return err 157 | } 158 | 159 | // parseURLConfig returns the dbURLConfig for the database 160 | // dbURL may be either: 161 | // - a production url (https://foo-bar.firebaseio.com/) 162 | // - an emulator URL (localhost:9000/?ns=foo-bar) 163 | // 164 | // The following rules will apply for determining the output: 165 | // - If the url does not use an https scheme it will be assumed to be an emulator url and be used. 166 | // - else If the FIREBASE_DATABASE_EMULATOR_HOST environment variable is set it will be used. 167 | // - else the url will be assumed to be a production url and be used. 168 | func parseURLConfig(dbURL string) (*dbURLConfig, bool, error) { 169 | parsedURL, err := url.ParseRequestURI(dbURL) 170 | if err == nil && parsedURL.Scheme != "https" { 171 | cfg, err := parseEmulatorHost(dbURL, parsedURL) 172 | return cfg, true, err 173 | } 174 | 175 | environmentEmulatorURL := os.Getenv(emulatorDatabaseEnvVar) 176 | if environmentEmulatorURL != "" { 177 | parsedURL, err = url.ParseRequestURI(environmentEmulatorURL) 178 | if err != nil { 179 | return nil, false, fmt.Errorf("%s: %w", environmentEmulatorURL, errInvalidURL) 180 | } 181 | cfg, err := parseEmulatorHost(environmentEmulatorURL, parsedURL) 182 | return cfg, true, err 183 | } 184 | 185 | if err != nil { 186 | return nil, false, fmt.Errorf("%s: %w", dbURL, errInvalidURL) 187 | } 188 | 189 | return &dbURLConfig{ 190 | BaseURL: dbURL, 191 | Namespace: "", 192 | }, false, nil 193 | } 194 | 195 | func parseEmulatorHost(rawEmulatorHostURL string, parsedEmulatorHost *url.URL) (*dbURLConfig, error) { 196 | if strings.Contains(rawEmulatorHostURL, "//") { 197 | return nil, fmt.Errorf(`invalid %s: "%s". It must follow format "host:port": %w`, emulatorDatabaseEnvVar, rawEmulatorHostURL, errInvalidURL) 198 | } 199 | 200 | baseURL := strings.Replace(rawEmulatorHostURL, fmt.Sprintf("?%s", parsedEmulatorHost.RawQuery), "", -1) 201 | if parsedEmulatorHost.Scheme != "http" { 202 | baseURL = fmt.Sprintf("http://%s", baseURL) 203 | } 204 | 205 | namespace := parsedEmulatorHost.Query().Get(emulatorNamespaceParam) 206 | if namespace == "" { 207 | if strings.Contains(rawEmulatorHostURL, ".") { 208 | namespace = strings.Split(rawEmulatorHostURL, ".")[0] 209 | } 210 | if namespace == "" { 211 | return nil, fmt.Errorf(`invalid database URL: "%s". Database URL must be a valid URL to a Firebase Realtime Database instance (include ?ns= query param)`, parsedEmulatorHost) 212 | } 213 | } 214 | 215 | return &dbURLConfig{ 216 | BaseURL: baseURL, 217 | Namespace: namespace, 218 | }, nil 219 | } 220 | -------------------------------------------------------------------------------- /errorutils/errorutils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package errorutils provides functions for checking and handling error conditions. 16 | package errorutils 17 | 18 | import ( 19 | "net/http" 20 | 21 | "firebase.google.com/go/v4/internal" 22 | ) 23 | 24 | // IsInvalidArgument checks if the given error was due to an invalid client argument. 25 | func IsInvalidArgument(err error) bool { 26 | return internal.HasPlatformErrorCode(err, internal.InvalidArgument) 27 | } 28 | 29 | // IsFailedPrecondition checks if the given error was because a request could not be executed 30 | // in the current system state, such as deleting a non-empty directory. 31 | func IsFailedPrecondition(err error) bool { 32 | return internal.HasPlatformErrorCode(err, internal.FailedPrecondition) 33 | } 34 | 35 | // IsOutOfRange checks if the given error due to an invalid range specified by the client. 36 | func IsOutOfRange(err error) bool { 37 | return internal.HasPlatformErrorCode(err, internal.OutOfRange) 38 | } 39 | 40 | // IsUnauthenticated checks if the given error was caused by an unauthenticated request. 41 | // 42 | // Unauthenticated requests are due to missing, invalid, or expired OAuth token. 43 | func IsUnauthenticated(err error) bool { 44 | return internal.HasPlatformErrorCode(err, internal.Unauthenticated) 45 | } 46 | 47 | // IsPermissionDenied checks if the given error was due to a client not having suffificient 48 | // permissions. 49 | // 50 | // This can happen because the OAuth token does not have the right scopes, the client doesn't have 51 | // permission, or the API has not been enabled for the client project. 52 | func IsPermissionDenied(err error) bool { 53 | return internal.HasPlatformErrorCode(err, internal.PermissionDenied) 54 | } 55 | 56 | // IsNotFound checks if the given error was due to a specified resource being not found. 57 | // 58 | // This may also occur when the request is rejected by undisclosed reasons, such as whitelisting. 59 | func IsNotFound(err error) bool { 60 | return internal.HasPlatformErrorCode(err, internal.NotFound) 61 | } 62 | 63 | // IsConflict checks if the given error was due to a concurrency conflict, such as a 64 | // read-modify-write conflict. 65 | // 66 | // This represents an HTTP 409 Conflict status code, without additional information to distinguish 67 | // between ABORTED or ALREADY_EXISTS error conditions. 68 | func IsConflict(err error) bool { 69 | return internal.HasPlatformErrorCode(err, internal.Conflict) 70 | } 71 | 72 | // IsAborted checks if the given error was due to a concurrency conflict, such as a 73 | // read-modify-write conflict. 74 | func IsAborted(err error) bool { 75 | return internal.HasPlatformErrorCode(err, internal.Aborted) 76 | } 77 | 78 | // IsAlreadyExists checks if the given error was because a resource that a client tried to create 79 | // already exists. 80 | func IsAlreadyExists(err error) bool { 81 | return internal.HasPlatformErrorCode(err, internal.AlreadyExists) 82 | } 83 | 84 | // IsResourceExhausted checks if the given error was caused by either running out of a quota or 85 | // reaching a rate limit. 86 | func IsResourceExhausted(err error) bool { 87 | return internal.HasPlatformErrorCode(err, internal.ResourceExhausted) 88 | } 89 | 90 | // IsCancelled checks if the given error was due to the client cancelling a request. 91 | func IsCancelled(err error) bool { 92 | return internal.HasPlatformErrorCode(err, internal.Cancelled) 93 | } 94 | 95 | // IsDataLoss checks if the given error was due to an unrecoverable data loss or corruption. 96 | // 97 | // The client should report such errors to the end user. 98 | func IsDataLoss(err error) bool { 99 | return internal.HasPlatformErrorCode(err, internal.DataLoss) 100 | } 101 | 102 | // IsUnknown checks if the given error was cuased by an unknown server error. 103 | // 104 | // This typically indicates a server bug. 105 | func IsUnknown(err error) bool { 106 | return internal.HasPlatformErrorCode(err, internal.Unknown) 107 | } 108 | 109 | // IsInternal checks if the given error was due to an internal server error. 110 | // 111 | // This typically indicates a server bug. 112 | func IsInternal(err error) bool { 113 | return internal.HasPlatformErrorCode(err, internal.Internal) 114 | } 115 | 116 | // IsUnavailable checks if the given error was caused by an unavailable service. 117 | // 118 | // This typically indicates that the target service is temporarily down. 119 | func IsUnavailable(err error) bool { 120 | return internal.HasPlatformErrorCode(err, internal.Unavailable) 121 | } 122 | 123 | // IsDeadlineExceeded checks if the given error was due a request exceeding a deadline. 124 | // 125 | // This will happen only if the caller sets a deadline that is shorter than the method's default 126 | // deadline (i.e. requested deadline is not enough for the server to process the request) and the 127 | // request did not finish within the deadline. 128 | func IsDeadlineExceeded(err error) bool { 129 | return internal.HasPlatformErrorCode(err, internal.DeadlineExceeded) 130 | } 131 | 132 | // HTTPResponse returns the http.Response instance that caused the given error. 133 | // 134 | // If the error was not caused by an HTTP error response, returns nil. 135 | // 136 | // Returns a buffered copy of the original response received from the network stack. It is safe to 137 | // read the response content from the returned http.Response. 138 | func HTTPResponse(err error) *http.Response { 139 | fe, ok := err.(*internal.FirebaseError) 140 | if ok { 141 | return fe.Response 142 | } 143 | 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /firebase.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package firebase is the entry point to the Firebase Admin SDK. It provides functionality for initializing App 16 | // instances, which serve as the central entities that provide access to various other Firebase services exposed 17 | // from the SDK. 18 | package firebase 19 | 20 | import ( 21 | "context" 22 | "encoding/json" 23 | "errors" 24 | "io/ioutil" 25 | "os" 26 | 27 | "cloud.google.com/go/firestore" 28 | "firebase.google.com/go/v4/appcheck" 29 | "firebase.google.com/go/v4/auth" 30 | "firebase.google.com/go/v4/db" 31 | "firebase.google.com/go/v4/iid" 32 | "firebase.google.com/go/v4/internal" 33 | "firebase.google.com/go/v4/messaging" 34 | "firebase.google.com/go/v4/remoteconfig" 35 | "firebase.google.com/go/v4/storage" 36 | "google.golang.org/api/option" 37 | "google.golang.org/api/transport" 38 | ) 39 | 40 | var defaultAuthOverrides = make(map[string]interface{}) 41 | 42 | // Version of the Firebase Go Admin SDK. 43 | const Version = "4.17.0" 44 | 45 | // firebaseEnvName is the name of the environment variable with the Config. 46 | const firebaseEnvName = "FIREBASE_CONFIG" 47 | 48 | // An App holds configuration and state common to all Firebase services that are exposed from the SDK. 49 | type App struct { 50 | authOverride map[string]interface{} 51 | dbURL string 52 | projectID string 53 | serviceAccountID string 54 | storageBucket string 55 | opts []option.ClientOption 56 | } 57 | 58 | // Config represents the configuration used to initialize an App. 59 | type Config struct { 60 | AuthOverride *map[string]interface{} `json:"databaseAuthVariableOverride"` 61 | DatabaseURL string `json:"databaseURL"` 62 | ProjectID string `json:"projectId"` 63 | ServiceAccountID string `json:"serviceAccountId"` 64 | StorageBucket string `json:"storageBucket"` 65 | } 66 | 67 | // Auth returns an instance of auth.Client. 68 | func (a *App) Auth(ctx context.Context) (*auth.Client, error) { 69 | conf := &internal.AuthConfig{ 70 | ProjectID: a.projectID, 71 | Opts: a.opts, 72 | ServiceAccountID: a.serviceAccountID, 73 | Version: Version, 74 | } 75 | return auth.NewClient(ctx, conf) 76 | } 77 | 78 | // Database returns an instance of db.Client to interact with the default Firebase Database 79 | // configured via Config.DatabaseURL. 80 | func (a *App) Database(ctx context.Context) (*db.Client, error) { 81 | return a.DatabaseWithURL(ctx, a.dbURL) 82 | } 83 | 84 | // DatabaseWithURL returns an instance of db.Client to interact with the Firebase Database 85 | // identified by the given URL. 86 | func (a *App) DatabaseWithURL(ctx context.Context, url string) (*db.Client, error) { 87 | conf := &internal.DatabaseConfig{ 88 | AuthOverride: a.authOverride, 89 | URL: url, 90 | Opts: a.opts, 91 | Version: Version, 92 | } 93 | return db.NewClient(ctx, conf) 94 | } 95 | 96 | // Storage returns a new instance of storage.Client. 97 | func (a *App) Storage(ctx context.Context) (*storage.Client, error) { 98 | conf := &internal.StorageConfig{ 99 | Opts: a.opts, 100 | Bucket: a.storageBucket, 101 | } 102 | return storage.NewClient(ctx, conf) 103 | } 104 | 105 | // Firestore returns a new firestore.Client instance from the https://godoc.org/cloud.google.com/go/firestore 106 | // package. 107 | func (a *App) Firestore(ctx context.Context) (*firestore.Client, error) { 108 | if a.projectID == "" { 109 | return nil, errors.New("project id is required to access Firestore") 110 | } 111 | return firestore.NewClient(ctx, a.projectID, a.opts...) 112 | } 113 | 114 | // InstanceID returns an instance of iid.Client. 115 | func (a *App) InstanceID(ctx context.Context) (*iid.Client, error) { 116 | conf := &internal.InstanceIDConfig{ 117 | ProjectID: a.projectID, 118 | Opts: a.opts, 119 | Version: Version, 120 | } 121 | return iid.NewClient(ctx, conf) 122 | } 123 | 124 | // Messaging returns an instance of messaging.Client. 125 | func (a *App) Messaging(ctx context.Context) (*messaging.Client, error) { 126 | conf := &internal.MessagingConfig{ 127 | ProjectID: a.projectID, 128 | Opts: a.opts, 129 | Version: Version, 130 | } 131 | return messaging.NewClient(ctx, conf) 132 | } 133 | 134 | // AppCheck returns an instance of appcheck.Client. 135 | func (a *App) AppCheck(ctx context.Context) (*appcheck.Client, error) { 136 | conf := &internal.AppCheckConfig{ 137 | ProjectID: a.projectID, 138 | } 139 | return appcheck.NewClient(ctx, conf) 140 | } 141 | 142 | // RemoteConfig returns an instance of remoteconfig.Client. 143 | func (a *App) RemoteConfig(ctx context.Context) (*remoteconfig.Client, error) { 144 | conf := &internal.RemoteConfigClientConfig{ 145 | ProjectID: a.projectID, 146 | Opts: a.opts, 147 | Version: Version, 148 | } 149 | return remoteconfig.NewClient(ctx, conf) 150 | } 151 | 152 | // NewApp creates a new App from the provided config and client options. 153 | // 154 | // If the client options contain a valid credential (a service account file, a refresh token 155 | // file or an oauth2.TokenSource) the App will be authenticated using that credential. Otherwise, 156 | // NewApp attempts to authenticate the App with Google application default credentials. 157 | // If `config` is nil, the SDK will attempt to load the config options from the 158 | // `FIREBASE_CONFIG` environment variable. If the value in it starts with a `{` it is parsed as a 159 | // JSON object, otherwise it is assumed to be the name of the JSON file containing the options. 160 | func NewApp(ctx context.Context, config *Config, opts ...option.ClientOption) (*App, error) { 161 | o := []option.ClientOption{option.WithScopes(internal.FirebaseScopes...)} 162 | o = append(o, opts...) 163 | if config == nil { 164 | var err error 165 | if config, err = getConfigDefaults(); err != nil { 166 | return nil, err 167 | } 168 | } 169 | 170 | pid := getProjectID(ctx, config, o...) 171 | ao := defaultAuthOverrides 172 | if config.AuthOverride != nil { 173 | ao = *config.AuthOverride 174 | } 175 | 176 | return &App{ 177 | authOverride: ao, 178 | dbURL: config.DatabaseURL, 179 | projectID: pid, 180 | serviceAccountID: config.ServiceAccountID, 181 | storageBucket: config.StorageBucket, 182 | opts: o, 183 | }, nil 184 | } 185 | 186 | // getConfigDefaults reads the default config file, defined by the FIREBASE_CONFIG 187 | // env variable, used only when options are nil. 188 | func getConfigDefaults() (*Config, error) { 189 | fbc := &Config{} 190 | confFileName := os.Getenv(firebaseEnvName) 191 | if confFileName == "" { 192 | return fbc, nil 193 | } 194 | var dat []byte 195 | if confFileName[0] == byte('{') { 196 | dat = []byte(confFileName) 197 | } else { 198 | var err error 199 | if dat, err = ioutil.ReadFile(confFileName); err != nil { 200 | return nil, err 201 | } 202 | } 203 | if err := json.Unmarshal(dat, fbc); err != nil { 204 | return nil, err 205 | } 206 | 207 | // Some special handling necessary for db auth overrides 208 | var m map[string]interface{} 209 | if err := json.Unmarshal(dat, &m); err != nil { 210 | return nil, err 211 | } 212 | if ao, ok := m["databaseAuthVariableOverride"]; ok && ao == nil { 213 | // Auth overrides are explicitly set to null 214 | var nullMap map[string]interface{} 215 | fbc.AuthOverride = &nullMap 216 | } 217 | return fbc, nil 218 | } 219 | 220 | func getProjectID(ctx context.Context, config *Config, opts ...option.ClientOption) string { 221 | if config.ProjectID != "" { 222 | return config.ProjectID 223 | } 224 | 225 | creds, _ := transport.Creds(ctx, opts...) 226 | if creds != nil && creds.ProjectID != "" { 227 | return creds.ProjectID 228 | } 229 | 230 | if pid := os.Getenv("GOOGLE_CLOUD_PROJECT"); pid != "" { 231 | return pid 232 | } 233 | 234 | return os.Getenv("GCLOUD_PROJECT") 235 | } 236 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module firebase.google.com/go/v4 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | cloud.google.com/go/firestore v1.18.0 7 | cloud.google.com/go/storage v1.53.0 8 | github.com/MicahParks/keyfunc v1.9.0 9 | github.com/golang-jwt/jwt/v4 v4.5.2 10 | github.com/google/go-cmp v0.7.0 11 | golang.org/x/oauth2 v0.30.0 12 | google.golang.org/api v0.231.0 13 | google.golang.org/appengine/v2 v2.0.6 14 | ) 15 | 16 | require ( 17 | cel.dev/expr v0.23.1 // indirect 18 | cloud.google.com/go v0.121.0 // indirect 19 | cloud.google.com/go/auth v0.16.1 // indirect 20 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 21 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 22 | cloud.google.com/go/iam v1.5.2 // indirect 23 | cloud.google.com/go/longrunning v0.6.7 // indirect 24 | cloud.google.com/go/monitoring v1.24.2 // indirect 25 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect 26 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect 27 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect 28 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 29 | github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect 30 | github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect 31 | github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect 32 | github.com/felixge/httpsnoop v1.0.4 // indirect 33 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 34 | github.com/go-logr/logr v1.4.2 // indirect 35 | github.com/go-logr/stdr v1.2.2 // indirect 36 | github.com/golang/protobuf v1.5.4 // indirect 37 | github.com/google/s2a-go v0.1.9 // indirect 38 | github.com/google/uuid v1.6.0 // indirect 39 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect 40 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect 41 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 42 | github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect 43 | github.com/zeebo/errs v1.4.0 // indirect 44 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 45 | go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect 46 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect 47 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 48 | go.opentelemetry.io/otel v1.35.0 // indirect 49 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 50 | go.opentelemetry.io/otel/sdk v1.35.0 // indirect 51 | go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect 52 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 53 | golang.org/x/crypto v0.38.0 // indirect 54 | golang.org/x/net v0.40.0 // indirect 55 | golang.org/x/sync v0.14.0 // indirect 56 | golang.org/x/sys v0.33.0 // indirect 57 | golang.org/x/text v0.25.0 // indirect 58 | golang.org/x/time v0.11.0 // indirect 59 | google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect 60 | google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect 61 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect 62 | google.golang.org/grpc v1.72.0 // indirect 63 | google.golang.org/protobuf v1.36.6 // indirect 64 | ) 65 | -------------------------------------------------------------------------------- /iid/iid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package iid contains functions for deleting instance IDs from Firebase projects. 16 | package iid 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "fmt" 22 | "net/http" 23 | "strings" 24 | 25 | "firebase.google.com/go/v4/errorutils" 26 | "firebase.google.com/go/v4/internal" 27 | ) 28 | 29 | const iidEndpoint = "https://console.firebase.google.com/v1" 30 | 31 | var errorMessages = map[int]string{ 32 | http.StatusBadRequest: "malformed instance id argument", 33 | http.StatusUnauthorized: "request not authorized", 34 | http.StatusForbidden: "project does not match instance ID or the client does not have sufficient privileges", 35 | http.StatusNotFound: "failed to find the instance id", 36 | http.StatusConflict: "already deleted", 37 | http.StatusTooManyRequests: "request throttled out by the backend server", 38 | http.StatusInternalServerError: "internal server error", 39 | http.StatusServiceUnavailable: "backend servers are over capacity", 40 | } 41 | 42 | // IsInvalidArgument checks if the given error was due to an invalid instance ID argument. 43 | // 44 | // Deprecated: Use errorutils.IsInvalidArgument() function instead. 45 | func IsInvalidArgument(err error) bool { 46 | return errorutils.IsInvalidArgument(err) 47 | } 48 | 49 | // IsInsufficientPermission checks if the given error was due to the request not having the 50 | // required authorization. This could be due to the client not having the required permission 51 | // or the specified instance ID not matching the target Firebase project. 52 | // 53 | // Deprecated: Use errorutils.IsUnauthenticated() or errorutils.IsPermissionDenied() instead. 54 | func IsInsufficientPermission(err error) bool { 55 | return errorutils.IsUnauthenticated(err) || errorutils.IsPermissionDenied(err) 56 | } 57 | 58 | // IsNotFound checks if the given error was due to a non existing instance ID. 59 | func IsNotFound(err error) bool { 60 | return errorutils.IsNotFound(err) 61 | } 62 | 63 | // IsAlreadyDeleted checks if the given error was due to the instance ID being already deleted from 64 | // the project. 65 | // 66 | // Deprecated: Use errorutils.IsConflict() function instead. 67 | func IsAlreadyDeleted(err error) bool { 68 | return errorutils.IsConflict(err) 69 | } 70 | 71 | // IsTooManyRequests checks if the given error was due to the client sending too many requests 72 | // causing a server quota to exceed. 73 | // 74 | // Deprecated: Use errorutils.IsResourceExhausted() function instead. 75 | func IsTooManyRequests(err error) bool { 76 | return errorutils.IsResourceExhausted(err) 77 | } 78 | 79 | // IsInternal checks if the given error was due to an internal server error. 80 | // 81 | // Deprecated: Use errorutils.IsInternal() function instead. 82 | func IsInternal(err error) bool { 83 | return errorutils.IsInternal(err) 84 | } 85 | 86 | // IsServerUnavailable checks if the given error was due to the backend server being temporarily 87 | // unavailable. 88 | // 89 | // Deprecated: Use errorutils.IsUnavailable() function instead. 90 | func IsServerUnavailable(err error) bool { 91 | return errorutils.IsUnavailable(err) 92 | } 93 | 94 | // IsUnknown checks if the given error was due to unknown error returned by the backend server. 95 | // 96 | // Deprecated: Use errorutils.IsUnknown() function instead. 97 | func IsUnknown(err error) bool { 98 | return errorutils.IsUnknown(err) 99 | } 100 | 101 | // Client is the interface for the Firebase Instance ID service. 102 | type Client struct { 103 | // To enable testing against arbitrary endpoints. 104 | endpoint string 105 | client *internal.HTTPClient 106 | project string 107 | } 108 | 109 | // NewClient creates a new instance of the Firebase instance ID Client. 110 | // 111 | // This function can only be invoked from within the SDK. Client applications should access the 112 | // the instance ID service through firebase.App. 113 | func NewClient(ctx context.Context, c *internal.InstanceIDConfig) (*Client, error) { 114 | if c.ProjectID == "" { 115 | return nil, errors.New("project id is required to access instance id client") 116 | } 117 | 118 | hc, _, err := internal.NewHTTPClient(ctx, c.Opts...) 119 | if err != nil { 120 | return nil, err 121 | } 122 | hc.Opts = []internal.HTTPOption{ 123 | internal.WithHeader("x-goog-api-client", internal.GetMetricsHeader(c.Version)), 124 | } 125 | 126 | hc.CreateErrFn = createError 127 | return &Client{ 128 | endpoint: iidEndpoint, 129 | client: hc, 130 | project: c.ProjectID, 131 | }, nil 132 | } 133 | 134 | // DeleteInstanceID deletes the specified instance ID and the associated data from Firebase.. 135 | // 136 | // Note that Google Analytics for Firebase uses its own form of Instance ID to keep track of 137 | // analytics data. Therefore deleting a regular instance ID does not delete Analytics data. 138 | // See https://firebase.google.com/support/privacy/manage-iids#delete_an_instance_id for 139 | // more information. 140 | func (c *Client) DeleteInstanceID(ctx context.Context, iid string) error { 141 | if iid == "" { 142 | return errors.New("instance id must not be empty") 143 | } 144 | 145 | url := fmt.Sprintf("%s/project/%s/instanceId/%s", c.endpoint, c.project, iid) 146 | _, err := c.client.Do(ctx, &internal.Request{Method: http.MethodDelete, URL: url}) 147 | return err 148 | } 149 | 150 | func createError(resp *internal.Response) error { 151 | err := internal.NewFirebaseError(resp) 152 | if msg, ok := errorMessages[resp.Status]; ok { 153 | requestPath := resp.LowLevelResponse().Request.URL.Path 154 | idx := strings.LastIndex(requestPath, "/") 155 | err.String = fmt.Sprintf("instance id %q: %s", requestPath[idx+1:], msg) 156 | } 157 | 158 | return err 159 | } 160 | -------------------------------------------------------------------------------- /integration/auth/project_config_mgt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package auth 15 | 16 | import ( 17 | "context" 18 | "reflect" 19 | "testing" 20 | 21 | "firebase.google.com/go/v4/auth" 22 | ) 23 | 24 | func TestProjectConfig(t *testing.T) { 25 | mfaObject := &auth.MultiFactorConfig{ 26 | ProviderConfigs: []*auth.ProviderConfig{ 27 | { 28 | State: auth.Enabled, 29 | TOTPProviderConfig: &auth.TOTPProviderConfig{ 30 | AdjacentIntervals: 5, 31 | }, 32 | }, 33 | }, 34 | } 35 | want := &auth.ProjectConfig{ 36 | MultiFactorConfig: mfaObject, 37 | } 38 | t.Run("UpdateProjectConfig()", func(t *testing.T) { 39 | mfaConfigReq := *want.MultiFactorConfig 40 | req := (&auth.ProjectConfigToUpdate{}). 41 | MultiFactorConfig(mfaConfigReq) 42 | projectConfig, err := client.UpdateProjectConfig(context.Background(), req) 43 | if err != nil { 44 | t.Fatalf("UpdateProjectConfig() = %v", err) 45 | } 46 | if !reflect.DeepEqual(projectConfig, want) { 47 | t.Errorf("UpdateProjectConfig() = %#v; want = %#v", projectConfig, want) 48 | } 49 | }) 50 | 51 | t.Run("GetProjectConfig()", func(t *testing.T) { 52 | project, err := client.GetProjectConfig(context.Background()) 53 | if err != nil { 54 | t.Fatalf("GetProjectConfig() = %v", err) 55 | } 56 | 57 | if !reflect.DeepEqual(project, want) { 58 | t.Errorf("GetProjectConfig() = %v; want = %#v", project, want) 59 | } 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /integration/db/query_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package db 16 | 17 | import ( 18 | "context" 19 | "reflect" 20 | "testing" 21 | 22 | "firebase.google.com/go/v4/db" 23 | ) 24 | 25 | var heightSorted = []string{ 26 | "linhenykus", "pterodactyl", "lambeosaurus", 27 | "triceratops", "stegosaurus", "bruhathkayosaurus", 28 | } 29 | 30 | func TestLimitToFirst(t *testing.T) { 31 | for _, tc := range []int{2, 10} { 32 | results, err := dinos.OrderByChild("height").LimitToFirst(tc).GetOrdered(context.Background()) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | wl := min(tc, len(heightSorted)) 38 | want := heightSorted[:wl] 39 | if len(results) != wl { 40 | t.Errorf("LimitToFirst() = %d; want = %d", len(results), wl) 41 | } 42 | got := getNames(results) 43 | if !reflect.DeepEqual(got, want) { 44 | t.Errorf("LimitToLast() = %v; want = %v", got, want) 45 | } 46 | compareValues(t, results) 47 | } 48 | } 49 | 50 | func TestLimitToLast(t *testing.T) { 51 | for _, tc := range []int{2, 10} { 52 | results, err := dinos.OrderByChild("height").LimitToLast(tc).GetOrdered(context.Background()) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | wl := min(tc, len(heightSorted)) 58 | want := heightSorted[len(heightSorted)-wl:] 59 | if len(results) != wl { 60 | t.Errorf("LimitToLast() = %d; want = %d", len(results), wl) 61 | } 62 | got := getNames(results) 63 | if !reflect.DeepEqual(got, want) { 64 | t.Errorf("LimitToLast() = %v; want = %v", got, want) 65 | } 66 | compareValues(t, results) 67 | } 68 | } 69 | 70 | func TestStartAt(t *testing.T) { 71 | results, err := dinos.OrderByChild("height").StartAt(3.5).GetOrdered(context.Background()) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | want := heightSorted[len(heightSorted)-2:] 77 | if len(results) != len(want) { 78 | t.Errorf("StartAt() = %d; want = %d", len(results), len(want)) 79 | } 80 | got := getNames(results) 81 | if !reflect.DeepEqual(got, want) { 82 | t.Errorf("LimitToLast() = %v; want = %v", got, want) 83 | } 84 | compareValues(t, results) 85 | } 86 | 87 | func TestEndAt(t *testing.T) { 88 | results, err := dinos.OrderByChild("height").EndAt(3.5).GetOrdered(context.Background()) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | 93 | want := heightSorted[:4] 94 | if len(results) != len(want) { 95 | t.Errorf("StartAt() = %d; want = %d", len(results), len(want)) 96 | } 97 | got := getNames(results) 98 | if !reflect.DeepEqual(got, want) { 99 | t.Errorf("LimitToLast() = %v; want = %v", got, want) 100 | } 101 | compareValues(t, results) 102 | } 103 | 104 | func TestStartAndEndAt(t *testing.T) { 105 | results, err := dinos.OrderByChild("height").StartAt(2.5).EndAt(5).GetOrdered(context.Background()) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | 110 | want := heightSorted[len(heightSorted)-3 : len(heightSorted)-1] 111 | if len(results) != len(want) { 112 | t.Errorf("StartAt(), EndAt() = %d; want = %d", len(results), len(want)) 113 | } 114 | got := getNames(results) 115 | if !reflect.DeepEqual(got, want) { 116 | t.Errorf("LimitToLast() = %v; want = %v", got, want) 117 | } 118 | compareValues(t, results) 119 | } 120 | 121 | func TestEqualTo(t *testing.T) { 122 | results, err := dinos.OrderByChild("height").EqualTo(0.6).GetOrdered(context.Background()) 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | 127 | want := heightSorted[:2] 128 | if len(results) != len(want) { 129 | t.Errorf("EqualTo() = %d; want = %d", len(results), len(want)) 130 | } 131 | got := getNames(results) 132 | if !reflect.DeepEqual(got, want) { 133 | t.Errorf("LimitToLast() = %v; want = %v", got, want) 134 | } 135 | compareValues(t, results) 136 | } 137 | 138 | func TestOrderByNestedChild(t *testing.T) { 139 | results, err := dinos.OrderByChild("ratings/pos").StartAt(4).GetOrdered(context.Background()) 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | 144 | want := []string{"pterodactyl", "stegosaurus", "triceratops"} 145 | if len(results) != len(want) { 146 | t.Errorf("OrderByChild(ratings/pos) = %d; want = %d", len(results), len(want)) 147 | } 148 | got := getNames(results) 149 | if !reflect.DeepEqual(got, want) { 150 | t.Errorf("LimitToLast() = %v; want = %v", got, want) 151 | } 152 | compareValues(t, results) 153 | } 154 | 155 | func TestOrderByKey(t *testing.T) { 156 | results, err := dinos.OrderByKey().LimitToFirst(2).GetOrdered(context.Background()) 157 | if err != nil { 158 | t.Fatal(err) 159 | } 160 | 161 | want := []string{"bruhathkayosaurus", "lambeosaurus"} 162 | if len(results) != len(want) { 163 | t.Errorf("OrderByKey() = %d; want = %d", len(results), len(want)) 164 | } 165 | got := getNames(results) 166 | if !reflect.DeepEqual(got, want) { 167 | t.Errorf("LimitToLast() = %v; want = %v", got, want) 168 | } 169 | compareValues(t, results) 170 | } 171 | 172 | func TestOrderByValue(t *testing.T) { 173 | scores := ref.Child("scores") 174 | results, err := scores.OrderByValue().LimitToLast(2).GetOrdered(context.Background()) 175 | if err != nil { 176 | t.Fatal(err) 177 | } 178 | 179 | want := []string{"linhenykus", "pterodactyl"} 180 | if len(results) != len(want) { 181 | t.Errorf("OrderByValue() = %d; want = %d", len(results), len(want)) 182 | } 183 | got := getNames(results) 184 | if !reflect.DeepEqual(got, want) { 185 | t.Errorf("LimitToLast() = %v; want = %v", got, want) 186 | } 187 | wantScores := []int{80, 93} 188 | for i, r := range results { 189 | var val int 190 | if err := r.Unmarshal(&val); err != nil { 191 | t.Fatalf("queryNode.Unmarshal() = %v", err) 192 | } 193 | if val != wantScores[i] { 194 | t.Errorf("queryNode.Unmarshal() = %d; want = %d", val, wantScores[i]) 195 | } 196 | } 197 | } 198 | 199 | func TestQueryWithContext(t *testing.T) { 200 | ctx, cancel := context.WithCancel(context.Background()) 201 | q := dinos.OrderByKey().LimitToFirst(2) 202 | var m map[string]Dinosaur 203 | if err := q.Get(ctx, &m); err != nil { 204 | t.Fatal(err) 205 | } 206 | 207 | want := []string{"bruhathkayosaurus", "lambeosaurus"} 208 | if len(m) != len(want) { 209 | t.Errorf("OrderByKey() = %d; want = %d", len(m), len(want)) 210 | } 211 | 212 | cancel() 213 | m = nil 214 | if err := q.Get(ctx, &m); len(m) != 0 || err == nil { 215 | t.Errorf("Get() = (%v, %v); want = (empty, error)", m, err) 216 | } 217 | } 218 | 219 | func TestUnorderedQuery(t *testing.T) { 220 | var m map[string]Dinosaur 221 | if err := dinos.OrderByChild("height"). 222 | StartAt(2.5). 223 | EndAt(5). 224 | Get(context.Background(), &m); err != nil { 225 | t.Fatal(err) 226 | } 227 | 228 | want := heightSorted[len(heightSorted)-3 : len(heightSorted)-1] 229 | if len(m) != len(want) { 230 | t.Errorf("Get() = %d; want = %d", len(m), len(want)) 231 | } 232 | for i, w := range want { 233 | if _, ok := m[w]; !ok { 234 | t.Errorf("[%d] result[%q] not present", i, w) 235 | } 236 | } 237 | } 238 | 239 | func min(i, j int) int { 240 | if i < j { 241 | return i 242 | } 243 | return j 244 | } 245 | 246 | func getNames(results []db.QueryNode) []string { 247 | s := make([]string, len(results)) 248 | for i, v := range results { 249 | s[i] = v.Key() 250 | } 251 | return s 252 | } 253 | 254 | func compareValues(t *testing.T, results []db.QueryNode) { 255 | for _, r := range results { 256 | var d Dinosaur 257 | if err := r.Unmarshal(&d); err != nil { 258 | t.Fatalf("queryNode.Unmarshal(%q) = %v", r.Key(), err) 259 | } 260 | if !reflect.DeepEqual(d, parsedTestData[r.Key()]) { 261 | t.Errorf("queryNode.Unmarshal(%q) = %v; want = %v", r.Key(), d, parsedTestData[r.Key()]) 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /integration/firestore/firestore_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package firestore 16 | 17 | import ( 18 | "context" 19 | "log" 20 | "reflect" 21 | "testing" 22 | 23 | "firebase.google.com/go/v4/integration/internal" 24 | ) 25 | 26 | func TestFirestore(t *testing.T) { 27 | if testing.Short() { 28 | log.Println("skipping Firestore integration tests in short mode.") 29 | return 30 | } 31 | ctx := context.Background() 32 | app, err := internal.NewTestApp(ctx, nil) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | client, err := app.Firestore(ctx) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | doc := client.Collection("cities").Doc("Mountain View") 43 | data := map[string]interface{}{ 44 | "name": "Mountain View", 45 | "country": "USA", 46 | "population": int64(77846), 47 | "capital": false, 48 | } 49 | if _, err := doc.Set(ctx, data); err != nil { 50 | t.Fatal(err) 51 | } 52 | snap, err := doc.Get(ctx) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | if !reflect.DeepEqual(snap.Data(), data) { 57 | t.Errorf("Get() = %v; want %v", snap.Data(), data) 58 | } 59 | if _, err := doc.Delete(ctx); err != nil { 60 | t.Fatal(err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /integration/iid/iid_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package iid contains integration tests for the firebase.google.com/go/iid package. 16 | package iid 17 | 18 | import ( 19 | "context" 20 | "flag" 21 | "log" 22 | "os" 23 | "testing" 24 | 25 | "firebase.google.com/go/v4/errorutils" 26 | "firebase.google.com/go/v4/iid" 27 | "firebase.google.com/go/v4/integration/internal" 28 | ) 29 | 30 | var client *iid.Client 31 | 32 | func TestMain(m *testing.M) { 33 | flag.Parse() 34 | if testing.Short() { 35 | log.Println("skipping instance ID integration tests in short mode.") 36 | os.Exit(0) 37 | } 38 | 39 | ctx := context.Background() 40 | app, err := internal.NewTestApp(ctx, nil) 41 | if err != nil { 42 | log.Fatalln(err) 43 | } 44 | 45 | client, err = app.InstanceID(ctx) 46 | if err != nil { 47 | log.Fatalln(err) 48 | } 49 | 50 | os.Exit(m.Run()) 51 | } 52 | 53 | func TestNonExisting(t *testing.T) { 54 | // legal instance IDs are /[cdef][A-Za-z0-9_-]{9}[AEIMQUYcgkosw048]/ 55 | // "fictive-ID0" is match for that. 56 | err := client.DeleteInstanceID(context.Background(), "fictive-ID0") 57 | if err == nil { 58 | t.Errorf("DeleteInstanceID(non-existing) = nil; want error") 59 | } 60 | want := `instance id "fictive-ID0": failed to find the instance id` 61 | if !errorutils.IsNotFound(err) || err.Error() != want { 62 | t.Errorf("DeleteInstanceID(non-existing) = %v; want = %v", err, want) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /integration/internal/internal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package internal contains utilities for running integration tests. 16 | package internal 17 | 18 | import ( 19 | "context" 20 | "encoding/json" 21 | "io/ioutil" 22 | "net/http" 23 | "path/filepath" 24 | "strings" 25 | 26 | firebase "firebase.google.com/go/v4" 27 | "firebase.google.com/go/v4/internal" 28 | "google.golang.org/api/option" 29 | "google.golang.org/api/transport" 30 | ) 31 | 32 | const certPath = "integration_cert.json" 33 | const apiKeyPath = "integration_apikey.txt" 34 | 35 | // Resource returns the absolute path to the specified test resource file. 36 | func Resource(name string) string { 37 | p := []string{"..", "..", "testdata", name} 38 | return filepath.Join(p...) 39 | } 40 | 41 | // NewTestApp creates a new App instance for integration tests. 42 | // 43 | // NewTestApp looks for a service account JSON file named integration_cert.json 44 | // in the testdata directory. This file is used to initialize the newly created 45 | // App instance. 46 | func NewTestApp(ctx context.Context, conf *firebase.Config) (*firebase.App, error) { 47 | return firebase.NewApp(ctx, conf, option.WithCredentialsFile(Resource(certPath))) 48 | } 49 | 50 | // APIKey fetches a Firebase API key for integration tests. 51 | // 52 | // APIKey reads the API key string from a file named integration_apikey.txt 53 | // in the testdata directory. 54 | func APIKey() (string, error) { 55 | b, err := ioutil.ReadFile(Resource(apiKeyPath)) 56 | if err != nil { 57 | return "", err 58 | } 59 | return strings.TrimSpace(string(b)), nil 60 | } 61 | 62 | // ProjectID fetches a Google Cloud project ID for integration tests. 63 | func ProjectID() (string, error) { 64 | b, err := ioutil.ReadFile(Resource(certPath)) 65 | if err != nil { 66 | return "", err 67 | } 68 | var serviceAccount struct { 69 | ProjectID string `json:"project_id"` 70 | } 71 | if err := json.Unmarshal(b, &serviceAccount); err != nil { 72 | return "", err 73 | } 74 | return serviceAccount.ProjectID, nil 75 | } 76 | 77 | // NewHTTPClient creates an HTTP client for making authorized requests during tests. 78 | func NewHTTPClient(ctx context.Context, opts ...option.ClientOption) (*http.Client, error) { 79 | opts = append( 80 | opts, 81 | option.WithCredentialsFile(Resource(certPath)), 82 | option.WithScopes(internal.FirebaseScopes...), 83 | ) 84 | hc, _, err := transport.NewHTTPClient(ctx, opts...) 85 | return hc, err 86 | } 87 | -------------------------------------------------------------------------------- /integration/storage/storage_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package storage 16 | 17 | import ( 18 | "context" 19 | "flag" 20 | "fmt" 21 | "io/ioutil" 22 | "log" 23 | "os" 24 | "testing" 25 | 26 | gcs "cloud.google.com/go/storage" 27 | firebase "firebase.google.com/go/v4" 28 | "firebase.google.com/go/v4/integration/internal" 29 | "firebase.google.com/go/v4/storage" 30 | ) 31 | 32 | var ctx context.Context 33 | var client *storage.Client 34 | 35 | func TestMain(m *testing.M) { 36 | flag.Parse() 37 | if testing.Short() { 38 | log.Println("skipping storage integration tests in short mode.") 39 | os.Exit(0) 40 | } 41 | 42 | pid, err := internal.ProjectID() 43 | if err != nil { 44 | log.Fatalln(err) 45 | } 46 | 47 | ctx = context.Background() 48 | app, err := internal.NewTestApp(ctx, &firebase.Config{ 49 | StorageBucket: fmt.Sprintf("%s.appspot.com", pid), 50 | }) 51 | if err != nil { 52 | log.Fatalln(err) 53 | } 54 | 55 | client, err = app.Storage(ctx) 56 | if err != nil { 57 | log.Fatalln(err) 58 | } 59 | 60 | os.Exit(m.Run()) 61 | } 62 | 63 | func TestDefaultBucket(t *testing.T) { 64 | bucket, err := client.DefaultBucket() 65 | if bucket == nil || err != nil { 66 | t.Errorf("DefaultBucket() = (%v, %v); want (bucket, nil)", bucket, err) 67 | } 68 | if err := verifyBucket(bucket); err != nil { 69 | t.Fatal(err) 70 | } 71 | } 72 | 73 | func TestCustomBucket(t *testing.T) { 74 | pid, err := internal.ProjectID() 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | bucket, err := client.Bucket(pid + ".appspot.com") 80 | if bucket == nil || err != nil { 81 | t.Errorf("Bucket() = (%v, %v); want (bucket, nil)", bucket, err) 82 | } 83 | if err := verifyBucket(bucket); err != nil { 84 | t.Fatal(err) 85 | } 86 | } 87 | 88 | func TestNonExistingBucket(t *testing.T) { 89 | bucket, err := client.Bucket("non-existing") 90 | if bucket == nil || err != nil { 91 | t.Errorf("Bucket() = (%v, %v); want (bucket, nil)", bucket, err) 92 | } 93 | if _, err := bucket.Attrs(context.Background()); err == nil { 94 | t.Errorf("bucket.Attr() = nil; want error") 95 | } 96 | } 97 | 98 | func verifyBucket(bucket *gcs.BucketHandle) error { 99 | const expected = "Hello World" 100 | 101 | // Create new object 102 | o := bucket.Object("data") 103 | w := o.NewWriter(ctx) 104 | w.ContentType = "text/plain" 105 | if _, err := w.Write([]byte(expected)); err != nil { 106 | return err 107 | } 108 | if err := w.Close(); err != nil { 109 | return err 110 | } 111 | 112 | // Read the created object 113 | r, err := o.NewReader(ctx) 114 | if err != nil { 115 | return err 116 | } 117 | defer r.Close() 118 | b, err := ioutil.ReadAll(r) 119 | if err != nil { 120 | return err 121 | } 122 | if string(b) != expected { 123 | return fmt.Errorf("fetched content: %q; want: %q", string(b), expected) 124 | } 125 | 126 | // Delete the object 127 | return o.Delete(ctx) 128 | } 129 | -------------------------------------------------------------------------------- /internal/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package internal 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "net" 21 | "net/http" 22 | "net/url" 23 | "os" 24 | "syscall" 25 | ) 26 | 27 | // ErrorCode represents the platform-wide error codes that can be raised by 28 | // Admin SDK APIs. 29 | type ErrorCode string 30 | 31 | const ( 32 | // InvalidArgument is a OnePlatform error code. 33 | InvalidArgument ErrorCode = "INVALID_ARGUMENT" 34 | 35 | // FailedPrecondition is a OnePlatform error code. 36 | FailedPrecondition ErrorCode = "FAILED_PRECONDITION" 37 | 38 | // OutOfRange is a OnePlatform error code. 39 | OutOfRange ErrorCode = "OUT_OF_RANGE" 40 | 41 | // Unauthenticated is a OnePlatform error code. 42 | Unauthenticated ErrorCode = "UNAUTHENTICATED" 43 | 44 | // PermissionDenied is a OnePlatform error code. 45 | PermissionDenied ErrorCode = "PERMISSION_DENIED" 46 | 47 | // NotFound is a OnePlatform error code. 48 | NotFound ErrorCode = "NOT_FOUND" 49 | 50 | // Conflict is a custom error code that represents HTTP 409 responses. 51 | // 52 | // OnePlatform APIs typically respond with ABORTED or ALREADY_EXISTS explicitly. But a few 53 | // old APIs send HTTP 409 Conflict without any additional details to distinguish between the two 54 | // cases. For these we currently use this error code. As more APIs adopt OnePlatform conventions 55 | // this will become less important. 56 | Conflict ErrorCode = "CONFLICT" 57 | 58 | // Aborted is a OnePlatform error code. 59 | Aborted ErrorCode = "ABORTED" 60 | 61 | // AlreadyExists is a OnePlatform error code. 62 | AlreadyExists ErrorCode = "ALREADY_EXISTS" 63 | 64 | // ResourceExhausted is a OnePlatform error code. 65 | ResourceExhausted ErrorCode = "RESOURCE_EXHAUSTED" 66 | 67 | // Cancelled is a OnePlatform error code. 68 | Cancelled ErrorCode = "CANCELLED" 69 | 70 | // DataLoss is a OnePlatform error code. 71 | DataLoss ErrorCode = "DATA_LOSS" 72 | 73 | // Unknown is a OnePlatform error code. 74 | Unknown ErrorCode = "UNKNOWN" 75 | 76 | // Internal is a OnePlatform error code. 77 | Internal ErrorCode = "INTERNAL" 78 | 79 | // Unavailable is a OnePlatform error code. 80 | Unavailable ErrorCode = "UNAVAILABLE" 81 | 82 | // DeadlineExceeded is a OnePlatform error code. 83 | DeadlineExceeded ErrorCode = "DEADLINE_EXCEEDED" 84 | ) 85 | 86 | // FirebaseError is an error type containing an error code string. 87 | type FirebaseError struct { 88 | ErrorCode ErrorCode 89 | String string 90 | Response *http.Response 91 | Ext map[string]interface{} 92 | } 93 | 94 | func (fe *FirebaseError) Error() string { 95 | return fe.String 96 | } 97 | 98 | // HasPlatformErrorCode checks if the given error contains a specific error code. 99 | func HasPlatformErrorCode(err error, code ErrorCode) bool { 100 | fe, ok := err.(*FirebaseError) 101 | return ok && fe.ErrorCode == code 102 | } 103 | 104 | var httpStatusToErrorCodes = map[int]ErrorCode{ 105 | http.StatusBadRequest: InvalidArgument, 106 | http.StatusUnauthorized: Unauthenticated, 107 | http.StatusForbidden: PermissionDenied, 108 | http.StatusNotFound: NotFound, 109 | http.StatusConflict: Conflict, 110 | http.StatusTooManyRequests: ResourceExhausted, 111 | http.StatusInternalServerError: Internal, 112 | http.StatusServiceUnavailable: Unavailable, 113 | } 114 | 115 | // NewFirebaseError creates a new error from the given HTTP response. 116 | func NewFirebaseError(resp *Response) *FirebaseError { 117 | code, ok := httpStatusToErrorCodes[resp.Status] 118 | if !ok { 119 | code = Unknown 120 | } 121 | 122 | return &FirebaseError{ 123 | ErrorCode: code, 124 | String: fmt.Sprintf("unexpected http response with status: %d\n%s", resp.Status, string(resp.Body)), 125 | Response: resp.LowLevelResponse(), 126 | Ext: make(map[string]interface{}), 127 | } 128 | } 129 | 130 | // NewFirebaseErrorOnePlatform parses the response payload as a GCP error response 131 | // and create an error from the details extracted. 132 | // 133 | // If the response failes to parse, or otherwise doesn't provide any useful details 134 | // NewFirebaseErrorOnePlatform creates an error with some sensible defaults. 135 | func NewFirebaseErrorOnePlatform(resp *Response) *FirebaseError { 136 | base := NewFirebaseError(resp) 137 | 138 | var gcpError struct { 139 | Error struct { 140 | Status string `json:"status"` 141 | Message string `json:"message"` 142 | } `json:"error"` 143 | } 144 | json.Unmarshal(resp.Body, &gcpError) // ignore any json parse errors at this level 145 | if gcpError.Error.Status != "" { 146 | base.ErrorCode = ErrorCode(gcpError.Error.Status) 147 | } 148 | 149 | if gcpError.Error.Message != "" { 150 | base.String = gcpError.Error.Message 151 | } 152 | 153 | return base 154 | } 155 | 156 | func newFirebaseErrorTransport(err error) *FirebaseError { 157 | var code ErrorCode 158 | var msg string 159 | if os.IsTimeout(err) { 160 | code = DeadlineExceeded 161 | msg = fmt.Sprintf("timed out while making an http call: %v", err) 162 | } else if isConnectionRefused(err) { 163 | code = Unavailable 164 | msg = fmt.Sprintf("failed to establish a connection: %v", err) 165 | } else { 166 | code = Unknown 167 | msg = fmt.Sprintf("unknown error while making an http call: %v", err) 168 | } 169 | 170 | return &FirebaseError{ 171 | ErrorCode: code, 172 | String: msg, 173 | Ext: make(map[string]interface{}), 174 | } 175 | } 176 | 177 | // isConnectionRefused attempts to determine if the given error was caused by a failure to establish a 178 | // connection. 179 | // 180 | // A net.OpError where the Op field is set to "dial" or "read" is considered a connection refused 181 | // error. Similarly an ECONNREFUSED error code (Linux-specific) is also considered a connection 182 | // refused error. 183 | func isConnectionRefused(err error) bool { 184 | switch t := err.(type) { 185 | case *url.Error: 186 | return isConnectionRefused(t.Err) 187 | case *net.OpError: 188 | if t.Op == "dial" || t.Op == "read" { 189 | return true 190 | } 191 | return isConnectionRefused(t.Err) 192 | case syscall.Errno: 193 | return t == syscall.ECONNREFUSED 194 | } 195 | 196 | return false 197 | } 198 | -------------------------------------------------------------------------------- /internal/internal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package internal contains functionality that is only accessible from within the Admin SDK. 16 | package internal 17 | 18 | import ( 19 | "time" 20 | 21 | "golang.org/x/oauth2" 22 | "google.golang.org/api/option" 23 | ) 24 | 25 | // FirebaseScopes is the set of OAuth2 scopes used by the Admin SDK. 26 | var FirebaseScopes = []string{ 27 | "https://www.googleapis.com/auth/cloud-platform", 28 | "https://www.googleapis.com/auth/datastore", 29 | "https://www.googleapis.com/auth/devstorage.full_control", 30 | "https://www.googleapis.com/auth/firebase", 31 | "https://www.googleapis.com/auth/identitytoolkit", 32 | "https://www.googleapis.com/auth/userinfo.email", 33 | } 34 | 35 | // SystemClock is a clock that returns local time of the system. 36 | var SystemClock = &systemClock{} 37 | 38 | // AuthConfig represents the configuration of Firebase Auth service. 39 | type AuthConfig struct { 40 | Opts []option.ClientOption 41 | ProjectID string 42 | ServiceAccountID string 43 | Version string 44 | } 45 | 46 | // HashConfig represents a hash algorithm configuration used to generate password hashes. 47 | type HashConfig map[string]interface{} 48 | 49 | // InstanceIDConfig represents the configuration of Firebase Instance ID service. 50 | type InstanceIDConfig struct { 51 | Opts []option.ClientOption 52 | ProjectID string 53 | Version string 54 | } 55 | 56 | // DatabaseConfig represents the configuration of Firebase Database service. 57 | type DatabaseConfig struct { 58 | Opts []option.ClientOption 59 | URL string 60 | Version string 61 | AuthOverride map[string]interface{} 62 | } 63 | 64 | // StorageConfig represents the configuration of Google Cloud Storage service. 65 | type StorageConfig struct { 66 | Opts []option.ClientOption 67 | Bucket string 68 | } 69 | 70 | // MessagingConfig represents the configuration of Firebase Cloud Messaging service. 71 | type MessagingConfig struct { 72 | Opts []option.ClientOption 73 | ProjectID string 74 | Version string 75 | } 76 | 77 | // RemoteConfigClientConfig represents the configuration of Firebase Remote Config 78 | type RemoteConfigClientConfig struct { 79 | Opts []option.ClientOption 80 | ProjectID string 81 | Version string 82 | } 83 | 84 | // AppCheckConfig represents the configuration of App Check service. 85 | type AppCheckConfig struct { 86 | ProjectID string 87 | } 88 | 89 | // MockTokenSource is a TokenSource implementation that can be used for testing. 90 | type MockTokenSource struct { 91 | AccessToken string 92 | } 93 | 94 | // Token returns the test token associated with the TokenSource. 95 | func (ts *MockTokenSource) Token() (*oauth2.Token, error) { 96 | return &oauth2.Token{AccessToken: ts.AccessToken}, nil 97 | } 98 | 99 | // Clock is used to query the current local time. 100 | type Clock interface { 101 | Now() time.Time 102 | } 103 | 104 | // systemClock returns the current system time. 105 | type systemClock struct{} 106 | 107 | // Now returns the current system time by calling time.Now(). 108 | func (s *systemClock) Now() time.Time { 109 | return time.Now() 110 | } 111 | 112 | // MockClock can be used to mock current time during tests. 113 | type MockClock struct { 114 | Timestamp time.Time 115 | } 116 | 117 | // Now returns the timestamp set in the MockClock. 118 | func (m *MockClock) Now() time.Time { 119 | return m.Timestamp 120 | } 121 | -------------------------------------------------------------------------------- /internal/json_http_client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package internal 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "io/ioutil" 22 | "net/http" 23 | "net/http/httptest" 24 | "strings" 25 | "testing" 26 | ) 27 | 28 | const wantURL = "/test" 29 | 30 | func TestDoAndUnmarshalGet(t *testing.T) { 31 | var req *http.Request 32 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 | req = r 34 | resp := `{ 35 | "name": "test" 36 | }` 37 | w.Write([]byte(resp)) 38 | }) 39 | server := httptest.NewServer(handler) 40 | defer server.Close() 41 | 42 | client := &HTTPClient{ 43 | Client: http.DefaultClient, 44 | } 45 | get := &Request{ 46 | Method: http.MethodGet, 47 | URL: fmt.Sprintf("%s%s", server.URL, wantURL), 48 | } 49 | var data responseBody 50 | 51 | resp, err := client.DoAndUnmarshal(context.Background(), get, &data) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | if resp.Status != http.StatusOK { 57 | t.Errorf("Status = %d; want = %d", resp.Status, http.StatusOK) 58 | } 59 | if data.Name != "test" { 60 | t.Errorf("Data = %v; want = {Name: %q}", data, "test") 61 | } 62 | if req.Method != http.MethodGet { 63 | t.Errorf("Method = %q; want = %q", req.Method, http.MethodGet) 64 | } 65 | if req.URL.Path != wantURL { 66 | t.Errorf("URL = %q; want = %q", req.URL.Path, wantURL) 67 | } 68 | } 69 | 70 | func TestDoAndUnmarshalPost(t *testing.T) { 71 | var req *http.Request 72 | var b []byte 73 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 74 | req = r 75 | b, _ = ioutil.ReadAll(r.Body) 76 | resp := `{ 77 | "name": "test" 78 | }` 79 | w.Write([]byte(resp)) 80 | }) 81 | server := httptest.NewServer(handler) 82 | defer server.Close() 83 | 84 | client := &HTTPClient{ 85 | Client: http.DefaultClient, 86 | } 87 | post := &Request{ 88 | Method: http.MethodPost, 89 | URL: fmt.Sprintf("%s%s", server.URL, wantURL), 90 | Body: NewJSONEntity(map[string]string{"input": "test-input"}), 91 | } 92 | var data responseBody 93 | 94 | resp, err := client.DoAndUnmarshal(context.Background(), post, &data) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | if resp.Status != http.StatusOK { 100 | t.Errorf("Status = %d; want = %d", resp.Status, http.StatusOK) 101 | } 102 | if data.Name != "test" { 103 | t.Errorf("Data = %v; want = {Name: %q}", data, "test") 104 | } 105 | if req.Method != http.MethodPost { 106 | t.Errorf("Method = %q; want = %q", req.Method, http.MethodGet) 107 | } 108 | if req.URL.Path != wantURL { 109 | t.Errorf("URL = %q; want = %q", req.URL.Path, wantURL) 110 | } 111 | 112 | var parsed struct { 113 | Input string `json:"input"` 114 | } 115 | if err := json.Unmarshal(b, &parsed); err != nil { 116 | t.Fatal(err) 117 | } 118 | if parsed.Input != "test-input" { 119 | t.Errorf("Request Body = %v; want = {Input: %q}", parsed, "test-input") 120 | } 121 | } 122 | 123 | func TestDoAndUnmarshalNotJSON(t *testing.T) { 124 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 125 | w.Write([]byte("not json")) 126 | }) 127 | server := httptest.NewServer(handler) 128 | defer server.Close() 129 | 130 | client := &HTTPClient{ 131 | Client: http.DefaultClient, 132 | } 133 | get := &Request{ 134 | Method: http.MethodGet, 135 | URL: server.URL, 136 | } 137 | var data interface{} 138 | wantPrefix := "error while parsing response: " 139 | 140 | resp, err := client.DoAndUnmarshal(context.Background(), get, &data) 141 | if resp != nil || err == nil || !strings.HasPrefix(err.Error(), wantPrefix) { 142 | t.Errorf("DoAndUnmarshal() = (%v, %v); want = (nil, %q)", resp, err, wantPrefix) 143 | } 144 | 145 | if data != nil { 146 | t.Errorf("Data = %v; want = nil", data) 147 | } 148 | } 149 | 150 | func TestDoAndUnmarshalNilPointer(t *testing.T) { 151 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 152 | w.Write([]byte("not json")) 153 | }) 154 | server := httptest.NewServer(handler) 155 | defer server.Close() 156 | 157 | client := &HTTPClient{ 158 | Client: http.DefaultClient, 159 | } 160 | get := &Request{ 161 | Method: http.MethodGet, 162 | URL: server.URL, 163 | } 164 | 165 | resp, err := client.DoAndUnmarshal(context.Background(), get, nil) 166 | if err != nil { 167 | t.Fatalf("DoAndUnmarshal() = %v; want = nil", err) 168 | } 169 | 170 | if resp.Status != http.StatusOK { 171 | t.Errorf("Status = %d; want = %d", resp.Status, http.StatusOK) 172 | } 173 | } 174 | 175 | func TestDoAndUnmarshalTransportError(t *testing.T) { 176 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) 177 | server := httptest.NewServer(handler) 178 | server.Close() 179 | 180 | client := &HTTPClient{ 181 | Client: http.DefaultClient, 182 | } 183 | get := &Request{ 184 | Method: http.MethodGet, 185 | URL: server.URL, 186 | } 187 | var data interface{} 188 | 189 | resp, err := client.DoAndUnmarshal(context.Background(), get, &data) 190 | if resp != nil || err == nil { 191 | t.Errorf("DoAndUnmarshal() = (%v, %v); want = (nil, error)", resp, err) 192 | } 193 | 194 | if data != nil { 195 | t.Errorf("Data = %v; want = nil", data) 196 | } 197 | } 198 | 199 | type responseBody struct { 200 | Name string `json:"name"` 201 | } 202 | -------------------------------------------------------------------------------- /messaging/messaging_utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package messaging 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "net/url" 21 | "regexp" 22 | "strings" 23 | ) 24 | 25 | var ( 26 | bareTopicNamePattern = regexp.MustCompile("^[a-zA-Z0-9-_.~%]+$") 27 | colorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") 28 | colorWithAlphaPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$") 29 | ) 30 | 31 | func validateMessage(message *Message) error { 32 | if message == nil { 33 | return fmt.Errorf("message must not be nil") 34 | } 35 | 36 | targets := countNonEmpty(message.Token, message.Condition, message.Topic) 37 | if targets != 1 { 38 | return fmt.Errorf("exactly one of token, topic or condition must be specified") 39 | } 40 | 41 | // validate topic 42 | if message.Topic != "" { 43 | bt := strings.TrimPrefix(message.Topic, "/topics/") 44 | if !bareTopicNamePattern.MatchString(bt) { 45 | return fmt.Errorf("malformed topic name") 46 | } 47 | } 48 | 49 | // validate Notification 50 | if err := validateNotification(message.Notification); err != nil { 51 | return err 52 | } 53 | 54 | // validate AndroidConfig 55 | if err := validateAndroidConfig(message.Android); err != nil { 56 | return err 57 | } 58 | 59 | // validate WebpushConfig 60 | if err := validateWebpushConfig(message.Webpush); err != nil { 61 | return err 62 | } 63 | 64 | // validate APNSConfig 65 | return validateAPNSConfig(message.APNS) 66 | } 67 | 68 | func validateNotification(notification *Notification) error { 69 | if notification == nil { 70 | return nil 71 | } 72 | 73 | image := notification.ImageURL 74 | if image != "" { 75 | if _, err := url.ParseRequestURI(image); err != nil { 76 | return fmt.Errorf("invalid image URL: %q", image) 77 | } 78 | } 79 | return nil 80 | } 81 | 82 | func validateAndroidConfig(config *AndroidConfig) error { 83 | if config == nil { 84 | return nil 85 | } 86 | 87 | if config.TTL != nil && config.TTL.Seconds() < 0 { 88 | return fmt.Errorf("ttl duration must not be negative") 89 | } 90 | if config.Priority != "" && config.Priority != "normal" && config.Priority != "high" { 91 | return fmt.Errorf("priority must be 'normal' or 'high'") 92 | } 93 | 94 | // validate AndroidNotification 95 | return validateAndroidNotification(config.Notification) 96 | } 97 | 98 | func validateAndroidNotification(notification *AndroidNotification) error { 99 | if notification == nil { 100 | return nil 101 | } 102 | if notification.Color != "" && !colorPattern.MatchString(notification.Color) { 103 | return fmt.Errorf("color must be in the #RRGGBB form") 104 | } 105 | if len(notification.TitleLocArgs) > 0 && notification.TitleLocKey == "" { 106 | return fmt.Errorf("titleLocKey is required when specifying titleLocArgs") 107 | } 108 | if len(notification.BodyLocArgs) > 0 && notification.BodyLocKey == "" { 109 | return fmt.Errorf("bodyLocKey is required when specifying bodyLocArgs") 110 | } 111 | image := notification.ImageURL 112 | if image != "" { 113 | if _, err := url.ParseRequestURI(image); err != nil { 114 | return fmt.Errorf("invalid image URL: %q", image) 115 | } 116 | } 117 | for _, timing := range notification.VibrateTimingMillis { 118 | if timing < 0 { 119 | return fmt.Errorf("vibrateTimingMillis must not be negative") 120 | } 121 | } 122 | 123 | return validateLightSettings(notification.LightSettings) 124 | } 125 | 126 | func validateLightSettings(light *LightSettings) error { 127 | if light == nil { 128 | return nil 129 | } 130 | if !colorWithAlphaPattern.MatchString(light.Color) { 131 | return errors.New("color must be in #RRGGBB or #RRGGBBAA form") 132 | } 133 | if light.LightOnDurationMillis < 0 { 134 | return errors.New("lightOnDuration must not be negative") 135 | } 136 | if light.LightOffDurationMillis < 0 { 137 | return errors.New("lightOffDuration must not be negative") 138 | } 139 | return nil 140 | } 141 | 142 | func validateAPNSConfig(config *APNSConfig) error { 143 | if config != nil { 144 | // validate FCMOptions 145 | if config.FCMOptions != nil { 146 | image := config.FCMOptions.ImageURL 147 | if image != "" { 148 | if _, err := url.ParseRequestURI(image); err != nil { 149 | return fmt.Errorf("invalid image URL: %q", image) 150 | } 151 | } 152 | } 153 | return validateAPNSPayload(config.Payload) 154 | } 155 | return nil 156 | } 157 | 158 | func validateAPNSPayload(payload *APNSPayload) error { 159 | if payload != nil { 160 | m := payload.standardFields() 161 | for k := range payload.CustomData { 162 | if _, contains := m[k]; contains { 163 | return fmt.Errorf("multiple specifications for the key %q", k) 164 | } 165 | } 166 | return validateAps(payload.Aps) 167 | } 168 | return nil 169 | } 170 | 171 | func validateAps(aps *Aps) error { 172 | if aps != nil { 173 | if aps.Alert != nil && aps.AlertString != "" { 174 | return fmt.Errorf("multiple alert specifications") 175 | } 176 | if aps.CriticalSound != nil { 177 | if aps.Sound != "" { 178 | return fmt.Errorf("multiple sound specifications") 179 | } 180 | if aps.CriticalSound.Volume < 0 || aps.CriticalSound.Volume > 1 { 181 | return fmt.Errorf("critical sound volume must be in the interval [0, 1]") 182 | } 183 | } 184 | m := aps.standardFields() 185 | for k := range aps.CustomData { 186 | if _, contains := m[k]; contains { 187 | return fmt.Errorf("multiple specifications for the key %q", k) 188 | } 189 | } 190 | return validateApsAlert(aps.Alert) 191 | } 192 | return nil 193 | } 194 | 195 | func validateApsAlert(alert *ApsAlert) error { 196 | if alert == nil { 197 | return nil 198 | } 199 | if len(alert.TitleLocArgs) > 0 && alert.TitleLocKey == "" { 200 | return fmt.Errorf("titleLocKey is required when specifying titleLocArgs") 201 | } 202 | if len(alert.SubTitleLocArgs) > 0 && alert.SubTitleLocKey == "" { 203 | return fmt.Errorf("subtitleLocKey is required when specifying subtitleLocArgs") 204 | } 205 | if len(alert.LocArgs) > 0 && alert.LocKey == "" { 206 | return fmt.Errorf("locKey is required when specifying locArgs") 207 | } 208 | return nil 209 | } 210 | 211 | func validateWebpushConfig(webpush *WebpushConfig) error { 212 | if webpush == nil || webpush.Notification == nil { 213 | return nil 214 | } 215 | dir := webpush.Notification.Direction 216 | if dir != "" && dir != "ltr" && dir != "rtl" && dir != "auto" { 217 | return fmt.Errorf("direction must be 'ltr', 'rtl' or 'auto'") 218 | } 219 | m := webpush.Notification.standardFields() 220 | for k := range webpush.Notification.CustomData { 221 | if _, contains := m[k]; contains { 222 | return fmt.Errorf("multiple specifications for the key %q", k) 223 | } 224 | } 225 | if webpush.FCMOptions != nil { 226 | link := webpush.FCMOptions.Link 227 | p, err := url.ParseRequestURI(link) 228 | if err != nil { 229 | return fmt.Errorf("invalid link URL: %q", link) 230 | } else if p.Scheme != "https" { 231 | return fmt.Errorf("invalid link URL: %q; want scheme: %q", link, "https") 232 | } 233 | } 234 | return nil 235 | } 236 | 237 | func countNonEmpty(strings ...string) int { 238 | count := 0 239 | for _, s := range strings { 240 | if s != "" { 241 | count++ 242 | } 243 | } 244 | return count 245 | } 246 | -------------------------------------------------------------------------------- /messaging/topic_mgt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package messaging 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "net/http" 22 | "strings" 23 | 24 | "firebase.google.com/go/v4/internal" 25 | ) 26 | 27 | const ( 28 | iidEndpoint = "https://iid.googleapis.com/iid/v1" 29 | iidSubscribe = "batchAdd" 30 | iidUnsubscribe = "batchRemove" 31 | ) 32 | 33 | // TopicManagementResponse is the result produced by topic management operations. 34 | // 35 | // TopicManagementResponse provides an overview of how many input tokens were successfully handled, 36 | // and how many failed. In case of failures, the Errors list provides specific details concerning 37 | // each error. 38 | type TopicManagementResponse struct { 39 | SuccessCount int 40 | FailureCount int 41 | Errors []*ErrorInfo 42 | } 43 | 44 | func newTopicManagementResponse(resp *iidResponse) *TopicManagementResponse { 45 | tmr := &TopicManagementResponse{} 46 | for idx, res := range resp.Results { 47 | if len(res) == 0 { 48 | tmr.SuccessCount++ 49 | } else { 50 | tmr.FailureCount++ 51 | reason := res["error"].(string) 52 | tmr.Errors = append(tmr.Errors, &ErrorInfo{ 53 | Index: idx, 54 | Reason: reason, 55 | }) 56 | } 57 | } 58 | return tmr 59 | } 60 | 61 | type iidClient struct { 62 | iidEndpoint string 63 | httpClient *internal.HTTPClient 64 | } 65 | 66 | func newIIDClient(hc *http.Client, conf *internal.MessagingConfig) *iidClient { 67 | client := internal.WithDefaultRetryConfig(hc) 68 | client.CreateErrFn = handleIIDError 69 | client.Opts = []internal.HTTPOption{ 70 | internal.WithHeader("access_token_auth", "true"), 71 | internal.WithHeader("x-goog-api-client", internal.GetMetricsHeader(conf.Version)), 72 | } 73 | return &iidClient{ 74 | iidEndpoint: iidEndpoint, 75 | httpClient: client, 76 | } 77 | } 78 | 79 | // SubscribeToTopic subscribes a list of registration tokens to a topic. 80 | // 81 | // The tokens list must not be empty, and have at most 1000 tokens. 82 | func (c *iidClient) SubscribeToTopic(ctx context.Context, tokens []string, topic string) (*TopicManagementResponse, error) { 83 | req := &iidRequest{ 84 | Topic: topic, 85 | Tokens: tokens, 86 | op: iidSubscribe, 87 | } 88 | return c.makeTopicManagementRequest(ctx, req) 89 | } 90 | 91 | // UnsubscribeFromTopic unsubscribes a list of registration tokens from a topic. 92 | // 93 | // The tokens list must not be empty, and have at most 1000 tokens. 94 | func (c *iidClient) UnsubscribeFromTopic(ctx context.Context, tokens []string, topic string) (*TopicManagementResponse, error) { 95 | req := &iidRequest{ 96 | Topic: topic, 97 | Tokens: tokens, 98 | op: iidUnsubscribe, 99 | } 100 | return c.makeTopicManagementRequest(ctx, req) 101 | } 102 | 103 | type iidRequest struct { 104 | Topic string `json:"to"` 105 | Tokens []string `json:"registration_tokens"` 106 | op string 107 | } 108 | 109 | type iidResponse struct { 110 | Results []map[string]interface{} `json:"results"` 111 | } 112 | 113 | type iidErrorResponse struct { 114 | Error string `json:"error"` 115 | } 116 | 117 | func (c *iidClient) makeTopicManagementRequest(ctx context.Context, req *iidRequest) (*TopicManagementResponse, error) { 118 | if len(req.Tokens) == 0 { 119 | return nil, fmt.Errorf("no tokens specified") 120 | } 121 | if len(req.Tokens) > 1000 { 122 | return nil, fmt.Errorf("tokens list must not contain more than 1000 items") 123 | } 124 | for _, token := range req.Tokens { 125 | if token == "" { 126 | return nil, fmt.Errorf("tokens list must not contain empty strings") 127 | } 128 | } 129 | 130 | if req.Topic == "" { 131 | return nil, fmt.Errorf("topic name not specified") 132 | } 133 | if !topicNamePattern.MatchString(req.Topic) { 134 | return nil, fmt.Errorf("invalid topic name: %q", req.Topic) 135 | } 136 | 137 | if !strings.HasPrefix(req.Topic, "/topics/") { 138 | req.Topic = "/topics/" + req.Topic 139 | } 140 | 141 | request := &internal.Request{ 142 | Method: http.MethodPost, 143 | URL: fmt.Sprintf("%s:%s", c.iidEndpoint, req.op), 144 | Body: internal.NewJSONEntity(req), 145 | } 146 | var result iidResponse 147 | if _, err := c.httpClient.DoAndUnmarshal(ctx, request, &result); err != nil { 148 | return nil, err 149 | } 150 | 151 | return newTopicManagementResponse(&result), nil 152 | } 153 | 154 | func handleIIDError(resp *internal.Response) error { 155 | base := internal.NewFirebaseError(resp) 156 | var ie iidErrorResponse 157 | json.Unmarshal(resp.Body, &ie) // ignore any json parse errors at this level 158 | if ie.Error != "" { 159 | base.String = fmt.Sprintf("error while calling the iid service: %s", ie.Error) 160 | } 161 | 162 | return base 163 | } 164 | -------------------------------------------------------------------------------- /messaging/topic_mgt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package messaging 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "io/ioutil" 21 | "net/http" 22 | "net/http/httptest" 23 | "reflect" 24 | "strings" 25 | "testing" 26 | 27 | "firebase.google.com/go/v4/errorutils" 28 | "firebase.google.com/go/v4/internal" 29 | ) 30 | 31 | func TestSubscribe(t *testing.T) { 32 | var tr *http.Request 33 | var b []byte 34 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 35 | tr = r 36 | b, _ = ioutil.ReadAll(r.Body) 37 | w.Header().Set("Content-Type", "application/json") 38 | w.Write([]byte("{\"results\": [{}, {\"error\": \"error_reason\"}]}")) 39 | })) 40 | defer ts.Close() 41 | 42 | ctx := context.Background() 43 | client, err := NewClient(ctx, testMessagingConfig) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | client.iidEndpoint = ts.URL + "/v1" 48 | 49 | resp, err := client.SubscribeToTopic(ctx, []string{"id1", "id2"}, "test-topic") 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | checkIIDRequest(t, b, tr, iidSubscribe) 54 | checkTopicMgtResponse(t, resp) 55 | } 56 | 57 | func TestInvalidSubscribe(t *testing.T) { 58 | ctx := context.Background() 59 | client, err := NewClient(ctx, testMessagingConfig) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | for _, tc := range invalidTopicMgtArgs { 64 | t.Run(tc.name, func(t *testing.T) { 65 | resp, err := client.SubscribeToTopic(ctx, tc.tokens, tc.topic) 66 | if err == nil || err.Error() != tc.want { 67 | t.Errorf( 68 | "SubscribeToTopic(%s) = (%#v, %v); want = (nil, %q)", tc.name, resp, err, tc.want) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func TestUnsubscribe(t *testing.T) { 75 | var tr *http.Request 76 | var b []byte 77 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 78 | tr = r 79 | b, _ = ioutil.ReadAll(r.Body) 80 | w.Header().Set("Content-Type", "application/json") 81 | w.Write([]byte("{\"results\": [{}, {\"error\": \"error_reason\"}]}")) 82 | })) 83 | defer ts.Close() 84 | 85 | ctx := context.Background() 86 | client, err := NewClient(ctx, testMessagingConfig) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | client.iidEndpoint = ts.URL + "/v1" 91 | 92 | resp, err := client.UnsubscribeFromTopic(ctx, []string{"id1", "id2"}, "test-topic") 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | checkIIDRequest(t, b, tr, iidUnsubscribe) 97 | checkTopicMgtResponse(t, resp) 98 | } 99 | 100 | func TestInvalidUnsubscribe(t *testing.T) { 101 | ctx := context.Background() 102 | client, err := NewClient(ctx, testMessagingConfig) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | for _, tc := range invalidTopicMgtArgs { 107 | t.Run(tc.name, func(t *testing.T) { 108 | resp, err := client.UnsubscribeFromTopic(ctx, tc.tokens, tc.topic) 109 | if err == nil || err.Error() != tc.want { 110 | t.Errorf( 111 | "UnsubscribeFromTopic(%s) = (%#v, %v); want = (nil, %q)", tc.name, resp, err, tc.want) 112 | } 113 | }) 114 | } 115 | } 116 | 117 | func TestTopicManagementError(t *testing.T) { 118 | var resp string 119 | var status int 120 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 121 | w.WriteHeader(status) 122 | w.Header().Set("Content-Type", "application/json") 123 | w.Write([]byte(resp)) 124 | })) 125 | defer ts.Close() 126 | 127 | ctx := context.Background() 128 | client, err := NewClient(ctx, testMessagingConfig) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | client.iidEndpoint = ts.URL + "/v1" 133 | client.iidClient.httpClient.RetryConfig = nil 134 | 135 | cases := []struct { 136 | name, resp, want string 137 | status int 138 | check func(err error) bool 139 | }{ 140 | { 141 | name: "EmptyResponse", 142 | resp: "{}", 143 | want: "unexpected http response with status: 500\n{}", 144 | status: http.StatusInternalServerError, 145 | check: errorutils.IsInternal, 146 | }, 147 | { 148 | name: "ErrorCode", 149 | resp: "{\"error\": \"INVALID_ARGUMENT\"}", 150 | want: "error while calling the iid service: INVALID_ARGUMENT", 151 | status: http.StatusBadRequest, 152 | check: errorutils.IsInvalidArgument, 153 | }, 154 | { 155 | name: "NotJson", 156 | resp: "not json", 157 | want: "unexpected http response with status: 500\nnot json", 158 | status: http.StatusInternalServerError, 159 | check: errorutils.IsInternal, 160 | }, 161 | } 162 | 163 | for _, tc := range cases { 164 | resp = tc.resp 165 | status = tc.status 166 | 167 | tmr, err := client.SubscribeToTopic(ctx, []string{"id1"}, "topic") 168 | if err == nil || err.Error() != tc.want || !tc.check(err) { 169 | t.Errorf("SubscribeToTopic(%s) = (%#v, %v); want = (nil, %q)", tc.name, tmr, err, tc.want) 170 | } 171 | 172 | tmr, err = client.UnsubscribeFromTopic(ctx, []string{"id1"}, "topic") 173 | if err == nil || err.Error() != tc.want || !tc.check(err) { 174 | t.Errorf("UnsubscribeFromTopic(%s) = (%#v, %v); want = (nil, %q)", tc.name, tmr, err, tc.want) 175 | } 176 | } 177 | } 178 | 179 | func checkIIDRequest(t *testing.T, b []byte, tr *http.Request, op string) { 180 | var parsed map[string]interface{} 181 | if err := json.Unmarshal(b, &parsed); err != nil { 182 | t.Fatal(err) 183 | } 184 | want := map[string]interface{}{ 185 | "to": "/topics/test-topic", 186 | "registration_tokens": []interface{}{"id1", "id2"}, 187 | } 188 | if !reflect.DeepEqual(parsed, want) { 189 | t.Errorf("Body = %#v; want = %#v", parsed, want) 190 | } 191 | 192 | if tr.Method != http.MethodPost { 193 | t.Errorf("Method = %q; want = %q", tr.Method, http.MethodPost) 194 | } 195 | wantOp := "/v1:" + op 196 | if tr.URL.Path != wantOp { 197 | t.Errorf("Path = %q; want = %q", tr.URL.Path, wantOp) 198 | } 199 | if h := tr.Header.Get("Authorization"); h != "Bearer test-token" { 200 | t.Errorf("Authorization = %q; want = %q", h, "Bearer test-token") 201 | } 202 | xGoogAPIClientHeader := internal.GetMetricsHeader(testMessagingConfig.Version) 203 | if h := tr.Header.Get("x-goog-api-client"); h != xGoogAPIClientHeader { 204 | t.Errorf("x-goog-api-client header = %q; want = %q", h, xGoogAPIClientHeader) 205 | } 206 | } 207 | 208 | func checkTopicMgtResponse(t *testing.T, resp *TopicManagementResponse) { 209 | if resp.SuccessCount != 1 { 210 | t.Errorf("SuccessCount = %d; want = %d", resp.SuccessCount, 1) 211 | } 212 | if resp.FailureCount != 1 { 213 | t.Errorf("FailureCount = %d; want = %d", resp.FailureCount, 1) 214 | } 215 | if len(resp.Errors) != 1 { 216 | t.Fatalf("Errors = %d; want = %d", len(resp.Errors), 1) 217 | } 218 | e := resp.Errors[0] 219 | if e.Index != 1 { 220 | t.Errorf("ErrorInfo.Index = %d; want = %d", e.Index, 1) 221 | } 222 | if e.Reason != "error_reason" { 223 | t.Errorf("ErrorInfo.Reason = %s; want = %s", e.Reason, "error_reason") 224 | } 225 | } 226 | 227 | var invalidTopicMgtArgs = []struct { 228 | name string 229 | tokens []string 230 | topic string 231 | want string 232 | }{ 233 | { 234 | name: "NoTokensAndTopic", 235 | want: "no tokens specified", 236 | }, 237 | { 238 | name: "NoTopic", 239 | tokens: []string{"token1"}, 240 | want: "topic name not specified", 241 | }, 242 | { 243 | name: "InvalidTopicName", 244 | tokens: []string{"token1"}, 245 | topic: "foo*bar", 246 | want: "invalid topic name: \"foo*bar\"", 247 | }, 248 | { 249 | name: "TooManyTokens", 250 | tokens: strings.Split("a"+strings.Repeat(",a", 1000), ","), 251 | topic: "topic", 252 | want: "tokens list must not contain more than 1000 items", 253 | }, 254 | { 255 | name: "EmptyToken", 256 | tokens: []string{"foo", ""}, 257 | topic: "topic", 258 | want: "tokens list must not contain empty strings", 259 | }, 260 | } 261 | -------------------------------------------------------------------------------- /remoteconfig/remoteconfig.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package remoteconfig provides functions to fetch and evaluate a server-side Remote Config template. 16 | package remoteconfig 17 | 18 | import ( 19 | "context" 20 | "encoding/json" 21 | "errors" 22 | "fmt" 23 | 24 | "firebase.google.com/go/v4/internal" 25 | ) 26 | 27 | const ( 28 | defaultBaseURL = "https://firebaseremoteconfig.googleapis.com" 29 | firebaseClientHeader = "X-Firebase-Client" 30 | ) 31 | 32 | // Client is the interface for the Remote Config Cloud service. 33 | type Client struct { 34 | *rcClient 35 | } 36 | 37 | // NewClient initializes a RemoteConfigClient with app-specific detail and returns a 38 | // client to be used by the user. 39 | func NewClient(ctx context.Context, c *internal.RemoteConfigClientConfig) (*Client, error) { 40 | if c.ProjectID == "" { 41 | return nil, errors.New("project ID is required to access Remote Conifg") 42 | } 43 | 44 | hc, _, err := internal.NewHTTPClient(ctx, c.Opts...) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return &Client{ 50 | rcClient: newRcClient(hc, c), 51 | }, nil 52 | } 53 | 54 | // RemoteConfigClient facilitates requests to the Firebase Remote Config backend. 55 | type rcClient struct { 56 | httpClient *internal.HTTPClient 57 | project string 58 | rcBaseURL string 59 | version string 60 | } 61 | 62 | func newRcClient(client *internal.HTTPClient, conf *internal.RemoteConfigClientConfig) *rcClient { 63 | version := fmt.Sprintf("fire-admin-go/%s", conf.Version) 64 | client.Opts = []internal.HTTPOption{ 65 | internal.WithHeader(firebaseClientHeader, version), 66 | internal.WithHeader("X-Firebase-ETag", "true"), 67 | internal.WithHeader("x-goog-api-client", internal.GetMetricsHeader(conf.Version)), 68 | } 69 | 70 | // Handles errors for non-success HTTP status codes from Remote Config servers. 71 | client.CreateErrFn = handleRemoteConfigError 72 | 73 | return &rcClient{ 74 | rcBaseURL: defaultBaseURL, 75 | project: conf.ProjectID, 76 | version: version, 77 | httpClient: client, 78 | } 79 | } 80 | 81 | // GetServerTemplate initializes a new ServerTemplate instance and fetches the server template. 82 | func (c *rcClient) GetServerTemplate(ctx context.Context, 83 | defaultConfig map[string]any) (*ServerTemplate, error) { 84 | template, err := c.InitServerTemplate(defaultConfig, "") 85 | 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | err = template.Load(ctx) 91 | return template, err 92 | } 93 | 94 | // InitServerTemplate initializes a new ServerTemplate with the default config and 95 | // an optional template data json. 96 | func (c *rcClient) InitServerTemplate(defaultConfig map[string]any, 97 | templateDataJSON string) (*ServerTemplate, error) { 98 | template, err := newServerTemplate(c, defaultConfig) 99 | 100 | if templateDataJSON != "" && err == nil { 101 | err = template.Set(templateDataJSON) 102 | } 103 | 104 | return template, err 105 | } 106 | 107 | func handleRemoteConfigError(resp *internal.Response) error { 108 | err := internal.NewFirebaseError(resp) 109 | var p struct { 110 | Error string `json:"error"` 111 | } 112 | json.Unmarshal(resp.Body, &p) 113 | if p.Error != "" { 114 | err.String = fmt.Sprintf("http error status: %d; reason: %s", resp.Status, p.Error) 115 | } 116 | 117 | return err 118 | } 119 | -------------------------------------------------------------------------------- /remoteconfig/remoteconfig_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package remoteconfig 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | 21 | "firebase.google.com/go/v4/internal" 22 | "google.golang.org/api/option" 23 | ) 24 | 25 | var ( 26 | client *Client 27 | 28 | testOpts = []option.ClientOption{ 29 | option.WithTokenSource(&internal.MockTokenSource{AccessToken: "mock-token"}), 30 | } 31 | ) 32 | 33 | // Test NewClient with valid config 34 | func TestNewClientSuccess(t *testing.T) { 35 | ctx := context.Background() 36 | config := &internal.RemoteConfigClientConfig{ 37 | ProjectID: "test-project", 38 | Opts: testOpts, 39 | Version: "1.2.3", 40 | } 41 | 42 | client, err := NewClient(ctx, config) 43 | if err != nil { 44 | t.Fatalf("NewClient failed: %v", err) 45 | } 46 | if client == nil { 47 | t.Error("NewClient returned nil client") 48 | } 49 | } 50 | 51 | // Test NewClient with missing Project ID 52 | func TestNewClientMissingProjectID(t *testing.T) { 53 | ctx := context.Background() 54 | config := &internal.RemoteConfigClientConfig{} 55 | _, err := NewClient(ctx, config) 56 | if err == nil { 57 | t.Fatal("NewClient should have failed with missing project ID") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /remoteconfig/server_config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package remoteconfig 16 | 17 | import ( 18 | "slices" 19 | "strconv" 20 | "strings" 21 | ) 22 | 23 | // ValueSource represents the source of a value. 24 | type ValueSource int 25 | 26 | // Constants for value source. 27 | const ( 28 | sourceUnspecified ValueSource = iota 29 | Static // Static represents a statically defined value. 30 | Remote // Remote represents a value fetched from a remote source. 31 | Default // Default represents a default value. 32 | ) 33 | 34 | // Value defines the interface for configuration values. 35 | type value struct { 36 | source ValueSource 37 | value string 38 | } 39 | 40 | // Default values for different parameter types. 41 | const ( 42 | DefaultValueForBoolean = false 43 | DefaultValueForString = "" 44 | DefaultValueForNumber = 0 45 | ) 46 | 47 | var booleanTruthyValues = []string{"1", "true", "t", "yes", "y", "on"} 48 | 49 | // ServerConfig is the implementation of the ServerConfig interface. 50 | type ServerConfig struct { 51 | configValues map[string]value 52 | } 53 | 54 | // NewServerConfig creates a new ServerConfig instance. 55 | func newServerConfig(configValues map[string]value) *ServerConfig { 56 | return &ServerConfig{configValues: configValues} 57 | } 58 | 59 | // GetBoolean returns the boolean value associated with the given key. 60 | // 61 | // It returns true if the string value is "1", "true", "t", "yes", "y", or "on" (case-insensitive). 62 | // Otherwise, or if the key is not found, it returns the default boolean value (false). 63 | func (s *ServerConfig) GetBoolean(key string) bool { 64 | return s.getValue(key).asBoolean() 65 | } 66 | 67 | // GetInt returns the integer value associated with the given key. 68 | // 69 | // If the parameter value cannot be parsed as an integer, or if the key is not found, 70 | // it returns the default numeric value (0). 71 | func (s *ServerConfig) GetInt(key string) int { 72 | return s.getValue(key).asInt() 73 | } 74 | 75 | // GetFloat returns the float value associated with the given key. 76 | // 77 | // If the parameter value cannot be parsed as a float64, or if the key is not found, 78 | // it returns the default float value (0). 79 | func (s *ServerConfig) GetFloat(key string) float64 { 80 | return s.getValue(key).asFloat() 81 | } 82 | 83 | // GetString returns the string value associated with the given key. 84 | // 85 | // If the key is not found, it returns the default string value (""). 86 | func (s *ServerConfig) GetString(key string) string { 87 | return s.getValue(key).asString() 88 | } 89 | 90 | // GetValueSource returns the source of the value. 91 | func (s *ServerConfig) GetValueSource(key string) ValueSource { 92 | return s.getValue(key).source 93 | } 94 | 95 | // getValue returns the value associated with the given key. 96 | func (s *ServerConfig) getValue(key string) *value { 97 | if val, ok := s.configValues[key]; ok { 98 | return &val 99 | } 100 | return newValue(Static, DefaultValueForString) 101 | } 102 | 103 | // newValue creates a new value instance. 104 | func newValue(source ValueSource, customValue string) *value { 105 | if customValue == "" { 106 | customValue = DefaultValueForString 107 | } 108 | return &value{source: source, value: customValue} 109 | } 110 | 111 | // asString returns the value as a string. 112 | func (v *value) asString() string { 113 | return v.value 114 | } 115 | 116 | // asBoolean returns the value as a boolean. 117 | func (v *value) asBoolean() bool { 118 | if v.source == Static { 119 | return DefaultValueForBoolean 120 | } 121 | 122 | return slices.Contains(booleanTruthyValues, strings.ToLower(v.value)) 123 | } 124 | 125 | // asInt returns the value as an integer. 126 | func (v *value) asInt() int { 127 | if v.source == Static { 128 | return DefaultValueForNumber 129 | } 130 | num, err := strconv.Atoi(v.value) 131 | 132 | if err != nil { 133 | return DefaultValueForNumber 134 | } 135 | 136 | return num 137 | } 138 | 139 | // asFloat returns the value as a float. 140 | func (v *value) asFloat() float64 { 141 | if v.source == Static { 142 | return DefaultValueForNumber 143 | } 144 | num, err := strconv.ParseFloat(v.value, doublePrecision) 145 | 146 | if err != nil { 147 | return DefaultValueForNumber 148 | } 149 | 150 | return num 151 | } 152 | -------------------------------------------------------------------------------- /remoteconfig/server_config_test.go: -------------------------------------------------------------------------------- 1 | package remoteconfig 2 | 3 | import "testing" 4 | 5 | type configGetterTestCase struct { 6 | name string 7 | key string 8 | expectedString string 9 | expectedInt int 10 | expectedBool bool 11 | expectedFloat float64 12 | expectedSource ValueSource 13 | } 14 | 15 | func getTestConfig() ServerConfig { 16 | config := ServerConfig{ 17 | configValues: map[string]value{ 18 | paramOne: { 19 | value: valueOne, 20 | source: Default, 21 | }, 22 | paramTwo: { 23 | value: valueTwo, 24 | source: Remote, 25 | }, 26 | paramThree: { 27 | value: valueThree, 28 | source: Default, 29 | }, 30 | paramFour: { 31 | value: valueFour, 32 | source: Remote, 33 | }, 34 | }, 35 | } 36 | return config 37 | } 38 | 39 | func TestServerConfigGetters(t *testing.T) { 40 | config := getTestConfig() 41 | testCases := []configGetterTestCase{ 42 | { 43 | name: "Parameter Value : String, Default Source", 44 | key: paramOne, 45 | expectedString: valueOne, 46 | expectedInt: 0, 47 | expectedBool: false, 48 | expectedFloat: 0, 49 | expectedSource: Default, 50 | }, 51 | { 52 | name: "Parameter Value : JSON, Remote Source", 53 | key: paramTwo, 54 | expectedString: valueTwo, 55 | expectedInt: 0, 56 | expectedBool: false, 57 | expectedFloat: 0, 58 | expectedSource: Remote, 59 | }, 60 | { 61 | name: "Unknown Parameter Value", 62 | key: "unknown_param", 63 | expectedString: "", 64 | expectedInt: 0, 65 | expectedBool: false, 66 | expectedFloat: 0, 67 | expectedSource: Static, 68 | }, 69 | { 70 | name: "Parameter Value - Float, Default Source", 71 | key: paramThree, 72 | expectedString: "123456789.123", 73 | expectedInt: 0, 74 | expectedBool: false, 75 | expectedFloat: 123456789.123, 76 | expectedSource: Default, 77 | }, 78 | { 79 | name: "Parameter Value - Boolean, Remote Source", 80 | key: paramFour, 81 | expectedString: "1", 82 | expectedInt: 1, 83 | expectedBool: true, 84 | expectedFloat: 1, 85 | expectedSource: Remote, 86 | }, 87 | } 88 | for _, tc := range testCases { 89 | t.Run(tc.name, func(t *testing.T) { 90 | if got := config.GetString(tc.key); got != tc.expectedString { 91 | t.Errorf("GetString(%q): got %q, want %q", tc.key, got, tc.expectedString) 92 | } 93 | 94 | if got := config.GetInt(tc.key); got != tc.expectedInt { 95 | t.Errorf("GetInt(%q): got %d, want %d", tc.key, got, tc.expectedInt) 96 | } 97 | 98 | if got := config.GetBoolean(tc.key); got != tc.expectedBool { 99 | t.Errorf("GetBoolean(%q): got %t, want %t", tc.key, got, tc.expectedBool) 100 | } 101 | 102 | if got := config.GetFloat(tc.key); got != tc.expectedFloat { 103 | t.Errorf("GetFloat(%q): got %f, want %f", tc.key, got, tc.expectedFloat) 104 | } 105 | 106 | if got := config.GetValueSource(tc.key); got != tc.expectedSource { 107 | t.Errorf("GetValueSource(%q): got %v, want %v", tc.key, got, tc.expectedSource) 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /remoteconfig/server_template.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package remoteconfig 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "log" 23 | "net/http" 24 | "sync/atomic" 25 | 26 | "firebase.google.com/go/v4/internal" 27 | ) 28 | 29 | // serverTemplateData stores the internal representation of the server template. 30 | type serverTemplateData struct { 31 | // A list of conditions in descending order by priority. 32 | Parameters map[string]parameter `json:"parameters,omitempty"` 33 | 34 | // Map of parameter keys to their optional default values and optional conditional values. 35 | Conditions []namedCondition `json:"conditions,omitempty"` 36 | 37 | // Version information for the current Remote Config template. 38 | Version *version `json:"version,omitempty"` 39 | 40 | // Current Remote Config template ETag. 41 | ETag string `json:"etag"` 42 | } 43 | 44 | // ServerTemplate represents a template with configuration data, cache, and service information. 45 | type ServerTemplate struct { 46 | rcClient *rcClient 47 | cache atomic.Pointer[serverTemplateData] 48 | stringifiedDefaultConfig map[string]string 49 | } 50 | 51 | // newServerTemplate initializes a new ServerTemplate with optional default configuration. 52 | func newServerTemplate(rcClient *rcClient, defaultConfig map[string]any) (*ServerTemplate, error) { 53 | stringifiedConfig := make(map[string]string, len(defaultConfig)) // Pre-allocate map 54 | 55 | for key, value := range defaultConfig { 56 | if value == nil { 57 | stringifiedConfig[key] = "" 58 | continue 59 | } 60 | 61 | if stringVal, ok := value.(string); ok { 62 | stringifiedConfig[key] = stringVal 63 | continue 64 | } 65 | 66 | // Marshal the value to JSON bytes. 67 | jsonBytes, err := json.Marshal(value) 68 | if err != nil { 69 | return nil, fmt.Errorf("unable to stringify default value for parameter '%s': %w", key, err) 70 | } 71 | 72 | stringifiedConfig[key] = string(jsonBytes) 73 | } 74 | 75 | return &ServerTemplate{ 76 | rcClient: rcClient, 77 | stringifiedDefaultConfig: stringifiedConfig, 78 | }, nil 79 | } 80 | 81 | // Load fetches the server template data from the remote config service and caches it. 82 | func (s *ServerTemplate) Load(ctx context.Context) error { 83 | request := &internal.Request{ 84 | Method: http.MethodGet, 85 | URL: fmt.Sprintf("%s/v1/projects/%s/namespaces/firebase-server/serverRemoteConfig", s.rcClient.rcBaseURL, s.rcClient.project), 86 | } 87 | 88 | templateData := new(serverTemplateData) 89 | response, err := s.rcClient.httpClient.DoAndUnmarshal(ctx, request, &templateData) 90 | 91 | if err != nil { 92 | return err 93 | } 94 | 95 | templateData.ETag = response.Header.Get("etag") 96 | s.cache.Store(templateData) 97 | return nil 98 | } 99 | 100 | // Set initializes a template using a server template JSON. 101 | func (s *ServerTemplate) Set(templateDataJSON string) error { 102 | templateData := new(serverTemplateData) 103 | if err := json.Unmarshal([]byte(templateDataJSON), &templateData); err != nil { 104 | return fmt.Errorf("error while parsing server template: %v", err) 105 | } 106 | s.cache.Store(templateData) 107 | return nil 108 | } 109 | 110 | // ToJSON returns a json representing the cached serverTemplateData. 111 | func (s *ServerTemplate) ToJSON() (string, error) { 112 | jsonServerTemplate, err := json.Marshal(s.cache.Load()) 113 | 114 | if err != nil { 115 | return "", fmt.Errorf("error while parsing server template: %v", err) 116 | } 117 | 118 | return string(jsonServerTemplate), nil 119 | } 120 | 121 | // Evaluate and processes the cached template data. 122 | func (s *ServerTemplate) Evaluate(context map[string]any) (*ServerConfig, error) { 123 | if s.cache.Load() == nil { 124 | return &ServerConfig{}, errors.New("no Remote Config Server template in Cache, call Load() before calling Evaluate()") 125 | } 126 | 127 | config := make(map[string]value) 128 | // Initialize config with in-app default values. 129 | for key, inAppDefault := range s.stringifiedDefaultConfig { 130 | config[key] = value{source: Default, value: inAppDefault} 131 | } 132 | 133 | usedConditions := s.cache.Load().filterUsedConditions() 134 | ce := conditionEvaluator{ 135 | conditions: usedConditions, 136 | evaluationContext: context, 137 | } 138 | evaluatedConditions := ce.evaluateConditions() 139 | 140 | // Overlays config value objects derived by evaluating the template. 141 | for key, parameter := range s.cache.Load().Parameters { 142 | var paramValueWrapper parameterValue 143 | var matchedConditionName string 144 | 145 | // Iterate through used conditions in decreasing priority order. 146 | for _, condition := range usedConditions { 147 | if value, ok := parameter.ConditionalValues[condition.Name]; ok && evaluatedConditions[condition.Name] { 148 | paramValueWrapper = value 149 | matchedConditionName = condition.Name 150 | break 151 | } 152 | } 153 | 154 | if paramValueWrapper.UseInAppDefault != nil && *paramValueWrapper.UseInAppDefault { 155 | log.Printf("Parameter '%s': Condition '%s' uses in-app default.\n", key, matchedConditionName) 156 | } else if paramValueWrapper.Value != nil { 157 | config[key] = value{source: Remote, value: *paramValueWrapper.Value} 158 | } else if parameter.DefaultValue.UseInAppDefault != nil && *parameter.DefaultValue.UseInAppDefault { 159 | log.Printf("Parameter '%s': Using parameter's in-app default.\n", key) 160 | } else if parameter.DefaultValue.Value != nil { 161 | config[key] = value{source: Remote, value: *parameter.DefaultValue.Value} 162 | } 163 | } 164 | return newServerConfig(config), nil 165 | } 166 | 167 | // filterUsedConditions identifies conditions that are referenced by parameters and returns them in order of decreasing priority. 168 | func (s *serverTemplateData) filterUsedConditions() []namedCondition { 169 | usedConditionNames := make(map[string]struct{}) 170 | for _, parameter := range s.Parameters { 171 | for name := range parameter.ConditionalValues { 172 | usedConditionNames[name] = struct{}{} 173 | } 174 | } 175 | 176 | // Filter the original conditions list, preserving order. 177 | conditionsToEvaluate := make([]namedCondition, 0, len(usedConditionNames)) 178 | for _, condition := range s.Conditions { 179 | if _, ok := usedConditionNames[condition.Name]; ok { 180 | conditionsToEvaluate = append(conditionsToEvaluate, condition) 181 | } 182 | } 183 | return conditionsToEvaluate 184 | } 185 | -------------------------------------------------------------------------------- /remoteconfig/server_template_types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package remoteconfig 16 | 17 | // Represents a Remote Config condition in the dataplane. 18 | // A condition targets a specific group of users. A list of these conditions 19 | // comprises part of a Remote Config template. 20 | type namedCondition struct { 21 | // A non-empty and unique name of this condition. 22 | Name string `json:"name,omitempty"` 23 | 24 | // The logic of this condition. 25 | // See the documentation on https://firebase.google.com/docs/remote-config/condition-reference 26 | // for the expected syntax of this field. 27 | Condition *oneOfCondition `json:"condition,omitempty"` 28 | } 29 | 30 | // Represents a condition that may be one of several types. 31 | // Only the first defined field will be processed. 32 | type oneOfCondition struct { 33 | // Makes this condition an OR condition. 34 | OrCondition *orCondition `json:"orCondition,omitempty"` 35 | 36 | // Makes this condition an AND condition. 37 | AndCondition *andCondition `json:"andCondition,omitempty"` 38 | 39 | // Makes this condition a percent condition. 40 | Percent *percentCondition `json:"percent,omitempty"` 41 | 42 | // Makes this condition a custom signal condition. 43 | CustomSignal *customSignalCondition `json:"customSignal,omitempty"` 44 | 45 | // Added for the purpose of testing. 46 | Boolean *bool `json:"boolean,omitempty"` 47 | } 48 | 49 | // Represents a collection of conditions that evaluate to true if any are true. 50 | type orCondition struct { 51 | Conditions []oneOfCondition `json:"conditions,omitempty"` 52 | } 53 | 54 | // Represents a collection of conditions that evaluate to true if all are true. 55 | type andCondition struct { 56 | Conditions []oneOfCondition `json:"conditions,omitempty"` 57 | } 58 | 59 | // Represents a condition that compares the instance pseudo-random percentile to a given limit. 60 | type percentCondition struct { 61 | // The choice of percent operator to determine how to compare targets to percent(s). 62 | PercentOperator string `json:"percentOperator,omitempty"` 63 | 64 | // The seed used when evaluating the hash function to map an instance to 65 | // a value in the hash space. This is a string which can have 0 - 32 66 | // characters and can contain ASCII characters [-_.0-9a-zA-Z].The string is case-sensitive. 67 | Seed string `json:"seed,omitempty"` 68 | 69 | // The limit of percentiles to target in micro-percents when 70 | // using the LESS_OR_EQUAL and GREATER_THAN operators. The value must 71 | // be in the range [0 and 100_000_000]. 72 | MicroPercent uint32 `json:"microPercent,omitempty"` 73 | 74 | // The micro-percent interval to be used with the BETWEEN operator. 75 | MicroPercentRange microPercentRange `json:"microPercentRange,omitempty"` 76 | } 77 | 78 | // Represents the limit of percentiles to target in micro-percents. 79 | // The value must be in the range [0 and 100_000_000]. 80 | type microPercentRange struct { 81 | // The lower limit of percentiles to target in micro-percents. 82 | // The value must be in the range [0 and 100_000_000]. 83 | MicroPercentLowerBound uint32 `json:"microPercentLowerBound"` 84 | 85 | // The upper limit of percentiles to target in micro-percents. 86 | // The value must be in the range [0 and 100_000_000]. 87 | MicroPercentUpperBound uint32 `json:"microPercentUpperBound"` 88 | } 89 | 90 | // Represents a condition that compares provided signals against a target value. 91 | type customSignalCondition struct { 92 | // The choice of custom signal operator to determine how to compare targets 93 | // to value(s). 94 | CustomSignalOperator string `json:"customSignalOperator,omitempty"` 95 | 96 | // The key of the signal set in the EvaluationContext. 97 | CustomSignalKey string `json:"customSignalKey,omitempty"` 98 | 99 | // A list of at most 100 target custom signal values. For numeric and semantic version operators, this will have exactly ONE target value. 100 | TargetCustomSignalValues []string `json:"targetCustomSignalValues,omitempty"` 101 | } 102 | 103 | // Structure representing a Remote Config parameter. 104 | // At minimum, a `defaultValue` or a `conditionalValues` entry must be present for the parameter to have any effect. 105 | type parameter struct { 106 | // The value to set the parameter to, when none of the named conditions evaluate to `true`. 107 | DefaultValue parameterValue `json:"defaultValue,omitempty"` 108 | 109 | // A `(condition name, value)` map. The condition name of the highest priority 110 | // (the one listed first in the Remote Config template's conditions list) determines the value of this parameter. 111 | ConditionalValues map[string]parameterValue `json:"conditionalValues,omitempty"` 112 | 113 | // A description for this parameter. Should not be over 100 characters and may contain any Unicode characters. 114 | Description string `json:"description,omitempty"` 115 | 116 | // The data type for all values of this parameter in the current version of the template. 117 | // It can be a string, number, boolean or JSON, and defaults to type string if unspecified. 118 | ValueType string `json:"valueType,omitempty"` 119 | } 120 | 121 | // Represents a Remote Config parameter value 122 | // that could be either an explicit parameter value or an in-app default value. 123 | type parameterValue struct { 124 | // The `string` value that the parameter is set to when it is an explicit parameter value. 125 | Value *string `json:"value,omitempty"` 126 | 127 | // If true, indicates that the in-app default value is to be used for the parameter. 128 | UseInAppDefault *bool `json:"useInAppDefault,omitempty"` 129 | } 130 | 131 | // Structure representing a Remote Config template version. 132 | // Output only, except for the version description. Contains metadata about a particular 133 | // version of the Remote Config template. All fields are set at the time the specified Remote Config template is published. 134 | type version struct { 135 | // The version number of a Remote Config template. 136 | VersionNumber string `json:"versionNumber,omitempty"` 137 | 138 | // The timestamp of when this version of the Remote Config template was written to the 139 | // Remote Config backend. 140 | UpdateTime string `json:"updateTime,omitempty"` 141 | 142 | // The origin of the template update action. 143 | UpdateOrigin string `json:"updateOrigin,omitempty"` 144 | 145 | // The type of the template update action. 146 | UpdateType string `json:"updateType,omitempty"` 147 | 148 | // Aggregation of all metadata fields about the account that performed the update. 149 | UpdateUser *remoteConfigUser `json:"updateUser,omitempty"` 150 | 151 | // The user-provided description of the corresponding Remote Config template. 152 | Description string `json:"description,omitempty"` 153 | 154 | // The version number of the Remote Config template that has become the current version 155 | // due to a rollback. Only present if this version is the result of a rollback. 156 | RollbackSource string `json:"rollbackSource,omitempty"` 157 | 158 | // Indicates whether this Remote Config template was published before version history was supported. 159 | IsLegacy bool `json:"isLegacy,omitempty"` 160 | } 161 | 162 | // Represents a Remote Config user. 163 | type remoteConfigUser struct { 164 | // Email address. Output only. 165 | Email string `json:"email,omitempty"` 166 | 167 | // Display name. Output only. 168 | Name string `json:"name,omitempty"` 169 | 170 | // Image URL. Output only. 171 | ImageURL string `json:"imageUrl,omitempty"` 172 | } 173 | -------------------------------------------------------------------------------- /snippets/init.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package snippets 16 | 17 | // [START admin_import_golang] 18 | import ( 19 | "context" 20 | "log" 21 | 22 | firebase "firebase.google.com/go/v4" 23 | "firebase.google.com/go/v4/auth" 24 | "google.golang.org/api/option" 25 | ) 26 | 27 | // [END admin_import_golang] 28 | 29 | // ================================================================== 30 | // https://firebase.google.com/docs/admin/setup 31 | // ================================================================== 32 | 33 | func initializeAppWithServiceAccount() *firebase.App { 34 | // [START initialize_app_service_account_golang] 35 | opt := option.WithCredentialsFile("path/to/serviceAccountKey.json") 36 | app, err := firebase.NewApp(context.Background(), nil, opt) 37 | if err != nil { 38 | log.Fatalf("error initializing app: %v\n", err) 39 | } 40 | // [END initialize_app_service_account_golang] 41 | 42 | return app 43 | } 44 | 45 | func initializeAppWithRefreshToken() *firebase.App { 46 | // [START initialize_app_refresh_token_golang] 47 | opt := option.WithCredentialsFile("path/to/refreshToken.json") 48 | config := &firebase.Config{ProjectID: "my-project-id"} 49 | app, err := firebase.NewApp(context.Background(), config, opt) 50 | if err != nil { 51 | log.Fatalf("error initializing app: %v\n", err) 52 | } 53 | // [END initialize_app_refresh_token_golang] 54 | 55 | return app 56 | } 57 | 58 | func initializeAppDefault() *firebase.App { 59 | // [START initialize_app_default_golang] 60 | app, err := firebase.NewApp(context.Background(), nil) 61 | if err != nil { 62 | log.Fatalf("error initializing app: %v\n", err) 63 | } 64 | // [END initialize_app_default_golang] 65 | 66 | return app 67 | } 68 | 69 | func initializeServiceAccountID() *firebase.App { 70 | // [START initialize_sdk_with_service_account_id] 71 | conf := &firebase.Config{ 72 | ServiceAccountID: "my-client-id@my-project-id.iam.gserviceaccount.com", 73 | } 74 | app, err := firebase.NewApp(context.Background(), conf) 75 | if err != nil { 76 | log.Fatalf("error initializing app: %v\n", err) 77 | } 78 | // [END initialize_sdk_with_service_account_id] 79 | return app 80 | } 81 | 82 | func accessServicesSingleApp() (*auth.Client, error) { 83 | // [START access_services_single_app_golang] 84 | // Initialize default app 85 | app, err := firebase.NewApp(context.Background(), nil) 86 | if err != nil { 87 | log.Fatalf("error initializing app: %v\n", err) 88 | } 89 | 90 | // Access auth service from the default app 91 | client, err := app.Auth(context.Background()) 92 | if err != nil { 93 | log.Fatalf("error getting Auth client: %v\n", err) 94 | } 95 | // [END access_services_single_app_golang] 96 | 97 | return client, err 98 | } 99 | 100 | func accessServicesMultipleApp() (*auth.Client, error) { 101 | // [START access_services_multiple_app_golang] 102 | // Initialize the default app 103 | defaultApp, err := firebase.NewApp(context.Background(), nil) 104 | if err != nil { 105 | log.Fatalf("error initializing app: %v\n", err) 106 | } 107 | 108 | // Initialize another app with a different config 109 | opt := option.WithCredentialsFile("service-account-other.json") 110 | otherApp, err := firebase.NewApp(context.Background(), nil, opt) 111 | if err != nil { 112 | log.Fatalf("error initializing app: %v\n", err) 113 | } 114 | 115 | // Access Auth service from default app 116 | defaultClient, err := defaultApp.Auth(context.Background()) 117 | if err != nil { 118 | log.Fatalf("error getting Auth client: %v\n", err) 119 | } 120 | 121 | // Access auth service from other app 122 | otherClient, err := otherApp.Auth(context.Background()) 123 | if err != nil { 124 | log.Fatalf("error getting Auth client: %v\n", err) 125 | } 126 | // [END access_services_multiple_app_golang] 127 | // Avoid unused 128 | _ = defaultClient 129 | return otherClient, nil 130 | } 131 | -------------------------------------------------------------------------------- /snippets/storage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package snippets 16 | 17 | import ( 18 | "context" 19 | "log" 20 | 21 | firebase "firebase.google.com/go/v4" 22 | "google.golang.org/api/option" 23 | ) 24 | 25 | // ================================================================== 26 | // https://firebase.google.com/docs/storage/admin/start 27 | // ================================================================== 28 | 29 | func cloudStorage() { 30 | // [START cloud_storage_golang] 31 | config := &firebase.Config{ 32 | StorageBucket: ".appspot.com", 33 | } 34 | opt := option.WithCredentialsFile("path/to/serviceAccountKey.json") 35 | app, err := firebase.NewApp(context.Background(), config, opt) 36 | if err != nil { 37 | log.Fatalln(err) 38 | } 39 | 40 | client, err := app.Storage(context.Background()) 41 | if err != nil { 42 | log.Fatalln(err) 43 | } 44 | 45 | bucket, err := client.DefaultBucket() 46 | if err != nil { 47 | log.Fatalln(err) 48 | } 49 | // 'bucket' is an object defined in the cloud.google.com/go/storage package. 50 | // See https://godoc.org/cloud.google.com/go/storage#BucketHandle 51 | // for more details. 52 | // [END cloud_storage_golang] 53 | 54 | log.Printf("Created bucket handle: %v\n", bucket) 55 | } 56 | 57 | func cloudStorageCustomBucket(app *firebase.App) { 58 | client, err := app.Storage(context.Background()) 59 | if err != nil { 60 | log.Fatalln(err) 61 | } 62 | 63 | // [START cloud_storage_custom_bucket_golang] 64 | bucket, err := client.Bucket("my-custom-bucket") 65 | // [END cloud_storage_custom_bucket_golang] 66 | if err != nil { 67 | log.Fatalln(err) 68 | } 69 | log.Printf("Created bucket handle: %v\n", bucket) 70 | } 71 | -------------------------------------------------------------------------------- /storage/storage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package storage provides functions for accessing Google Cloud Storge buckets. 16 | package storage 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "os" 22 | 23 | "cloud.google.com/go/storage" 24 | "firebase.google.com/go/v4/internal" 25 | ) 26 | 27 | // Client is the interface for the Firebase Storage service. 28 | type Client struct { 29 | client *storage.Client 30 | bucket string 31 | } 32 | 33 | // NewClient creates a new instance of the Firebase Storage Client. 34 | // 35 | // This function can only be invoked from within the SDK. Client applications should access the 36 | // the Storage service through firebase.App. 37 | func NewClient(ctx context.Context, c *internal.StorageConfig) (*Client, error) { 38 | if os.Getenv("STORAGE_EMULATOR_HOST") == "" && os.Getenv("FIREBASE_STORAGE_EMULATOR_HOST") != "" { 39 | os.Setenv("STORAGE_EMULATOR_HOST", os.Getenv("FIREBASE_STORAGE_EMULATOR_HOST")) 40 | } 41 | client, err := storage.NewClient(ctx, c.Opts...) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return &Client{client: client, bucket: c.Bucket}, nil 46 | } 47 | 48 | // DefaultBucket returns a handle to the default Cloud Storage bucket. 49 | // 50 | // To use this method, the default bucket name must be specified via firebase.Config when 51 | // initializing the App. 52 | func (c *Client) DefaultBucket() (*storage.BucketHandle, error) { 53 | return c.Bucket(c.bucket) 54 | } 55 | 56 | // Bucket returns a handle to the specified Cloud Storage bucket. 57 | func (c *Client) Bucket(name string) (*storage.BucketHandle, error) { 58 | if name == "" { 59 | return nil, errors.New("bucket name not specified") 60 | } 61 | return c.client.Bucket(name), nil 62 | } 63 | -------------------------------------------------------------------------------- /storage/storage_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package storage 16 | 17 | import ( 18 | "context" 19 | "os" 20 | "testing" 21 | 22 | "firebase.google.com/go/v4/internal" 23 | "google.golang.org/api/option" 24 | ) 25 | 26 | var opts = []option.ClientOption{ 27 | option.WithCredentialsFile("../testdata/service_account.json"), 28 | } 29 | 30 | func TestNewClientError(t *testing.T) { 31 | invalid := []option.ClientOption{ 32 | option.WithCredentialsFile("../testdata/non_existing.json"), 33 | } 34 | client, err := NewClient(context.Background(), &internal.StorageConfig{ 35 | Opts: invalid, 36 | }) 37 | if client != nil || err == nil { 38 | t.Errorf("NewClient() = (%v, %v); want (nil, error)", client, err) 39 | } 40 | } 41 | 42 | func TestNewClientEmulatorHostEnvVar(t *testing.T) { 43 | emulatorHost := "localhost:9099" 44 | os.Setenv("FIREBASE_STORAGE_EMULATOR_HOST", emulatorHost) 45 | defer os.Unsetenv("FIREBASE_STORAGE_EMULATOR_HOST") 46 | os.Unsetenv("STORAGE_EMULATOR_HOST") 47 | defer os.Unsetenv("STORAGE_EMULATOR_HOST") 48 | 49 | _, err := NewClient(context.Background(), &internal.StorageConfig{ 50 | Opts: opts, 51 | }) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | if host := os.Getenv("STORAGE_EMULATOR_HOST"); host != emulatorHost { 57 | t.Errorf("emulator host: %q; want: %q", host, emulatorHost) 58 | } 59 | } 60 | 61 | func TestNoBucketName(t *testing.T) { 62 | client, err := NewClient(context.Background(), &internal.StorageConfig{ 63 | Opts: opts, 64 | }) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | if _, err := client.DefaultBucket(); err == nil { 69 | t.Errorf("DefaultBucket() = nil; want error") 70 | } 71 | } 72 | 73 | func TestEmptyBucketName(t *testing.T) { 74 | client, err := NewClient(context.Background(), &internal.StorageConfig{ 75 | Opts: opts, 76 | }) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | if _, err := client.Bucket(""); err == nil { 81 | t.Errorf("Bucket('') = nil; want error") 82 | } 83 | } 84 | 85 | func TestDefaultBucket(t *testing.T) { 86 | client, err := NewClient(context.Background(), &internal.StorageConfig{ 87 | Bucket: "bucket.name", 88 | Opts: opts, 89 | }) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | bucket, err := client.DefaultBucket() 94 | if bucket == nil || err != nil { 95 | t.Errorf("DefaultBucket() = (%v, %v); want: (bucket, nil)", bucket, err) 96 | } 97 | } 98 | 99 | func TestBucket(t *testing.T) { 100 | client, err := NewClient(context.Background(), &internal.StorageConfig{ 101 | Opts: opts, 102 | }) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | bucket, err := client.Bucket("bucket.name") 107 | if bucket == nil || err != nil { 108 | t.Errorf("Bucket() = (%v, %v); want: (bucket, nil)", bucket, err) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /testdata/appcheck_pk.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEArFYQyEdjj43mnpXwj+3WgAE01TSYe1+XFE9mxUDShysFwtVZ 3 | OHFSMm6kl+B3Y/O8NcPt5osntLlH6KHvygExAE0tDmFYq8aKt7LQQF8rTv0rI6MP 4 | 92ezyCEp4MPmAPFD/tY160XGrkqApuY2/+L8eEXdkRyH2H7lCYypFC0u3DIY25Vl 5 | q+ZDkxB2kGykGgb1zVazCDDViqV1p9hSltmm4el9AyF08FsMCpk/NvwKOY4pJ/sm 6 | 99CDKxMhQBaT9lrIQt0B1VqTpEwlOoiFiyXASRXp9ZTeL4mrLPqSeozwPvspD81w 7 | bgecd62F640scKBr3ko73L8M8UWcwgd+moKCJwIDAQABAoIBAEDPJQSMhE6KKL5e 8 | 2NbntJDy4zGC1A0hh6llqtpnZETc0w/QN/tX8ndw0IklKwD1ukPl6OOYVVhLjVVZ 9 | ANpQ1GKuo1ETHsuKoMQwhMyQfbL41m5SdkCuSRfsENmsEiUslkuRtzlBRlRpRDR/ 10 | wxM8A4IflBFsT1IFdpC+yx8BVuwLc35iVnaGQpo/jhSDibt07j+FdOKEWkMGj+rL 11 | sHC6cpB2NMTBl9CIDLW/eq1amBOAGtsSKqoGJvaQY/mZf7SPkRjYIfIl2PWSaduT 12 | fmMrsYYFtHUKVOMYAD7P5RWNkS8oERucnXT3ouAECvip3Ew2JqlQc0FP7FS5CxH3 13 | WdfvLuECgYEA8Q7rJrDOdO867s7P/lXMklbAGnuNnAZJdAEXUMIaPJi7al97F119 14 | 4DKBuF7c/dDf8CdiOvMzP8r/F8+FFx2D61xxkQNeuxo5Xjlt23OzW5EI2S6ABesZ 15 | /3sQWqvKCGuqN7WENYF3EiKyByQ22MYXk8CE7KZuO57Aj88t6TsaNhkCgYEAtwSs 16 | hbqKSCneC1bQ3wfSAF2kPYRrQEEa2VCLlX1Mz7zHufxksUWAnAbU8O3hIGnXjz6T 17 | qzivyJJhFSgNGeYpwV67GfXnibpr3OZ/yx2YXIQfp0daivj++kvEU7aNfM9rHZA9 18 | S3Gh7hKELdB9b0DkrX5GpLiZWA6NnJdrIRYbAj8CgYBCZSyJvJsxBA+EZTxOvk0Z 19 | ZYGGCc/oUKb8p6xHVx8o35yHYQMjXWHlVaP7J03RLy3vFLnuqLvN71ixszviMQP7 20 | 2LuDCJ2YBVIVzNWgY07cgqcgQrmKZ8YCY2AOyVBdX2JD8+AVaLJmMV49r1DYBj/K 21 | N3WlRPYJv+Ej+xmXKus+SQKBgHh/Zkthxxu+HQigL0M4teYxwSoTnj2e39uGsXBK 22 | ICGCLIniiDVDCmswAFFkfV3G8frI+5a26t2Gqs6wIPgVVxaOlWeBROGkUNIPHMKR 23 | iLgY8XJEg3OOfuoyql9niP5M3jyHtCOQ/Elv/YDgjUWLl0Q3KLHZLHUSl+AqvYj6 24 | MewnAoGBANgYzPZgP+wreI55BFR470blKh1mFz+YGa+53DCd7JdMH2pdp4hoh303 25 | XxpOSVlAuyv9SgTsZ7WjGO5UdhaBzVPKgN0OO6JQmQ5ZrOR8ZJ7VB73FiVHCEerj 26 | 1m2zyFv6OT7vqdg+V1/SzxMEmXXFQv1g69k6nWGazne3IJlzrSpj 27 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /testdata/dinosaurs.json: -------------------------------------------------------------------------------- 1 | { 2 | "dinosaurs": { 3 | "bruhathkayosaurus": { 4 | "appeared": -70000000, 5 | "height": 25, 6 | "length": 44, 7 | "order": "saurischia", 8 | "vanished": -70000000, 9 | "weight": 135000, 10 | "ratings": { 11 | "pos": 1 12 | } 13 | }, 14 | "lambeosaurus": { 15 | "appeared": -76000000, 16 | "height": 2.1, 17 | "length": 12.5, 18 | "order": "ornithischia", 19 | "vanished": -75000000, 20 | "weight": 5000, 21 | "ratings": { 22 | "pos": 2 23 | } 24 | }, 25 | "linhenykus": { 26 | "appeared": -85000000, 27 | "height": 0.6, 28 | "length": 1, 29 | "order": "theropoda", 30 | "vanished": -75000000, 31 | "weight": 3, 32 | "ratings": { 33 | "pos": 3 34 | } 35 | }, 36 | "pterodactyl": { 37 | "appeared": -150000000, 38 | "height": 0.6, 39 | "length": 0.8, 40 | "order": "pterosauria", 41 | "vanished": -148500000, 42 | "weight": 2, 43 | "ratings": { 44 | "pos": 4 45 | } 46 | }, 47 | "stegosaurus": { 48 | "appeared": -155000000, 49 | "height": 4, 50 | "length": 9, 51 | "order": "ornithischia", 52 | "vanished": -150000000, 53 | "weight": 2500, 54 | "ratings": { 55 | "pos": 5 56 | } 57 | }, 58 | "triceratops": { 59 | "appeared": -68000000, 60 | "height": 3, 61 | "length": 8, 62 | "order": "ornithischia", 63 | "vanished": -66000000, 64 | "weight": 11000, 65 | "ratings": { 66 | "pos": 6 67 | } 68 | } 69 | }, 70 | "scores": { 71 | "bruhathkayosaurus": 55, 72 | "lambeosaurus": 21, 73 | "linhenykus": 80, 74 | "pterodactyl": 93, 75 | "stegosaurus": 5, 76 | "triceratops": 22 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /testdata/dinosaurs_index.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "_adminsdk": { 4 | "go": { 5 | "dinodb": { 6 | "dinosaurs": { 7 | ".indexOn": ["height", "ratings/pos"] 8 | }, 9 | "scores": { 10 | ".indexOn": ".value" 11 | } 12 | }, 13 | "protected": { 14 | "$uid": { 15 | ".read": "auth != null", 16 | ".write": "$uid === auth.uid" 17 | } 18 | }, 19 | "admin": { 20 | ".read": "false", 21 | ".write": "false" 22 | }, 23 | "public": { 24 | ".read": "true" 25 | } 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /testdata/firebase_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "databaseURL": "https://auto-init.database.url", 3 | "projectId": "auto-init-project-id", 4 | "storageBucket": "auto-init.storage.bucket" 5 | } 6 | -------------------------------------------------------------------------------- /testdata/firebase_config_empty.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/firebase-admin-go/d515faf47673ae79005d4b0abceca74716a5ac92/testdata/firebase_config_empty.json -------------------------------------------------------------------------------- /testdata/firebase_config_invalid.json: -------------------------------------------------------------------------------- 1 | baaad 2 | -------------------------------------------------------------------------------- /testdata/firebase_config_invalid_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "project1d_bad_key": "auto-init-project-id", 3 | "storageBucket": "auto-init.storage.bucket" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/firebase_config_partial.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": "auto-init-project-id" 3 | } 4 | -------------------------------------------------------------------------------- /testdata/get_disabled_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "identitytoolkit#GetAccountInfoResponse", 3 | "users": [ 4 | { 5 | "localId": "testuser", 6 | "email": "testuser@example.com", 7 | "phoneNumber": "+1234567890", 8 | "emailVerified": true, 9 | "displayName": "Test User", 10 | "photoUrl": "http://www.example.com/testuser/photo.png", 11 | "passwordHash": "passwordhash", 12 | "salt": "salt===", 13 | "passwordUpdatedAt": 1.494364393E+12, 14 | "validSince": "1494364393", 15 | "disabled": true, 16 | "createdAt": "1234567890000", 17 | "lastLoginAt": "1233211232000", 18 | "customAttributes": "{\"admin\": true, \"package\": \"gold\"}", 19 | "tenantId": "testTenant" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /testdata/get_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "identitytoolkit#GetAccountInfoResponse", 3 | "users": [ 4 | { 5 | "localId": "testuser", 6 | "email": "testuser@example.com", 7 | "phoneNumber": "+1234567890", 8 | "emailVerified": true, 9 | "displayName": "Test User", 10 | "providerUserInfo": [ 11 | { 12 | "providerId": "password", 13 | "displayName": "Test User", 14 | "photoUrl": "http://www.example.com/testuser/photo.png", 15 | "federatedId": "testuser@example.com", 16 | "email": "testuser@example.com", 17 | "rawId": "testuid" 18 | }, 19 | { 20 | "providerId": "phone", 21 | "phoneNumber": "+1234567890", 22 | "rawId": "testuid" 23 | } 24 | ], 25 | "photoUrl": "http://www.example.com/testuser/photo.png", 26 | "passwordHash": "passwordhash", 27 | "salt": "salt===", 28 | "passwordUpdatedAt": 1.494364393E+12, 29 | "validSince": "1494364393", 30 | "disabled": false, 31 | "createdAt": "1234567890000", 32 | "lastLoginAt": "1233211232000", 33 | "customAttributes": "{\"admin\": true, \"package\": \"gold\"}", 34 | "tenantId": "testTenant", 35 | "mfaInfo": [ 36 | { 37 | "phoneInfo": "+1234567890", 38 | "mfaEnrollmentId": "enrolledPhoneFactor", 39 | "displayName": "My MFA Phone", 40 | "enrolledAt": "2021-03-03T13:06:20.542896Z" 41 | }, 42 | { 43 | "totpInfo": {}, 44 | "mfaEnrollmentId": "enrolledTOTPFactor", 45 | "displayName": "My MFA TOTP", 46 | "enrolledAt": "2021-03-03T13:06:20.542896Z" 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /testdata/invalid_service_account.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "mock-project-id", 4 | "private_key_id": "mock-key-id-1", 5 | "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwJENcRev+eXZKvhhWLiV3Lz2MvO+naQRHo59g3vaNQnbgyduN/L4krlr\nJ5c6FiikXdtJNb/QrsAHSyJWCu8j3T9CruiwbidGAk2W0RuViTVspjHUTsIHExx9euWM0Uom\nGvYkoqXahdhPL/zViVSJt+Rt8bHLsMvpb8RquTIb9iKY3SMV2tCofNmyCSgVbghq/y7lKORt\nV/IRguWs6R22fbkb0r2MCYoNAbZ9dqnbRIFNZBC7itYtUoTEresRWcyFMh0zfAIJycWOJlVL\nDLqkY2SmIx8u7fuysCg1wcoSZoStuDq02nZEMw1dx8HGzE0hynpHlloRLByuIuOAfMCCYwID\nAQABAoIBADFtihu7TspAO0wSUTpqttzgC/nsIsNn95T2UjVLtyjiDNxPZLUrwq42tdCFur0x\nVW9Z+CK5x6DzXWvltlw8IeKKeF1ZEOBVaFzy+YFXKTz835SROcO1fgdjyrme7lRSShGlmKW/\nGKY+baUNquoDLw5qreXaE0SgMp0jt5ktyYuVxvhLDeV4omw2u6waoGkifsGm8lYivg5l3VR7\nw2IVOvYZTt4BuSYVwOM+qjwaS1vtL7gv0SUjrj85Ja6zERRdFiITDhZw6nsvacr9/+/aut9E\naL/koSSb62g5fntQMEwoT4hRnjPnAedmorM9Rhddh2TB3ZKTBbMN1tUk3fJxOuECgYEA+z6l\neSaAcZ3qvwpntcXSpwwJ0SSmzLTH2RJNf+Ld3eBHiSvLTG53dWB7lJtF4R1KcIwf+KGcOFJv\nsnepzcZBylRvT8RrAAkV0s9OiVm1lXZyaepbLg4GGFJBPi8A6VIAj7zYknToRApdW0s1x/XX\nChewfJDckqsevTMovdbg8YkCgYEAxDYX+3mfvv/opo6HNNY3SfVunM+4vVJL+n8gWZ2w9kz3\nQ9Ub9YbRmI7iQaiVkO5xNuoG1n9bM+3Mnm84aQ1YeNT01YqeyQsipP5Wi+um0PzYTaBw9RO+\n8Gh6992OwlJiRtFk5WjalNWOxY4MU0ImnJwIfKQlUODvLmcixm68NYsCgYEAuAqI3jkk55Vd\nKvotREsX5wP7gPePM+7NYiZ1HNQL4Ab1f/bTojZdTV8Sx6YCR0fUiqMqnE+OBvfkGGBtw22S\nLesx6sWf99Ov58+x4Q0U5dpxL0Lb7d2Z+2Dtp+Z4jXFjNeeI4ae/qG/LOR/b0pE0J5F415ap\n7Mpq5v89vepUtrkCgYAjMXytu4v+q1Ikhc4UmRPDrUUQ1WVSd+9u19yKlnFGTFnRjej86hiw\nH3jPxBhHra0a53EgiilmsBGSnWpl1WH4EmJz5vBCKUAmjgQiBrueIqv9iHiaTNdjsanUyaWw\njyxXfXl2eI80QPXh02+8g1H/pzESgjK7Rg1AqnkfVH9nrwKBgQDJVxKBPTw9pigYMVt9iHrR\niCl9zQVjRMbWiPOc0J56+/5FZYm/AOGl9rfhQ9vGxXZYZiOP5FsNkwt05Y1UoAAH4B4VQwbL\nqod71qOcI0ywgZiIR87CYw40gzRfjWnN+YEEW1qfyoNLilEwJB8iB/T+ZePHGmJ4MmQ/cTn9\nxpdLXA==\n-----END RSA PRIVATE KEY-----", 6 | "client_id": "1234567890", 7 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 8 | "token_uri": "https://accounts.google.com/o/oauth2/token", 9 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 10 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/mock-project-id.iam.gserviceaccount.com" 11 | } 12 | -------------------------------------------------------------------------------- /testdata/list_users.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "identitytoolkit#DownloadAccountResponse", 3 | "users": [ 4 | { 5 | "localId": "testuser", 6 | "email": "testuser@example.com", 7 | "phoneNumber": "+1234567890", 8 | "emailVerified": true, 9 | "displayName": "Test User", 10 | "providerUserInfo": [ 11 | { 12 | "providerId": "password", 13 | "displayName": "Test User", 14 | "photoUrl": "http://www.example.com/testuser/photo.png", 15 | "federatedId": "testuser@example.com", 16 | "email": "testuser@example.com", 17 | "rawId": "testuid" 18 | }, 19 | { 20 | "providerId": "phone", 21 | "phoneNumber": "+1234567890", 22 | "rawId": "testuid" 23 | } 24 | ], 25 | "photoUrl": "http://www.example.com/testuser/photo.png", 26 | "passwordHash": "passwordhash1", 27 | "salt": "salt1", 28 | "passwordUpdatedAt": 1.494364393E+12, 29 | "validSince": "1494364393", 30 | "disabled": false, 31 | "createdAt": "1234567890000", 32 | "lastLoginAt": "1233211232000", 33 | "customAttributes": "{\"admin\": true, \"package\": \"gold\"}", 34 | "tenantId": "testTenant", 35 | "mfaInfo": [ 36 | { 37 | "phoneInfo": "+1234567890", 38 | "mfaEnrollmentId": "enrolledPhoneFactor", 39 | "displayName": "My MFA Phone", 40 | "enrolledAt": "2021-03-03T13:06:20.542896Z" 41 | }, 42 | { 43 | "totpInfo": {}, 44 | "mfaEnrollmentId": "enrolledTOTPFactor", 45 | "displayName": "My MFA TOTP", 46 | "enrolledAt": "2021-03-03T13:06:20.542896Z" 47 | } 48 | ] 49 | }, 50 | { 51 | "localId": "testuser", 52 | "email": "testuser@example.com", 53 | "phoneNumber": "+1234567890", 54 | "emailVerified": true, 55 | "displayName": "Test User", 56 | "providerUserInfo": [ 57 | { 58 | "providerId": "password", 59 | "displayName": "Test User", 60 | "photoUrl": "http://www.example.com/testuser/photo.png", 61 | "federatedId": "testuser@example.com", 62 | "email": "testuser@example.com", 63 | "rawId": "testuid" 64 | }, 65 | { 66 | "providerId": "phone", 67 | "phoneNumber": "+1234567890", 68 | "rawId": "testuid" 69 | } 70 | ], 71 | "photoUrl": "http://www.example.com/testuser/photo.png", 72 | "passwordHash": "passwordhash2", 73 | "salt": "salt2", 74 | "passwordUpdatedAt": 1.494364393E+12, 75 | "validSince": "1494364393", 76 | "disabled": false, 77 | "createdAt": "1234567890000", 78 | "lastLoginAt": "1233211232000", 79 | "customAttributes": "{\"admin\": true, \"package\": \"gold\"}", 80 | "tenantId": "testTenant", 81 | "mfaInfo": [ 82 | { 83 | "phoneInfo": "+1234567890", 84 | "mfaEnrollmentId": "enrolledPhoneFactor", 85 | "displayName": "My MFA Phone", 86 | "enrolledAt": "2021-03-03T13:06:20.542896Z" 87 | }, 88 | { 89 | "totpInfo": {}, 90 | "mfaEnrollmentId": "enrolledTOTPFactor", 91 | "displayName": "My MFA TOTP", 92 | "enrolledAt": "2021-03-03T13:06:20.542896Z" 93 | } 94 | ] 95 | }, 96 | { 97 | "localId": "testusernomfa", 98 | "email": "testusernomfa@example.com", 99 | "phoneNumber": "+1234567890", 100 | "emailVerified": true, 101 | "displayName": "Test User Without MFA", 102 | "providerUserInfo": [ 103 | { 104 | "providerId": "password", 105 | "displayName": "Test User Without MFA", 106 | "photoUrl": "http://www.example.com/testusernomfa/photo.png", 107 | "federatedId": "testusernomfa@example.com", 108 | "email": "testusernomfa@example.com", 109 | "rawId": "testuid" 110 | }, 111 | { 112 | "providerId": "phone", 113 | "phoneNumber": "+1234567890", 114 | "rawId": "testuid" 115 | } 116 | ], 117 | "photoUrl": "http://www.example.com/testusernomfa/photo.png", 118 | "passwordHash": "passwordhash3", 119 | "salt": "salt3", 120 | "passwordUpdatedAt": 1.494364393E+12, 121 | "validSince": "1494364393", 122 | "disabled": false, 123 | "createdAt": "1234567890000", 124 | "lastLoginAt": "1233211232000", 125 | "customAttributes": "{\"admin\": true, \"package\": \"gold\"}", 126 | "tenantId": "testTenant" 127 | } 128 | ], 129 | "nextPageToken": "" 130 | } 131 | -------------------------------------------------------------------------------- /testdata/mock.jwks.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "RSA", 5 | "e": "AQAB", 6 | "use": "sig", 7 | "kid": "FGQdnRlzAmKyKr6-Hg_kMQrBkj_H6i6ADnBQz4OI6BU", 8 | "alg": "RS256", 9 | "n": "rFYQyEdjj43mnpXwj-3WgAE01TSYe1-XFE9mxUDShysFwtVZOHFSMm6kl-B3Y_O8NcPt5osntLlH6KHvygExAE0tDmFYq8aKt7LQQF8rTv0rI6MP92ezyCEp4MPmAPFD_tY160XGrkqApuY2_-L8eEXdkRyH2H7lCYypFC0u3DIY25Vlq-ZDkxB2kGykGgb1zVazCDDViqV1p9hSltmm4el9AyF08FsMCpk_NvwKOY4pJ_sm99CDKxMhQBaT9lrIQt0B1VqTpEwlOoiFiyXASRXp9ZTeL4mrLPqSeozwPvspD81wbgecd62F640scKBr3ko73L8M8UWcwgd-moKCJw" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /testdata/plain_text.txt: -------------------------------------------------------------------------------- 1 | Test 2 | -------------------------------------------------------------------------------- /testdata/public_certs.json: -------------------------------------------------------------------------------- 1 | { 2 | "mock-key-id-1": "-----BEGIN CERTIFICATE-----\nMIIEFTCCAv2gAwIBAgIJALLYfi2oN8cPMA0GCSqGSIb3DQEBCwUAMIGgMQswCQYD\nVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxDzAN\nBgNVBAoMBkdvb2dsZTERMA8GA1UECwwIRmlyZWJhc2UxHDAaBgNVBAMME2ZpcmVi\nYXNlLmdvb2dsZS5jb20xKjAoBgkqhkiG9w0BCQEWG3N1cHBvcnRAZmlyZWJhc2Uu\nZ29vZ2xlLmNvbTAeFw0xNzAzMjIwMDM4MzRaFw0yNzAzMjAwMDM4MzRaMIGgMQsw\nCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcx\nDzANBgNVBAoMBkdvb2dsZTERMA8GA1UECwwIRmlyZWJhc2UxHDAaBgNVBAMME2Zp\ncmViYXNlLmdvb2dsZS5jb20xKjAoBgkqhkiG9w0BCQEWG3N1cHBvcnRAZmlyZWJh\nc2UuZ29vZ2xlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMCR\nDXEXr/nl2Sr4YVi4ldy89jLzvp2kER6OfYN72jUJ24Mnbjfy+JK5ayeXOhYopF3b\nSTW/0K7AB0siVgrvI90/Qq7osG4nRgJNltEblYk1bKYx1E7CBxMcfXrljNFKJhr2\nJKKl2oXYTy/81YlUibfkbfGxy7DL6W/EarkyG/YimN0jFdrQqHzZsgkoFW4Iav8u\n5SjkbVfyEYLlrOkdtn25G9K9jAmKDQG2fXap20SBTWQQu4rWLVKExK3rEVnMhTId\nM3wCCcnFjiZVSwy6pGNkpiMfLu37srAoNcHKEmaErbg6tNp2RDMNXcfBxsxNIcp6\nR5ZaESwcriLjgHzAgmMCAwEAAaNQME4wHQYDVR0OBBYEFGmG5dc2YEEDbFA2+SBS\nA13S5l4VMB8GA1UdIwQYMBaAFGmG5dc2YEEDbFA2+SBSA13S5l4VMAwGA1UdEwQF\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAAEmICKB6kq/Y++JKHZg88JS4nlWzIFh\nNBrfyCnMQiL9mmllEXQIhK25xleQwQGsBF2odDj+8H9CG/lwWLmyC5+TryFjWrhn\nHlt8QJb8E4dIZkYAxDL/ii6tXfFTjvrXsTcY2moD6ZoOoxahVOjVfwkHup0ONn2v\nsCL/11FneR0jhgruXKoqrKspgNVuYp+t4IKnnePpeGJb/I3SyS9GUXlScV/uWyRw\nLdIoR2teEWcWeNrMLmth0NSa3AF3gd9+HTaGpESsusG4qPamqiSM7+INAeTo4k8b\nlbqLwo3Ju6cNGGlDSsDXIUahpCdKnqxBALytITmIcHwsR4vYaDP4iOE=\n-----END CERTIFICATE-----", 3 | "mock-key-id-2": "-----BEGIN CERTIFICATE-----\nMIIDKjCCAhKgAwIBAgIIBIUnv7pTIx8wDQYJKoZIhvcNAQEFBQAwODE2MDQGA1UE\nAxMtdGVzdC00ODQubWctdGVzdC0xMjEwLmlhbS5nc2VydmljZWFjY291bnQuY29t\nMB4XDTE2MDMxOTE3NTE1NFoXDTE2MDMyMTA2NTE1NFowODE2MDQGA1UEAxMtdGVz\ndC00ODQubWctdGVzdC0xMjEwLmlhbS5nc2VydmljZWFjY291bnQuY29tMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7beJFmTrA/T4AeMWk/IjxUlGpaxH\n6D1CYbfxEBJUqzuIe7ujaxh76ik/FPQV5WxlL1GOjW0/f5CsmrNaFmTmQbsK4BY3\n3cCd3gM8LcEtmF1I9NxxpXxrZihlfuwbEpb5NpjGPkCC+fG3gTY7qtjuO6e8pGb2\nVQQguOGXKw/YZLZRZXZ41xkQRYrs+tFw48+4YkjMsYJIxyBMiL5Q/HNAQ2IUyZwr\nuc+CMcWyPLNcnsRNXgnPXQD/GKZQnjjJ5KzQAU1vnDcufL9V5KRhb0kRxTTUjE7D\nJl3x4+J6+hbAheZFu9Fntrxie9TvQuQbEBm/437QFYZphfQli0fDjlPHSwIDAQAB\nozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK\nBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEAQzlUGQiWiHgeBZyUsetuoTiQ\nsxzU7B1qw3la/FQrG+jRFr9GE3yjOOxi9JvX16U/ebwSHLUip8UFf/Ir6AJlt/tt\nIjBA6TOd8DysAtr4PCZrAP/m43H9w4lBWdWl1XJE2YfYQgZnorveAMUZqTo0P0pd\nFo3IsYBSTMflKv2Vqz91PPiHgyu2fk+8TYwJT57rnnkS6VzdORTIf+9ZB+J1ye9i\nQN5IgdZ/eqFiJPD8qT5jOcXelWSWqHHdGrNjQNp+z8jgMusY5/ZAlZUe55eo3I0m\nuDSPImLNkDwqY0+bBW6Fp5xi/4O+gJg3cQ+/PeIHzoFqKAlSpxQZSCziPpGfAA==\n-----END CERTIFICATE-----\n", 4 | "mock-key-id-3": "-----BEGIN CERTIFICATE-----\nMIIC+jCCAeKgAwIBAgIIRKlYUHIlbRkwDQYJKoZIhvcNAQEFBQAwIDEeMBwGA1UE\nAxMVMTAwNzcyNzQyMjQ5MTUwNTc4MjYyMB4XDTE2MDIxMDAxMzI1N1oXDTI2MDIw\nNzAxMzI1N1owIDEeMBwGA1UEAxMVMTAwNzcyNzQyMjQ5MTUwNTc4MjYyMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkIqM1JR3h61LDUN+FaTDFnwQkrVD\nG9qDy54QNSxElxgEPRyGhc4KAz7cRTMSWfoCWYkYaW/nZkqTfWlhsawZQU1oK8pj\nxJhUQHYpTISA4DTLmz5R4ng9mVGMqZWaCs4oiPvdizyrxus6RIdQ5bRbZyhl4pzn\n23E8tszCxnFX4KYneyvLtbcXoEvSezhm6n6yT4bNzSZguKxOSZU3XNFPmcBVjYPN\njA2aWzE54uu0ve+JrBVkRq/3XB/OvvJyjIovdxnzF91YJ5KukHJMhZqnQRIc3VcN\njx5+WwSQKE2klR7wB0AsAIs0lxA4wxH/+EUCHM2S5lfrMidz7cOXmMmeJwIDAQAB\nozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK\nBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEAF+V8kmeJQnvpPKlFT74BROi0\n1Eple2mSsyQbtm1kL7FJpl1AXZ4sLXXTVj3ql0LsqVawDCVtUSvDXBSHtejnh0bi\nZ0WUyEEJ38XPfXRilIaTrYP408ezowDaXxrfLhho1EjoMOPgXjksu1FyhBFoHmif\ndLJoxyA4f+8DZ8jj7ew6ZIVEmvONYgctpU72uUh36Vyl84oc9D2GByq/zYDXvVvl\nSKWYZ5+86/eGocO4sosB5QrsEdVGT2Im6mz2DUIewSyIvrDgZ5r3XyL4RXpdi8+8\n9re/meIh5pnhimU4pX9weQia8bqSPf0oZhh0uAWxO5ES7k1GwblnJfxeCZ0xDQ==\n-----END CERTIFICATE-----\n" 5 | } 6 | -------------------------------------------------------------------------------- /testdata/refresh_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "authorized_user", 3 | "client_id": "mock.apps.googleusercontent.com", 4 | "client_secret": "mock-secret", 5 | "refresh_token": "mock-refresh-token" 6 | } 7 | -------------------------------------------------------------------------------- /testdata/service_account.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "mock-project-id", 4 | "private_key_id": "mock-key-id-1", 5 | "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwJENcRev+eXZKvhhWLiV3Lz2MvO+naQRHo59g3vaNQnbgyduN/L4krlr\nJ5c6FiikXdtJNb/QrsAHSyJWCu8j3T9CruiwbidGAk2W0RuViTVspjHUTsIHExx9euWM0Uom\nGvYkoqXahdhPL/zViVSJt+Rt8bHLsMvpb8RquTIb9iKY3SMV2tCofNmyCSgVbghq/y7lKORt\nV/IRguWs6R22fbkb0r2MCYoNAbZ9dqnbRIFNZBC7itYtUoTEresRWcyFMh0zfAIJycWOJlVL\nDLqkY2SmIx8u7fuysCg1wcoSZoStuDq02nZEMw1dx8HGzE0hynpHlloRLByuIuOAfMCCYwID\nAQABAoIBADFtihu7TspAO0wSUTpqttzgC/nsIsNn95T2UjVLtyjiDNxPZLUrwq42tdCFur0x\nVW9Z+CK5x6DzXWvltlw8IeKKeF1ZEOBVaFzy+YFXKTz835SROcO1fgdjyrme7lRSShGlmKW/\nGKY+baUNquoDLw5qreXaE0SgMp0jt5ktyYuVxvhLDeV4omw2u6waoGkifsGm8lYivg5l3VR7\nw2IVOvYZTt4BuSYVwOM+qjwaS1vtL7gv0SUjrj85Ja6zERRdFiITDhZw6nsvacr9/+/aut9E\naL/koSSb62g5fntQMEwoT4hRnjPnAedmorM9Rhddh2TB3ZKTBbMN1tUk3fJxOuECgYEA+z6l\neSaAcZ3qvwpntcXSpwwJ0SSmzLTH2RJNf+Ld3eBHiSvLTG53dWB7lJtF4R1KcIwf+KGcOFJv\nsnepzcZBylRvT8RrAAkV0s9OiVm1lXZyaepbLg4GGFJBPi8A6VIAj7zYknToRApdW0s1x/XX\nChewfJDckqsevTMovdbg8YkCgYEAxDYX+3mfvv/opo6HNNY3SfVunM+4vVJL+n8gWZ2w9kz3\nQ9Ub9YbRmI7iQaiVkO5xNuoG1n9bM+3Mnm84aQ1YeNT01YqeyQsipP5Wi+um0PzYTaBw9RO+\n8Gh6992OwlJiRtFk5WjalNWOxY4MU0ImnJwIfKQlUODvLmcixm68NYsCgYEAuAqI3jkk55Vd\nKvotREsX5wP7gPePM+7NYiZ1HNQL4Ab1f/bTojZdTV8Sx6YCR0fUiqMqnE+OBvfkGGBtw22S\nLesx6sWf99Ov58+x4Q0U5dpxL0Lb7d2Z+2Dtp+Z4jXFjNeeI4ae/qG/LOR/b0pE0J5F415ap\n7Mpq5v89vepUtrkCgYAjMXytu4v+q1Ikhc4UmRPDrUUQ1WVSd+9u19yKlnFGTFnRjej86hiw\nH3jPxBhHra0a53EgiilmsBGSnWpl1WH4EmJz5vBCKUAmjgQiBrueIqv9iHiaTNdjsanUyaWw\njyxXfXl2eI80QPXh02+8g1H/pzESgjK7Rg1AqnkfVH9nrwKBgQDJVxKBPTw9pigYMVt9iHrR\niCl9zQVjRMbWiPOc0J56+/5FZYm/AOGl9rfhQ9vGxXZYZiOP5FsNkwt05Y1UoAAH4B4VQwbL\nqod71qOcI0ywgZiIR87CYw40gzRfjWnN+YEEW1qfyoNLilEwJB8iB/T+ZePHGmJ4MmQ/cTn9\nxpdLXA==\n-----END RSA PRIVATE KEY-----", 6 | "client_email": "mock-email@mock-project.iam.gserviceaccount.com", 7 | "client_id": "1234567890", 8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 9 | "token_uri": "https://accounts.google.com/o/oauth2/token", 10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/mock-project-id.iam.gserviceaccount.com" 12 | } 13 | --------------------------------------------------------------------------------