diff --git a/.github/workflows/api-information.yml b/.github/workflows/api-information.yml new file mode 100644 index 00000000000..cf2d37da5da --- /dev/null +++ b/.github/workflows/api-information.yml @@ -0,0 +1,31 @@ +name: API Information + +on: [ pull_request ] + +jobs: + api-information-check: + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 2 + submodules: true + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Set up fireci + run: pip3 install -e ci/fireci + - name: Run api-information check + run: | + fireci api_information \ + --issue_number=${{ github.event.pull_request.number }} \ + --repo_name=${{ github.repository }} \ + --auth_token=${{ secrets.GOOGLE_OSS_BOT_TOKEN }} diff --git a/.github/workflows/build-release-artifacts.yml b/.github/workflows/build-release-artifacts.yml new file mode 100644 index 00000000000..90928ed494f --- /dev/null +++ b/.github/workflows/build-release-artifacts.yml @@ -0,0 +1,33 @@ +name: Build Release Artifacts + +on: + workflow_dispatch: + pull_request: + branches: + - 'releases/**' + +jobs: + build-artifacts: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4.1.1 + + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - name: Perform gradle build + run: | + ./gradlew firebasePublish + + - name: Upload generated artifacts + uses: actions/upload-artifact@v4.3.3 + with: + name: release_artifacts + path: build/*.zip + retention-days: 15 diff --git a/.github/workflows/build-src-check.yml b/.github/workflows/build-src-check.yml new file mode 100644 index 00000000000..98aed0d6649 --- /dev/null +++ b/.github/workflows/build-src-check.yml @@ -0,0 +1,32 @@ +name: build-src-check + +on: + pull_request: + paths: + - 'buildSrc/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build-src-check: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4.1.1 + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: buildSrc Tests + env: + FIREBASE_CI: 1 + run: | + ./gradlew -b buildSrc/build.gradle.kts -PenablePluginTests=true check + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@b9f6c61d965bcaa18acc02d6daf706373a448f02 + with: + files: "**/build/test-results/**/*.xml" + check_name: "buildSrc Test Results" diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 00000000000..ddf50fb93f7 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,29 @@ +name: Changelog + +on: + pull_request + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + changelog-check: + runs-on: ubuntu-22.04 + env: + BUNDLE_GEMFILE: ./ci/danger/Gemfile + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 100 + submodules: true + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '2.7' + - name: Setup Bundler + run: ./ci/danger/setup_bundler.sh + - name: Danger CHANGELOG verifier + env: + DANGER_GITHUB_API_TOKEN: ${{ secrets.GOOGLE_OSS_BOT_TOKEN }} + run: + '[ ! -z $DANGER_GITHUB_API_TOKEN ] && bundle exec danger --dangerfile=./ci/danger/Dangerfile || echo "Skipping Danger for External Contributor"' diff --git a/.github/workflows/check-head-dependencies.yml b/.github/workflows/check-head-dependencies.yml new file mode 100644 index 00000000000..088724bf1d4 --- /dev/null +++ b/.github/workflows/check-head-dependencies.yml @@ -0,0 +1,22 @@ +name: Check Head Dependencies + +on: + workflow_dispatch: + pull_request: + branches: + - 'releases/**' + +jobs: + check-head-dependencies: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Perform gradle build + run: | + ./gradlew checkHeadDependencies diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml new file mode 100644 index 00000000000..beb12d0a5bf --- /dev/null +++ b/.github/workflows/ci_tests.yml @@ -0,0 +1,275 @@ +name: CI Tests +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +on: + pull_request: + push: + branches: + - master + +jobs: + determine_changed: + name: "Determine changed modules" + runs-on: ubuntu-22.04 + if: (github.repository == 'Firebase/firebase-android-sdk' && github.event_name == 'push') || github.event_name == 'pull_request' + outputs: + modules: ${{ steps.changed-modules.outputs.modules }} + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 2 + submodules: true + + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - id: changed-modules + run: | + git diff --name-only HEAD~1 | xargs printf -- '--changed-git-paths %s\n' | xargs ./gradlew writeChangedProjects --output-file-path=modules.json + echo modules=$(cat modules.json) >> $GITHUB_OUTPUT + + unit_tests: + name: "Unit Tests" + runs-on: ubuntu-22.04 + needs: + - determine_changed + strategy: + fail-fast: false + matrix: + module: ${{ fromJSON(needs.determine_changed.outputs.modules) }} + + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 2 + submodules: true + + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - name: Add google-services.json + env: + INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} + run: | + echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > google-services.json + + - name: ${{ matrix.module }} Unit Tests + env: + FIREBASE_CI: 1 + run: | + ./gradlew ${{matrix.module}}:check withErrorProne + - name: Compute upload file name + run: | + MODULE=${{matrix.module}} + echo "ARTIFACT_NAME=${MODULE//:/_}" >> $GITHUB_ENV + - name: Upload Test Results + uses: actions/upload-artifact@v4.3.3 + if: always() + with: + name: unit-test-result-${{env.ARTIFACT_NAME}} + path: "**/build/test-results/**/*.xml" + retention-days: 7 + if-no-files-found: ignore + + # A job that fails if any job in the unit_tests matrix fails, + # to be used as a required check for merging. + check_all: + runs-on: ubuntu-22.04 + if: always() + name: Unit Tests (matrix) + needs: unit_tests + steps: + - name: Check test matrix + if: needs.unit_tests.result != 'success' + run: exit 1 + + + integ_tests: + name: "Instrumentation Tests" + # only run on post submit or PRs not originating from forks. + if: (github.repository == 'Firebase/firebase-android-sdk' && github.event_name == 'push') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) + runs-on: ubuntu-22.04 + needs: + - determine_changed + strategy: + fail-fast: false + matrix: + module: ${{ fromJSON(needs.determine_changed.outputs.modules) }} + + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 2 + submodules: true + + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - name: Add google-services.json + env: + INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} + run: | + echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > google-services.json + - uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} + - uses: google-github-actions/setup-gcloud@v2 + - name: ${{ matrix.module }} Integ Tests + env: + FIREBASE_CI: 1 + FTL_RESULTS_BUCKET: android-ci + FTL_RESULTS_DIR: ${{ github.event_name == 'pull_request' && format('pr-logs/pull/{0}/{1}/{2}/{3}_{4}/artifacts/', github.repository, github.event.pull_request.number, github.job, github.run_id, github.run_attempt) || format('logs/{0}/{1}_{2}/artifacts/', github.workflow, github.run_id, github.run_attempt)}} + FIREBASE_APP_CHECK_DEBUG_SECRET: ${{ secrets.FIREBASE_APP_CHECK_DEBUG_SECRET }} + run: | + ./gradlew ${{matrix.module}}:deviceCheck withErrorProne -PtargetBackend="prod" + + firestore_custom_integ_tests: + name: "Firestore Custom Instrumentation Tests Against Named DB" + runs-on: ubuntu-22.04 + needs: + - determine_changed + # only run on post submit or PRs not originating from forks. + if: ((github.repository == 'Firebase/firebase-android-sdk' && github.event_name == 'push') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) && contains(fromJSON(needs.determine_changed.outputs.modules), ':firebase-firestore') + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 2 + submodules: true + + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - name: Add google-services.json + env: + INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} + run: | + echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > google-services.json + - uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} + - uses: google-github-actions/setup-gcloud@v2 + + # create composite indexes with Terraform + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2 + - name: Terraform Init + run: | + cd firebase-firestore + terraform init + continue-on-error: true + - name: Terraform Apply + if: github.event_name == 'pull_request' + run: | + cd firebase-firestore + + # Define a temporary file, redirect both stdout and stderr to the file + output_file=$(mktemp) + if ! terraform apply -var-file=../google-services.json -auto-approve > "$output_file" 2>&1 ; then + cat "$output_file" + if cat "$output_file" | grep -q "index already exists"; then + echo "===================================================================================" + echo -e "\e[93m\e[1mTerraform apply failed due to index already exists; We can safely ignore this error.\e[0m" + echo "===================================================================================" + fi + exit 1 + fi + rm -f "$output_file" + continue-on-error: true + + - name: Firestore Named DB Integ Tests + env: + FIREBASE_CI: 1 + FTL_RESULTS_BUCKET: android-ci + FTL_RESULTS_DIR: ${{ github.event_name == 'pull_request' && format('pr-logs/pull/{0}/{1}/{2}/{3}_{4}/artifacts/', github.repository, github.event.pull_request.number, github.job, github.run_id, github.run_attempt) || format('logs/{0}/{1}_{2}/artifacts/', github.workflow, github.run_id, github.run_attempt)}} + FIREBASE_APP_CHECK_DEBUG_SECRET: ${{ secrets.FIREBASE_APP_CHECK_DEBUG_SECRET }} + run: | + ./gradlew firebase-firestore:deviceCheck withErrorProne -PtargetBackend="prod" -PtargetDatabaseId="test-db" + + + firestore_nightly_integ_tests: + name: "Firestore Instrumentation Tests Against Nightly Environment" + runs-on: ubuntu-22.04 + needs: + - determine_changed + # only run on post submit or PRs not originating from forks. + if: ((github.repository == 'Firebase/firebase-android-sdk' && github.event_name == 'push') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) && contains(fromJSON(needs.determine_changed.outputs.modules), ':firebase-firestore') + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 2 + submodules: true + + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - name: Add google-services.json + env: + INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.NIGHTLY_INTEG_TESTS_GOOGLE_SERVICES }} + run: | + echo $INTEG_TESTS_GOOGLE_SERVICES > google-services.json + - uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} + - uses: google-github-actions/setup-gcloud@v2 + + - name: Firestore Nightly Integ Tests + env: + FIREBASE_CI: 1 + FTL_RESULTS_BUCKET: android-ci + FTL_RESULTS_DIR: ${{ github.event_name == 'pull_request' && format('pr-logs/pull/{0}/{1}/{2}/{3}_{4}/artifacts/', github.repository, github.event.pull_request.number, github.job, github.run_id, github.run_attempt) || format('logs/{0}/{1}_{2}/artifacts/', github.workflow, github.run_id, github.run_attempt)}} + FIREBASE_APP_CHECK_DEBUG_SECRET: ${{ secrets.FIREBASE_APP_CHECK_DEBUG_SECRET }} + run: | + ./gradlew firebase-firestore:deviceCheck withErrorProne -PtargetBackend="nightly" + + + publish-test-results: + name: "Publish Tests Results" + needs: + - unit_tests + runs-on: ubuntu-22.04 + + permissions: + checks: write + + # only needed unless run with comment_mode: off + pull-requests: write + + if: always() + + steps: + - name: Download Artifacts + uses: actions/download-artifact@v4.1.5 + with: + path: artifacts + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v1 + with: + files: "artifacts/**/*.xml" diff --git a/.github/workflows/config-e2e.yml b/.github/workflows/config-e2e.yml new file mode 100644 index 00000000000..604115b324d --- /dev/null +++ b/.github/workflows/config-e2e.yml @@ -0,0 +1,42 @@ +name: Firebase Remote Config E2E Tests + +on: + schedule: + - cron: 24 */4 * * * # every 4 hours @ 24 minutes past the hour + workflow_dispatch: # allow triggering the workflow manually + +concurrency: + group: ${{ github.workflow }} + +env: + REMOTE_CONFIG_E2E_GOOGLE_SERVICES: ${{ secrets.REMOTE_CONFIG_E2E_GOOGLE_SERVICES }} + +jobs: + test: + + runs-on: ubuntu-latest + + steps: + - name: Checkout firebase-config + uses: actions/checkout@v4.1.1 + + - name: set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Add google-services.json + run: | + echo $REMOTE_CONFIG_E2E_GOOGLE_SERVICES | base64 -d > google-services.json + + - uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_service_account }} + - uses: google-github-actions/setup-gcloud@v2 + - name: Run Remote Config end-to-end tests + env: + FTL_RESULTS_BUCKET: fireescape + run: | + ./gradlew :firebase-config:test-app:deviceCheck withErrorProne -PtargetBackend="prod" diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml new file mode 100644 index 00000000000..dd17b2acd4f --- /dev/null +++ b/.github/workflows/copyright-check.yml @@ -0,0 +1,27 @@ +name: Copyright check + +on: pull_request + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + copyright-check: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4.1.1 + - uses: actions/setup-python@v2 + with: + python-version: '3.9' + - run: | + pip install -e "ci/fireci" + - run: | + fireci copyright_check \ + -e py \ + -e gradle \ + -e java \ + -e kt \ + -e groovy \ + -e sh \ + -e proto diff --git a/.github/workflows/create_releases.yml b/.github/workflows/create_releases.yml new file mode 100644 index 00000000000..3d3c654683e --- /dev/null +++ b/.github/workflows/create_releases.yml @@ -0,0 +1,50 @@ +name: Create release + +on: + workflow_dispatch: + inputs: + name: + description: 'Release name' + required: true + type: string + past-name: + description: 'Past release name' + required: true + type: string + +jobs: + create-branches: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Create base branch + uses: peterjgrainger/action-create-branch@c2800a3a9edbba2218da6861fa46496cf8f3195a + with: + branch: 'releases/${{ inputs.name }}' + + create-pull-request: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Create release configuration template + run: | + ./gradlew generateReleaseConfig -PcurrentRelease=${{ inputs.name }} -PpastRelease=${{ inputs.past-name }} -PprintOutput=true + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v4 + with: + base: 'releases/${{ inputs.name }}' + branch: 'releases/${{ inputs.name }}.release' + add-paths: release.json,release_report.md,release_report.json + title: '${{ inputs.name}} release' + body: 'Auto-generated PR for release ${{ inputs.name}}' + commit-message: 'Create release config for ${{ inputs.name }}' diff --git a/.github/workflows/diff-javadoc.yml b/.github/workflows/diff-javadoc.yml new file mode 100644 index 00000000000..17fb370d0bf --- /dev/null +++ b/.github/workflows/diff-javadoc.yml @@ -0,0 +1,64 @@ +name: Diff Javadoc + +on: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Make diff directory + run: mkdir ~/diff + + - name: Checkout PR branch + uses: actions/checkout@v4.1.1 + with: + fetch-depth: 2 + submodules: true + + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - name: Generate docs for PR branch + run: ./gradlew kotlindoc + + - name: Move branch docs to diff directory + run: mv build ~/diff/modified + + - name: Checkout master + uses: actions/checkout@v4.1.1 + with: + ref: ${{ github.base_ref }} + + - name: Generate docs for Master + run: ./gradlew kotlindoc + + - name: Move master docs to diff directory + run: mv build ~/diff/original + + - name: Get diff between Master and Branch docs + run: > + `# Recursively diff directories, including new files, git style, with 3 lines of context` + diff -wEburN ~/diff/original ~/diff/modified + `# Remove the first line and new file signifier of the output` + | tail -n +2 + `# Replace the diff new file signifier with the end and start of a new codeblock` + | sed "s/^diff.*$/\`\`\`\\n\`\`\`diff/g" + `# Add a collapsable block, summary, and start the first code block on the first line` + | sed "1s/^/
\\nJavadoc Changes:<\/summary>\\n\\n\`\`\`diff\\n/" + `# Close the final code block and close the collapsable on the final line` + | sed "$ s/$/\\n\`\`\`\\n<\/details>/" + `# Write to diff.md for later` + > diff.md + + - name: Add comment + continue-on-error: true + uses: mshick/add-pr-comment@a65df5f64fc741e91c59b8359a4bc56e57aaf5b1 + with: + message-path: diff.md diff --git a/.github/workflows/fireci.yml b/.github/workflows/fireci.yml new file mode 100644 index 00000000000..2181236e2a8 --- /dev/null +++ b/.github/workflows/fireci.yml @@ -0,0 +1,27 @@ +name: fireci + +on: + pull_request: + paths: + - 'ci/**' + - '.github/workflows/fireci.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + fireci: + name: "fireci tests" + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4.1.1 + - uses: actions/setup-python@v2 + with: + python-version: '3.8' + - run: | + pip install -e "ci/fireci[test]" + - run: | + pytest ci/fireci + - run: | + mypy --config-file ci/fireci/setup.cfg ci/fireci/ diff --git a/.github/workflows/fireperf-e2e.yml b/.github/workflows/fireperf-e2e.yml new file mode 100644 index 00000000000..d1e09f1e2d7 --- /dev/null +++ b/.github/workflows/fireperf-e2e.yml @@ -0,0 +1,107 @@ +name: FirePerf E2E Tests + +on: + schedule: + - cron: 4 */4 * * * # every 4 hours at 04 minutes past the hour + workflow_dispatch: # allow triggering the workflow manually + +concurrency: + group: ${{ github.workflow }} + +env: + PERF_E2E_GOOGLE_SERVICES: ${{ secrets.PERF_E2E_GOOGLE_SERVICES }} + FTL_RESULTS_BUCKET: fireescape + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + environment: [ prod, autopush ] + steps: + - name: Checkout firebase-android-sdk + uses: actions/checkout@v4.1.1 + - name: Checkout firebase-android-buildtools + uses: actions/checkout@v4.1.1 + with: + repository: FirebasePrivate/firebase-android-buildtools + token: ${{ secrets.GOOGLE_OSS_BOT_TOKEN }} + path: firebase-android-buildtools + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Set up fireci + run: pip3 install -e ci/fireci + - uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} + - uses: google-github-actions/setup-gcloud@v2 + - name: Add google-services.json + run: echo $PERF_E2E_GOOGLE_SERVICES | base64 -d > google-services.json + - name: Run fireperf end-to-end tests + run: | + fireci fireperf_e2e_test \ + --plugin_repo_dir=firebase-android-buildtools \ + --target_environment=${{ matrix.environment }} + - name: Notify developers upon failures + if: ${{ failure() }} + uses: actions/github-script@v6 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const commit = context.sha; + const run = context.runId; + const url = `https://github.com/${owner}/${repo}/actions/runs/${run}`; + + const datetime = (new Date()).toLocaleString('en-US', { + timeZone: 'America/Los_Angeles', + dateStyle: 'medium', + timeStyle: 'long', + }); + + const text = + `Failed on commit ${commit} at ${datetime}. + + ${url}`; + + const { data: issues } = await github.rest.issues.listForRepo({ + owner: owner, + repo: repo, + state: 'open', + labels: 'fireperf-e2e-tests' + }); + + if (issues.length) { + github.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: issues[0].number, + body: text, + }); + } else { + github.rest.issues.create({ + owner: owner, + repo: repo, + title: 'FirePerf E2E Test Failures', + body: text, + labels: ['fireperf-e2e-tests'], + assignees: ['raymondlam', 'visumickey'] + }); + } + - name: Upload test artifacts + if: always() + uses: actions/upload-artifact@v4.3.3 + with: + name: test-artifacts (${{ matrix.environment }}) + path: | + ~/.m2/repository/com/google/firebase/perf-plugin + **/build/reports + **/build/test-results diff --git a/.github/workflows/health-metrics.yml b/.github/workflows/health-metrics.yml new file mode 100644 index 00000000000..a47c682ae61 --- /dev/null +++ b/.github/workflows/health-metrics.yml @@ -0,0 +1,134 @@ +name: Health Metrics + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +on: + pull_request: + push: + branches: + - master + # add other feature branches here + # TODO(yifany): support workflow_dispatch for metric tests (or only for startup time test) + +env: + GITHUB_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + +jobs: + coverage: + name: Coverage + if: | + (github.event_name == 'push' && github.repository == 'firebase/firebase-android-sdk') + || (github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 2 + submodules: true + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - uses: google-github-actions/auth@v2 + with: + credentials_json: '${{ secrets.GCP_SERVICE_ACCOUNT }}' + - uses: google-github-actions/setup-gcloud@v2 + - name: Set up fireci + run: pip3 install -e ci/fireci + - name: Run coverage tests (presubmit) + if: ${{ github.event_name == 'pull_request' }} + run: fireci coverage --pull-request + - name: Run coverage tests (post-submit) + if: ${{ github.event_name == 'push' }} + run: fireci coverage + + size: + name: Size + if: | + (github.event_name == 'push' && github.repository == 'firebase/firebase-android-sdk') + || (github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 2 + submodules: true + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - uses: google-github-actions/auth@v2 + with: + credentials_json: '${{ secrets.GCP_SERVICE_ACCOUNT }}' + - uses: google-github-actions/setup-gcloud@v2 + - name: Set up fireci + run: pip3 install -e ci/fireci + - name: Run size tests (presubmit) + if: ${{ github.event_name == 'pull_request' }} + run: fireci binary_size --pull-request + - name: Run size tests (post-submit) + if: ${{ github.event_name == 'push' }} + run: fireci binary_size + + startup_time: + name: Startup Time + if: | + (github.event_name == 'push' && github.repository == 'firebase/firebase-android-sdk') + || (github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository + && github.event.pull_request.base.ref == 'master') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 2 + submodules: true + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - uses: google-github-actions/auth@v2 + with: + credentials_json: '${{ secrets.GCP_SERVICE_ACCOUNT }}' + - uses: google-github-actions/setup-gcloud@v2 + - name: Set up fireci + run: pip3 install -e ci/fireci + - name: Add google-services.json + env: + INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} + BENCHMARK_APP_LOCATION: health-metrics/benchmark/template/app/google-services.json + run: | + echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > $BENCHMARK_APP_LOCATION + - name: Run startup-time tests (presubmit) + if: ${{ github.event_name == 'pull_request' }} + run: | + git diff --name-only HEAD~1 | \ + xargs printf -- '--changed-git-paths %s\n' | \ + xargs ./gradlew writeChangedProjects --output-file-path=modules.json + fireci macrobenchmark ci --pull-request --changed-modules-file modules.json + - name: Run startup-time tests (post-submit) + if: ${{ github.event_name == 'push' }} + run: | + fireci macrobenchmark ci --push diff --git a/.github/workflows/jekyll-gh-pages.yml b/.github/workflows/jekyll-gh-pages.yml new file mode 100644 index 00000000000..2a6bd787e0d --- /dev/null +++ b/.github/workflows/jekyll-gh-pages.yml @@ -0,0 +1,55 @@ +name: Jekyll with GitHub Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["master"] + paths: + - '.github/workflows/jekyll-gh-pages.yml' + - 'contributor-docs/**' + pull_request: + paths: + - '.github/workflows/jekyll-gh-pages.yml' + - 'contributor-docs/**' + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4.1.1 + - name: Setup Pages + uses: actions/configure-pages@v2 + - name: Build with Jekyll + uses: actions/jekyll-build-pages@v1 + with: + source: ./contributor-docs + destination: ./_site + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + + deploy: + if: ${{ github.event_name == 'push' && github.repository == 'firebase/firebase-android-sdk' }} + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 diff --git a/.github/workflows/make-bom.yml b/.github/workflows/make-bom.yml new file mode 100644 index 00000000000..2678455add3 --- /dev/null +++ b/.github/workflows/make-bom.yml @@ -0,0 +1,39 @@ +name: Make BoM + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - uses: actions/checkout@v4.1.1 + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - name: Build + run: | + ./ci/run.sh \ + --artifact-target-dir=./logs/artifacts \ + --artifact-patterns=bom.zip \ + --artifact-patterns=bomReleaseNotes.md \ + --artifact-patterns=recipeVersionUpdate.txt \ + gradle \ + -- \ + --build-cache \ + buildBomZip + + - name: Upload generated artifacts + uses: actions/upload-artifact@v4.3.3 + with: + name: artifacts + path: ./logs/artifacts/ + retention-days: 5 diff --git a/.github/workflows/merge-to-main.yml b/.github/workflows/merge-to-main.yml new file mode 100644 index 00000000000..70b03e29b61 --- /dev/null +++ b/.github/workflows/merge-to-main.yml @@ -0,0 +1,30 @@ +name: Merge to main + +on: + pull_request: + branches: + - master + types: + - opened + - labeled + - unlabeled + +jobs: + pr-message: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: mshick/add-pr-comment@a65df5f64fc741e91c59b8359a4bc56e57aaf5b1 + with: + message: > + ### 📝 PRs merging into main branch + + **Our main branch should always be in a releasable state**. + If you are working on a larger change, or if you don't want + this change to see the light of the day just yet, consider + using a feature branch first, and only merge into the main + branch when the code complete and ready to be released. + + - name: Success + run: exit 0 diff --git a/.github/workflows/post_release_cleanup.yml b/.github/workflows/post_release_cleanup.yml new file mode 100644 index 00000000000..fb6d7dc8f96 --- /dev/null +++ b/.github/workflows/post_release_cleanup.yml @@ -0,0 +1,43 @@ +name: Post release cleanup + +on: + workflow_dispatch: + inputs: + name: + description: 'Release name' + required: true + type: string + +jobs: + create-pull-request: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Run post release cleanup task + run: | + ./gradlew postReleaseCleanup + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v4 + with: + base: 'master' + branch: 'releases/${{ inputs.name }}.mergeback' + add-paths: | + **/CHANGELOG.md + **/gradle.properties + **/*.gradle + **/*.gradle.kts + title: '${{ inputs.name}} mergeback' + body: | + Auto-generated PR for cleaning up release ${{ inputs.name}} + + NO_RELEASE_CHANGE + commit-message: 'Post release cleanup for ${{ inputs.name }}' diff --git a/.github/workflows/private-mirror-sync.yml b/.github/workflows/private-mirror-sync.yml new file mode 100644 index 00000000000..9bb2ca13a0b --- /dev/null +++ b/.github/workflows/private-mirror-sync.yml @@ -0,0 +1,25 @@ +name: Private Mirror Sync + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +on: + push: + branches: + - master + +jobs: + sync: + if: github.repository == 'firebase/firebase-android-sdk' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 + submodules: true + token: ${{ secrets.GOOGLE_OSS_BOT_TOKEN }} + - name: Force push HEAD to private repo main branch + run: | + git remote add mirror https://github.com/FirebasePrivate/firebase-android-sdk.git + git push mirror HEAD:main --force --verbose diff --git a/.github/workflows/release-note-changes.yml b/.github/workflows/release-note-changes.yml new file mode 100644 index 00000000000..2ef318e9809 --- /dev/null +++ b/.github/workflows/release-note-changes.yml @@ -0,0 +1,67 @@ +name: Release note changes + +on: + pull_request: + branches: + - 'master' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 + + - name: Create output file + run: touch changelog_comment.md + + - name: Get changed changelog files + id: changed-files + uses: tj-actions/changed-files@v36.0.10 + with: + files_ignore: | + buildSrc/** + files: | + **/CHANGELOG.md + + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + if: ${{ steps.changed-files.outputs.any_changed == 'true' }} + with: + python-version: '3.10' + + - name: Set up fireci + id: install-fireci + if: ${{ steps.changed-files.outputs.any_changed == 'true' }} + run: pip3 install -e ci/fireci + + - name: Generate comment + id: generate-comment + if: ${{ steps.install-fireci.outcome == 'success' }} + run: | + fireci changelog_comment -c "${{ steps.changed-files.outputs.all_changed_files }}" -o ./changelog_comment.md + + - name: Add PR Comment + uses: mshick/add-pr-comment@v2.8.1 + continue-on-error: true + with: + status: ${{ steps.generate-comment.outcome }} + message-path: ./changelog_comment.md + message-skipped: | + ## Release note changes + No release note changes were detected. If you made changes that should be + present in the next release, ensure you've added an entry in the appropriate + `CHANGELOG.md` file(s). + message-failure: | + ## Release note changes + A `CHANGELOG.md` file seems to not match the expected format. + Please ensure your changelog files are following the format as + defined in [our documentation](#). diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml new file mode 100644 index 00000000000..130ac90b22e --- /dev/null +++ b/.github/workflows/scorecards.yml @@ -0,0 +1,72 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecards supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '45 11 * * 1' + push: + branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecards analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@v4.1.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # v2.0.6 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecards on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@v4.3.3 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@807578363a7869ca324a79039e6db9c843e0e100 # v2.1.27 + with: + sarif_file: results.sarif diff --git a/.github/workflows/semver-check.yml b/.github/workflows/semver-check.yml new file mode 100644 index 00000000000..2fc7eb38843 --- /dev/null +++ b/.github/workflows/semver-check.yml @@ -0,0 +1,22 @@ +name: Semver Check + +on: + workflow_dispatch: + pull_request: + branches: + - 'releases/**' + +jobs: + semver-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Perform gradle build + run: | + ./gradlew semverCheckForRelease diff --git a/.github/workflows/sessions-e2e.yml b/.github/workflows/sessions-e2e.yml new file mode 100644 index 00000000000..048cd92eee9 --- /dev/null +++ b/.github/workflows/sessions-e2e.yml @@ -0,0 +1,42 @@ +name: Firebase Sessions E2E Tests + +on: + schedule: + - cron: 24 */4 * * * # every 4 hours at 24 minutes past the hour + workflow_dispatch: # allow triggering the workflow manually + +concurrency: + group: ${{ github.workflow }} + +env: + SESSIONS_E2E_GOOGLE_SERVICES: ${{ secrets.SESSIONS_E2E_GOOGLE_SERVICES }} + +jobs: + test: + + runs-on: ubuntu-latest + + steps: + - name: Checkout firebase-sessions + uses: actions/checkout@v4.1.1 + + - name: set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: '11' + distribution: 'temurin' + cache: gradle + + - name: Add google-services.json + run: | + echo $SESSIONS_E2E_GOOGLE_SERVICES | base64 -d > google-services.json + + - uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} + - uses: google-github-actions/setup-gcloud@v2 + - name: Run sessions end-to-end tests + env: + FTL_RESULTS_BUCKET: fireescape + run: | + ./gradlew :firebase-sessions:test-app:deviceCheck withErrorProne -PtargetBackend="prod" -PtriggerCrashes diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml new file mode 100644 index 00000000000..07ab7dbeeb2 --- /dev/null +++ b/.github/workflows/smoke-tests.yml @@ -0,0 +1,60 @@ +name: Smoke Tests + +on: [ pull_request ] + +jobs: + smoke-tests: + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 2 + submodules: true + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + - uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} + - uses: google-github-actions/setup-gcloud@v2 + + # TODO(yifany): make it a fireci plugin and remove the separately distributed jar file + - name: Download smoke tests runner + run: | + SMOKE_TESTS_RUNNER_URL="https://storage.googleapis.com/android-ci/smoke-tests-runner.jar" + curl ${SMOKE_TESTS_RUNNER_URL} --output runner.jar + + # TODO(yifany): remove hardcoded reference to /smoke-tests-google-services from the runner + - name: Add google-services.json + env: + SMOKE_TESTS_GOOGLE_SERVICES: ${{ secrets.SMOKE_TESTS_GOOGLE_SERVICES }} + run: | + echo $SMOKE_TESTS_GOOGLE_SERVICES | base64 -d > google-services.json + sudo mkdir /smoke-tests-google-services + sudo mv google-services.json /smoke-tests-google-services + + # TODO(yifany): remove hardcoded reference to Prow environment variables from the runner + - name: Run smoke tests + env: + FIREBASE_CI: 1 + REPO_OWNER: ${{ github.repository_owner }} + REPO_NAME: firebase-android-sdk + PULL_NUMBER: ${{ github.event.pull_request.number }} + JOB_NAME: smoke-tests + BUILD_ID: ${{ github.run_id }} + ARTIFACTS: ${{ runner.temp }} + run: java -jar runner.jar smoke-tests/runner.config + + - name: Upload test artifacts + if: always() + uses: actions/upload-artifact@v4.3.3 + with: + name: smoke-tests-artifacts + path: | + ${{ runner.temp }}/**/*.apk + ${{ runner.temp }}/**/changed-artifacts.json + ${{ runner.temp }}/**/smoke-test-dependencies.log diff --git a/.github/workflows/update-cpp-sdk-on-release.yml b/.github/workflows/update-cpp-sdk-on-release.yml index 7f611ce7895..40dbbad5d1d 100644 --- a/.github/workflows/update-cpp-sdk-on-release.yml +++ b/.github/workflows/update-cpp-sdk-on-release.yml @@ -23,7 +23,7 @@ jobs: outputs: released_version_changed: ${{ steps.check_version.outputs.released_version_changed }} steps: - - uses: actions/checkout@v2.3.1 + - uses: actions/checkout@v4.1.1 with: # Check out the actual head commit, not any merge commit. ref: ${{ github.sha }} @@ -35,7 +35,7 @@ jobs: # Query the git history for all gradle.properties files changed by this push. # Then, check the diff to see if any "latestReleasedVersion=" lines changed. if (git diff '${{ github.event.before }}' -- '**/gradle.properties' | grep -q '^[-+]latestReleasedVersion='); then - echo "::set-output name=released_version_changed::1" + echo "released_version_changed=1" >> $GITHUB_OUTPUT else echo "No change to latestReleasedVersion detected since ${{ github.event.before }}" fi @@ -56,13 +56,13 @@ jobs: python-version: 3.7 - name: Check out firebase-cpp-sdk - uses: actions/checkout@v2.3.1 + uses: actions/checkout@v4.1.1 with: repository: firebase/firebase-cpp-sdk ref: main - name: Get firebase-workflow-trigger token - uses: tibdex/github-app-token@v1 + uses: tibdex/github-app-token@021a2405c7f990db57f5eae5397423dcc554159c id: generate-token with: app_id: ${{ secrets.CPP_WORKFLOW_TRIGGER_APP_ID }} @@ -71,5 +71,5 @@ jobs: - name: Trigger firebase-cpp-sdk update run: | - pip install -r scripts/gha/requirements.txt + pip install -r scripts/gha/python_requirements.txt python scripts/gha/trigger_workflow.py -t ${{ steps.generate-token.outputs.token }} -w update-dependencies.yml -p updateAndroid 1 -p updateiOS 0 -p comment "[Triggered]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) by [firebase-android-sdk $(date '+%b %d') release]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/${{ github.sha }})." -s 10 -A diff --git a/.github/workflows/validate-dependencies.yml b/.github/workflows/validate-dependencies.yml new file mode 100644 index 00000000000..c91ad8aee0c --- /dev/null +++ b/.github/workflows/validate-dependencies.yml @@ -0,0 +1,22 @@ +name: Validate Artifact Dependencies + +on: + workflow_dispatch: + pull_request: + branches: + - 'releases/**' + +jobs: + build-artifacts: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Perform gradle build + run: | + ./gradlew validatePomForRelease diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml new file mode 100644 index 00000000000..f5f285e29a0 --- /dev/null +++ b/.github/workflows/version-check.yml @@ -0,0 +1,22 @@ +name: Version Check + +on: + workflow_dispatch: + pull_request: + branches: + - 'releases/**' + +jobs: + version-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Build + run: | + ./gradlew gmavenVersionCheck diff --git a/.gitignore b/.gitignore index afeb384ada6..d76e10d3c80 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,13 @@ _artifacts .DS_Store firebase-crashlytics-ndk/.externalNativeBuild/ firebase-crashlytics-ndk/.cxx/ +smoke-test-logs/ +smoke-tests/build-debug-headGit-smoke-test +smoke-tests/firehorn.log +macrobenchmark-output.json + +# generated Terraform docs +.terraform/* +.terraform.lock.hcl +*.tfstate +*.tfstate.* \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c0969ee7ef..e0360670700 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,3 +27,7 @@ information on using pull requests. This project follows [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). +## Contributor Documentation + +To know more about how to setup your environment, how Firebase internals work, and +best practices, take a look at our detailed [contributor documentation](https://firebase.github.io/firebase-android-sdk/). \ No newline at end of file diff --git a/README.md b/README.md index 55dca269146..4e6500c4820 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,7 @@ # Firebase Android Open Source Development -This repository contains a subset of the Firebase Android SDK source. It -currently includes the following Firebase libraries, and some of their -dependencies: - - * `firebase-abt` - * `firebase-appdistribution` - * `firebase-common` - * `firebase-common-ktx` - * `firebase-crashlytics` - * `firebase-crashlytics-ktx` - * `firebase-crashlytics-ndk` - * `firebase-database` - * `firebase-database-ktx` - * `firebase-database-collection` - * `firebase-datatransport` - * `firebase-firestore` - * `firebase-firestore-ktx` - * `firebase-functions` - * `firebase-functions-ktx` - * `firebase-inappmessaging` - * `firebase-inappmessaging-ktx` - * `firebase-inappmessaging-display` - * `firebase-inappmessaging-display-ktx` - * `firebase-messaging` - * `firebase-messaging-directboot` - * `firebase-perf` - * `firebase-perf-ktx` - * `firebase-remote-config` - * `firebase-remote-config-ktx` - * `firebase-storage` - * `firebase-storage-ktx` - +This repository contains the source code for all Android Firebase SDKs except +Analytics and Auth. Firebase is an app development platform with tools to help you build, grow and monetize your app. More information about Firebase can be found at @@ -65,7 +35,7 @@ https://firebase.google.com. * Import the firebase-android-sdk gradle project into Android Studio using the **Import project(Gradle, Eclipse ADT, etc.)** option. * `firebase-crashlytics-ndk` must be built with NDK 21. See - [firebase-crashlytics-ndk](firebase-crashlytics-ndk/README.md) for more + [firebase-crashlytics-ndk](firebase-crashlytics-ndk/README.md) for more details. ## Testing @@ -143,7 +113,7 @@ Integration tests can be executed on the command line by running ``` This will execute tests on devices that are configured per project, if nothing is configured for the -project, the tests will run on `model=Pixel2,version=27,locale=en,orientation=portrait`. +project, the tests will run on `model=panther,version=33,locale=en,orientation=portrait`. Projects can be configured in the following way: @@ -225,13 +195,14 @@ strategy](https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.Resolutio ### Commands -The simplest way to publish a project and all its associated dependencies is to -just publish all projects. The following command builds SNAPSHOT dependencies of -all projects. All pom level dependencies within the published artifacts will -also point to SNAPSHOT versions that are co-published. +For more advanced use cases where developers wish to make changes to a project, +but have transitive dependencies point to publicly released versions, individual +projects may be published as follows. ```bash -./gradlew publishAllToLocal +# e.g. to publish Firestore and Functions +./gradlew -PprojectsToPublish="firebase-firestore,firebase-functions" \ + publishReleasingLibrariesToMavenLocal ``` Developers may take a dependency on these locally published versions by adding @@ -239,27 +210,23 @@ the `mavenLocal()` repository to your [repositories block](https://docs.gradle.org/current/userguide/declaring_repositories.html) in your app module's build.gradle. -For more advanced use cases where developers wish to make changes to a project, -but have transitive dependencies point to publicly released versions, individual -projects may be published as follows. +### Code Formatting -```bash -# e.g. to publish Firestore and Functions -./gradlew -PprojectsToPublish=":firebase-firestore,:firebase-functions" \ - publishProjectsToMavenLocal -``` +#### Java -### Code Formatting +N/A for now + + +#### Kotlin -Code in this repo is formatted with the google-java-format tool. You can enable +Kotlin code in this repo is formatted with the `ktfmt` tool. You can enable this formatting in Android Studio by downloading and installing the -[google-java-format plugin](https://github.com/google/google-java-format). -The plugin is disabled by default, but the repo contains configuration information -and links to additional plugins. +[ktfmt plugin](https://plugins.jetbrains.com/plugin/14912-ktfmt). +Enable the plugin in Preferences | Editor | ktfmt Settings. and set code style to Google (internal). To run formatting on your entire project you can run ```bash -./gradlew ::googleJavaFormat +./gradlew ::ktfmtFormat ``` ### Contributing diff --git a/alternative-root-project.gradle b/alternative-root-project.gradle deleted file mode 100644 index c202e9964f4..00000000000 --- a/alternative-root-project.gradle +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import com.google.firebase.gradle.MultiProjectReleasePlugin - -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - ext.kotlinVersion = '1.3.72' - repositories { - google() - jcenter() - mavenCentral() - maven { - url "https://plugins.gradle.org/m2/" - } - maven { - url 'https://storage.googleapis.com/android-ci/mvn/' - } - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.4.3' - classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.14' - classpath 'net.ltgt.gradle:gradle-errorprone-plugin:1.3.0' - classpath 'gradle.plugin.com.github.sherter.google-java-format:google-java-format-gradle-plugin:0.9' - classpath 'com.google.gms:google-services:4.3.3' - classpath 'digital.wup:android-maven-publish:3.6.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - classpath 'org.jlleitschuh.gradle:ktlint-gradle:9.2.1' - } -} - -apply from: 'sdkProperties.gradle' - -ext { - playServicesVersion = '16.0.1' - supportAnnotationsVersion = '28.0.0' - gMavenRoot = 'https://dl.google.com/dl/android/maven2' - firebaseDefaultPreguardFile='oss/default-preguard.txt' - errorproneVersion = '2.3.2' - errorproneJavacVersion = '9+181-r4173-1' - googleTruthVersion = '0.40' - -} - -apply from: 'build.gradle' - -apply plugin: MultiProjectReleasePlugin diff --git a/appcheck/firebase-appcheck-debug-testing/CHANGELOG.md b/appcheck/firebase-appcheck-debug-testing/CHANGELOG.md new file mode 100644 index 00000000000..afaeae8a097 --- /dev/null +++ b/appcheck/firebase-appcheck-debug-testing/CHANGELOG.md @@ -0,0 +1,64 @@ +# Unreleased + + +# 18.0.0 +* [changed] Bump internal dependencies + +# 17.1.2 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + +# 17.1.1 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + +# 17.1.0 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + +# 17.0.0 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + +# 16.1.2 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + +# 16.1.1 +* [changed] Integrated the [app_check] Debug Testing SDK with Firebase + components. + (GitHub [#4436](//github.com/firebase/firebase-android-sdk/issues/4436){: .external}) + +# 16.1.0 +* [unchanged] Updated to accommodate the release of the updated + [app_check] Kotlin extensions library. + +# 16.0.1 +* [changed] Updated dependency of `play-services-basement` to its latest + version (v18.1.0). + +# 16.0.0 +* [changed] [app_check] has exited beta and is now generally available for + use. + +# 16.0.0-beta06 +* [fixed] Fixed a bug in the [app_check] token refresh flow when using a + custom provider. + +# 16.0.0-beta05 +* [changed] Internal improvements. + +# 16.0.0-beta04 +* [changed] Improved error handling logic by minimizing the amount of requests + that are unlikely to succeed. +* [fixed] Fixed heartbeat reporting. + +# 16.0.0-beta03 +* [changed] Added `X-Android-Package` and `X-Android-Cert` request headers to + [app_check] network calls. + +# 16.0.0-beta02 +* [feature] Added [`getAppCheckToken()`](/docs/reference/android/com/google/firebase/appcheck/FirebaseAppCheck#getAppCheckToken(boolean)), + [`AppCheckTokenListener`](/docs/reference/android/com/google/firebase/appcheck/FirebaseAppCheck.AppCheckListener), + and associated setters and removers for developers to request and observe + changes to the [app_check] token. + +# 16.0.0-beta01 +* [feature] Initial beta release of the [app_check] Debug Testing SDK with + abuse reduction features. + diff --git a/appcheck/firebase-appcheck-debug-testing/api.txt b/appcheck/firebase-appcheck-debug-testing/api.txt new file mode 100644 index 00000000000..24535cd8bfb --- /dev/null +++ b/appcheck/firebase-appcheck-debug-testing/api.txt @@ -0,0 +1,15 @@ +// Signature format: 2.0 +package com.google.firebase.appcheck.debug.testing { + + public final class DebugAppCheckTestHelper { + method @NonNull public static com.google.firebase.appcheck.debug.testing.DebugAppCheckTestHelper fromInstrumentationArgs(); + method public void withDebugProvider(@NonNull com.google.firebase.appcheck.debug.testing.DebugAppCheckTestHelper.MaybeThrowingRunnable) throws E; + method public void withDebugProvider(@NonNull com.google.firebase.FirebaseApp, @NonNull com.google.firebase.appcheck.debug.testing.DebugAppCheckTestHelper.MaybeThrowingRunnable) throws E; + } + + public static interface DebugAppCheckTestHelper.MaybeThrowingRunnable { + method public void run() throws E; + } + +} + diff --git a/appcheck/firebase-appcheck-debug-testing/firebase-appcheck-debug-testing.gradle b/appcheck/firebase-appcheck-debug-testing/firebase-appcheck-debug-testing.gradle index c6ff822738c..84354a59255 100644 --- a/appcheck/firebase-appcheck-debug-testing/firebase-appcheck-debug-testing.gradle +++ b/appcheck/firebase-appcheck-debug-testing/firebase-appcheck-debug-testing.gradle @@ -14,9 +14,11 @@ plugins { id 'firebase-library' + id 'copy-google-services' } firebaseLibrary { + libraryGroup "appcheck" testLab.enabled = true publishSources = true } @@ -26,11 +28,13 @@ android { timeOutInMs 60 * 1000 } - compileSdkVersion project.targetSdkVersion + namespace "com.google.firebase.appcheck.debug.testing" + compileSdkVersion project.compileSdkVersion defaultConfig { targetSdkVersion project.targetSdkVersion minSdkVersion project.minSdkVersion versionName version + multiDexEnabled = true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArgument "firebaseAppCheckDebugSecret", System.getenv("FIREBASE_APP_CHECK_DEBUG_SECRET") ?: '' } @@ -38,38 +42,34 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - testOptions.unitTests.includeAndroidResources = true } dependencies { - implementation project(':firebase-common') - implementation project(':firebase-components') - implementation project(':appcheck:firebase-appcheck') - implementation project(':appcheck:firebase-appcheck-debug') - implementation project(':appcheck:firebase-appcheck-interop') + javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' + + api project(':appcheck:firebase-appcheck') + api project(':appcheck:firebase-appcheck-debug') + api 'com.google.firebase:firebase-appcheck-interop:17.0.0' + api 'com.google.firebase:firebase-common:21.0.0' + api 'com.google.firebase:firebase-common-ktx:21.0.0' + api 'com.google.firebase:firebase-components:18.0.0' + + implementation "androidx.test:core:$androidxTestCoreVersion" implementation 'com.google.android.gms:play-services-base:18.0.1' implementation 'com.google.android.gms:play-services-tasks:18.0.1' - implementation 'androidx.test:core:1.2.0' + testImplementation project(':appcheck:firebase-appcheck-playintegrity') + testImplementation "androidx.test:core:$androidxTestCoreVersion" + testImplementation "com.google.truth:truth:$googleTruthVersion" testImplementation 'junit:junit:4.13-beta-2' testImplementation 'org.mockito:mockito-core:2.25.0' testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation "com.google.truth:truth:$googleTruthVersion" - testImplementation 'androidx.test:core:1.2.0' - testImplementation project(':appcheck:firebase-appcheck-safetynet') androidTestImplementation project(':firebase-storage') - androidTestImplementation 'junit:junit:4.13-beta-2' - androidTestImplementation "com.google.truth:truth:$googleTruthVersion" - androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation "androidx.test.ext:junit:$androidxTestJUnitVersion" + androidTestImplementation "com.google.truth:truth:$googleTruthVersion" + androidTestImplementation 'junit:junit:4.13-beta-2' androidTestImplementation 'org.mockito:mockito-core:2.25.0' } - -// ========================================================================== -// Copy from here down if you want to use the google-services plugin in your -// androidTest integration tests. -// ========================================================================== -ext.packageName = "com.google.firebase.appcheck.debug.testing" -apply from: '../../gradle/googleServices.gradle' diff --git a/appcheck/firebase-appcheck-debug-testing/gradle.properties b/appcheck/firebase-appcheck-debug-testing/gradle.properties index 671ce6a5ae5..9b7be4891d1 100644 --- a/appcheck/firebase-appcheck-debug-testing/gradle.properties +++ b/appcheck/firebase-appcheck-debug-testing/gradle.properties @@ -1 +1,2 @@ -version=16.0.1 +version=18.0.1 +latestReleasedVersion=18.0.0 diff --git a/appcheck/firebase-appcheck-debug-testing/src/main/AndroidManifest.xml b/appcheck/firebase-appcheck-debug-testing/src/main/AndroidManifest.xml index 3426ff98472..b47f8ebc903 100644 --- a/appcheck/firebase-appcheck-debug-testing/src/main/AndroidManifest.xml +++ b/appcheck/firebase-appcheck-debug-testing/src/main/AndroidManifest.xml @@ -13,8 +13,14 @@ - + - + + + + + + diff --git a/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelper.java b/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelper.java index 82562258bf3..6473c636d78 100644 --- a/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelper.java +++ b/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelper.java @@ -15,13 +15,11 @@ package com.google.firebase.appcheck.debug.testing; import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import androidx.test.platform.app.InstrumentationRegistry; import com.google.firebase.FirebaseApp; import com.google.firebase.appcheck.AppCheckProviderFactory; import com.google.firebase.appcheck.FirebaseAppCheck; import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory; -import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactoryHelper; import com.google.firebase.appcheck.internal.DefaultFirebaseAppCheck; /** @@ -66,28 +64,16 @@ * */ public final class DebugAppCheckTestHelper { - private static final String DEBUG_SECRET_KEY = "firebaseAppCheckDebugSecret"; - - private final String debugSecret; - /** - * Creates a {@link DebugAppCheckTestHelper} instance with the debug secret obtained from {@link - * InstrumentationRegistry} arguments. + * Creates a {@link DebugAppCheckTestHelper} instance with a debug secret obtained from
+ * {@link InstrumentationRegistry} arguments. */ @NonNull public static DebugAppCheckTestHelper fromInstrumentationArgs() { - String debugSecret = InstrumentationRegistry.getArguments().getString(DEBUG_SECRET_KEY); - return new DebugAppCheckTestHelper(debugSecret); + return new DebugAppCheckTestHelper(); } - @VisibleForTesting - static DebugAppCheckTestHelper fromString(String debugSecret) { - return new DebugAppCheckTestHelper(debugSecret); - } - - private DebugAppCheckTestHelper(String debugSecret) { - this.debugSecret = debugSecret; - } + private DebugAppCheckTestHelper() {} /** * Installs a {@link DebugAppCheckProviderFactory} to the default {@link FirebaseApp} and runs the @@ -109,8 +95,7 @@ public void withDebugProvider( (DefaultFirebaseAppCheck) FirebaseAppCheck.getInstance(firebaseApp); AppCheckProviderFactory currentAppCheckProviderFactory = firebaseAppCheck.getInstalledAppCheckProviderFactory(); - firebaseAppCheck.installAppCheckProviderFactory( - DebugAppCheckProviderFactoryHelper.createDebugAppCheckProviderFactory(debugSecret)); + firebaseAppCheck.installAppCheckProviderFactory(DebugAppCheckProviderFactory.getInstance()); try { runnable.run(); } catch (Throwable throwable) { diff --git a/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/DebugSecretProvider.java b/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/DebugSecretProvider.java new file mode 100644 index 00000000000..c4c0caf74eb --- /dev/null +++ b/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/DebugSecretProvider.java @@ -0,0 +1,35 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.appcheck.debug.testing; + +import androidx.test.platform.app.InstrumentationRegistry; +import com.google.firebase.appcheck.debug.InternalDebugSecretProvider; + +/** @hide */ +public class DebugSecretProvider implements InternalDebugSecretProvider { + private static final String DEBUG_SECRET_KEY = "firebaseAppCheckDebugSecret"; + + DebugSecretProvider() {} + + /** + * Returns a debug secret from {@link InstrumentationRegistry} arguments to be used with the + * {@link com.google.firebase.appcheck.debug.internal.DebugAppCheckProvider} in continuous + * integration testing flows. + */ + @Override + public String getDebugSecret() { + return InstrumentationRegistry.getArguments().getString(DEBUG_SECRET_KEY); + } +} diff --git a/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/FirebaseAppCheckDebugTestingRegistrar.java b/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/FirebaseAppCheckDebugTestingRegistrar.java new file mode 100644 index 00000000000..d1951beb185 --- /dev/null +++ b/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/FirebaseAppCheckDebugTestingRegistrar.java @@ -0,0 +1,45 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.appcheck.debug.testing; + +import com.google.android.gms.common.annotation.KeepForSdk; +import com.google.firebase.appcheck.debug.BuildConfig; +import com.google.firebase.appcheck.debug.InternalDebugSecretProvider; +import com.google.firebase.components.Component; +import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.platforminfo.LibraryVersionComponent; +import java.util.Arrays; +import java.util.List; + +/** + * {@link ComponentRegistrar} for setting up FirebaseAppCheck debug testing's dependency injections + * in Firebase Android Components. + * + * @hide + */ +@KeepForSdk +public class FirebaseAppCheckDebugTestingRegistrar implements ComponentRegistrar { + private static final String LIBRARY_NAME = "fire-app-check-debug-testing"; + + @Override + public List> getComponents() { + return Arrays.asList( + Component.builder(DebugSecretProvider.class, (InternalDebugSecretProvider.class)) + .name(LIBRARY_NAME) + .factory((container) -> new DebugSecretProvider()) + .build(), + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); + } +} diff --git a/appcheck/firebase-appcheck-debug-testing/src/test/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelperTest.java b/appcheck/firebase-appcheck-debug-testing/src/test/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelperTest.java index 5e27218d25c..bf9224d3cb1 100644 --- a/appcheck/firebase-appcheck-debug-testing/src/test/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelperTest.java +++ b/appcheck/firebase-appcheck-debug-testing/src/test/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelperTest.java @@ -23,7 +23,7 @@ import com.google.firebase.appcheck.FirebaseAppCheck; import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory; import com.google.firebase.appcheck.internal.DefaultFirebaseAppCheck; -import com.google.firebase.appcheck.safetynet.SafetyNetAppCheckProviderFactory; +import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,10 +35,9 @@ public class DebugAppCheckTestHelperTest { private static final String PROJECT_ID = "projectId"; private static final String APP_ID = "appId"; private static final String OTHER_FIREBASE_APP_NAME = "otherFirebaseAppName"; - private static final String DEBUG_SECRET = "debugSecret"; private final DebugAppCheckTestHelper debugAppCheckTestHelper = - DebugAppCheckTestHelper.fromString(DEBUG_SECRET); + DebugAppCheckTestHelper.fromInstrumentationArgs(); @Before public void setUp() { @@ -52,11 +51,12 @@ public void setUp() { public void testDebugAppCheckTestHelper_withDebugProviderDefaultApp_installsDebugProvider() { DefaultFirebaseAppCheck firebaseAppCheck = (DefaultFirebaseAppCheck) FirebaseAppCheck.getInstance(); - firebaseAppCheck.installAppCheckProviderFactory(SafetyNetAppCheckProviderFactory.getInstance()); + firebaseAppCheck.installAppCheckProviderFactory( + PlayIntegrityAppCheckProviderFactory.getInstance()); // Sanity check assertThat(firebaseAppCheck.getInstalledAppCheckProviderFactory()) - .isEqualTo(SafetyNetAppCheckProviderFactory.getInstance()); + .isEqualTo(PlayIntegrityAppCheckProviderFactory.getInstance()); debugAppCheckTestHelper.withDebugProvider( () -> { @@ -66,7 +66,7 @@ public void testDebugAppCheckTestHelper_withDebugProviderDefaultApp_installsDebu // Make sure the factory is reset. assertThat(firebaseAppCheck.getInstalledAppCheckProviderFactory()) - .isEqualTo(SafetyNetAppCheckProviderFactory.getInstance()); + .isEqualTo(PlayIntegrityAppCheckProviderFactory.getInstance()); } @Test @@ -74,11 +74,12 @@ public void testDebugAppCheckTestHelper_withDebugProviderNamedApp_installsDebugP FirebaseApp firebaseApp = FirebaseApp.getInstance(OTHER_FIREBASE_APP_NAME); DefaultFirebaseAppCheck firebaseAppCheck = (DefaultFirebaseAppCheck) FirebaseAppCheck.getInstance(firebaseApp); - firebaseAppCheck.installAppCheckProviderFactory(SafetyNetAppCheckProviderFactory.getInstance()); + firebaseAppCheck.installAppCheckProviderFactory( + PlayIntegrityAppCheckProviderFactory.getInstance()); // Sanity check assertThat(firebaseAppCheck.getInstalledAppCheckProviderFactory()) - .isEqualTo(SafetyNetAppCheckProviderFactory.getInstance()); + .isEqualTo(PlayIntegrityAppCheckProviderFactory.getInstance()); debugAppCheckTestHelper.withDebugProvider( firebaseApp, @@ -89,7 +90,7 @@ public void testDebugAppCheckTestHelper_withDebugProviderNamedApp_installsDebugP // Make sure the factory is reset. assertThat(firebaseAppCheck.getInstalledAppCheckProviderFactory()) - .isEqualTo(SafetyNetAppCheckProviderFactory.getInstance()); + .isEqualTo(PlayIntegrityAppCheckProviderFactory.getInstance()); } @Test @@ -102,10 +103,9 @@ public void testDebugAppCheckTestHelper_withDebugProviderNamedApp_installsDebugP assertThat(firebaseAppCheck.getInstalledAppCheckProviderFactory()).isNull(); debugAppCheckTestHelper.withDebugProvider( - () -> { - assertThat(firebaseAppCheck.getInstalledAppCheckProviderFactory()) - .isInstanceOf(DebugAppCheckProviderFactory.class); - }); + () -> + assertThat(firebaseAppCheck.getInstalledAppCheckProviderFactory()) + .isInstanceOf(DebugAppCheckProviderFactory.class)); // Make sure the factory is reset. assertThat(firebaseAppCheck.getInstalledAppCheckProviderFactory()).isNull(); diff --git a/appcheck/firebase-appcheck-debug-testing/src/test/java/com/google/firebase/appcheck/debug/testing/FirebaseAppCheckDebugTestingRegistrarTest.java b/appcheck/firebase-appcheck-debug-testing/src/test/java/com/google/firebase/appcheck/debug/testing/FirebaseAppCheckDebugTestingRegistrarTest.java new file mode 100644 index 00000000000..05eadd44d4b --- /dev/null +++ b/appcheck/firebase-appcheck-debug-testing/src/test/java/com/google/firebase/appcheck/debug/testing/FirebaseAppCheckDebugTestingRegistrarTest.java @@ -0,0 +1,38 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.appcheck.debug.testing; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.firebase.components.Component; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link FirebaseAppCheckDebugTestingRegistrar}. */ +@RunWith(RobolectricTestRunner.class) +public class FirebaseAppCheckDebugTestingRegistrarTest { + @Test + public void testGetComponents() { + FirebaseAppCheckDebugTestingRegistrar registrar = new FirebaseAppCheckDebugTestingRegistrar(); + List> components = registrar.getComponents(); + assertThat(components).isNotEmpty(); + assertThat(components).hasSize(2); + Component appCheckDebugTestingComponent = components.get(0); + assertThat(appCheckDebugTestingComponent.getDependencies()).isEmpty(); + assertThat(appCheckDebugTestingComponent.isLazy()).isTrue(); + } +} diff --git a/appcheck/firebase-appcheck-debug/CHANGELOG.md b/appcheck/firebase-appcheck-debug/CHANGELOG.md new file mode 100644 index 00000000000..ced2dde64d0 --- /dev/null +++ b/appcheck/firebase-appcheck-debug/CHANGELOG.md @@ -0,0 +1,69 @@ +# Unreleased + + +# 18.0.0 +* [changed] Bump internal dependencies + +# 17.1.2 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + +# 17.1.1 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + +# 17.1.0 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + +# 17.0.0 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + +# 16.1.2 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + +# 16.1.1 +* [changed] Migrated [app_check] SDKs to use standard Firebase executors. + (GitHub [#4431](//github.com/firebase/firebase-android-sdk/issues/4431){: .external} + and + [#4449](//github.com/firebase/firebase-android-sdk/issues/4449){: .external}) +* [changed] Integrated the [app_check] Debug SDK with Firebase components. + (GitHub [#4436](//github.com/firebase/firebase-android-sdk/issues/4436){: .external}) +* [changed] Moved Task continuations off the main thread. + (GitHub [#4453](//github.com/firebase/firebase-android-sdk/issues/4453){: .external}) + +# 16.1.0 +* [unchanged] Updated to accommodate the release of the updated + [app_check] Kotlin extensions library. + +# 16.0.1 +* [changed] Updated dependency of `play-services-basement` to its latest + version (v18.1.0). + +# 16.0.0 +* [changed] [app_check] has exited beta and is now generally available for + use. + +# 16.0.0-beta06 +* [fixed] Fixed a bug in the [app_check] token refresh flow when using a + custom provider. + +# 16.0.0-beta05 +* [changed] Internal improvements. + +# 16.0.0-beta04 +* [changed] Improved error handling logic by minimizing the amount of requests + that are unlikely to succeed. +* [fixed] Fixed heartbeat reporting. + +# 16.0.0-beta03 +* [changed] Added `X-Android-Package` and `X-Android-Cert` request headers to + [app_check] network calls. + +# 16.0.0-beta02 +* [feature] Added [`getAppCheckToken()`](/docs/reference/android/com/google/firebase/appcheck/FirebaseAppCheck#getAppCheckToken(boolean)), + [`AppCheckTokenListener`](/docs/reference/android/com/google/firebase/appcheck/FirebaseAppCheck.AppCheckListener), + and associated setters and removers for developers to request and observe + changes to the [app_check] token. + +# 16.0.0-beta01 +* [feature] Initial beta release of the [app_check] Debug SDK with abuse + reduction features. + diff --git a/appcheck/firebase-appcheck-debug/api.txt b/appcheck/firebase-appcheck-debug/api.txt new file mode 100644 index 00000000000..a98578ea9da --- /dev/null +++ b/appcheck/firebase-appcheck-debug/api.txt @@ -0,0 +1,10 @@ +// Signature format: 2.0 +package com.google.firebase.appcheck.debug { + + public class DebugAppCheckProviderFactory implements com.google.firebase.appcheck.AppCheckProviderFactory { + method @NonNull public com.google.firebase.appcheck.AppCheckProvider create(@NonNull com.google.firebase.FirebaseApp); + method @NonNull public static com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory getInstance(); + } + +} + diff --git a/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle b/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle index 80cad7da537..d92893190ac 100644 --- a/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle +++ b/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle @@ -17,6 +17,7 @@ plugins { } firebaseLibrary { + libraryGroup "appcheck" publishSources = true } @@ -25,7 +26,8 @@ android { timeOutInMs 60 * 1000 } - compileSdkVersion project.targetSdkVersion + namespace "com.google.firebase.appcheck.debug" + compileSdkVersion project.compileSdkVersion defaultConfig { targetSdkVersion project.targetSdkVersion minSdkVersion project.minSdkVersion @@ -41,18 +43,25 @@ android { } dependencies { - implementation project(':firebase-common') - implementation project(':firebase-components') - implementation project(':appcheck:firebase-appcheck') + javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' + + api project(':appcheck:firebase-appcheck') + api 'com.google.firebase:firebase-annotations:16.2.0' + api 'com.google.firebase:firebase-common:21.0.0' + api 'com.google.firebase:firebase-common-ktx:21.0.0' + api 'com.google.firebase:firebase-components:18.0.0' + implementation 'com.google.android.gms:play-services-base:18.0.1' implementation 'com.google.android.gms:play-services-tasks:18.0.1' - javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' - + testImplementation(project(":integ-testing")){ + exclude group: 'com.google.firebase', module: 'firebase-common' + exclude group: 'com.google.firebase', module: 'firebase-components' + } + testImplementation "androidx.test:core:$androidxTestCoreVersion" + testImplementation 'androidx.test:rules:1.2.0' + testImplementation "com.google.truth:truth:$googleTruthVersion" testImplementation 'junit:junit:4.13-beta-2' testImplementation 'org.mockito:mockito-core:2.25.0' testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation "com.google.truth:truth:$googleTruthVersion" - testImplementation 'androidx.test:core:1.2.0' - testImplementation 'androidx.test:rules:1.2.0' } diff --git a/appcheck/firebase-appcheck-debug/gradle.properties b/appcheck/firebase-appcheck-debug/gradle.properties index 671ce6a5ae5..9b7be4891d1 100644 --- a/appcheck/firebase-appcheck-debug/gradle.properties +++ b/appcheck/firebase-appcheck-debug/gradle.properties @@ -1 +1,2 @@ -version=16.0.1 +version=18.0.1 +latestReleasedVersion=18.0.0 diff --git a/appcheck/firebase-appcheck-debug/src/main/AndroidManifest.xml b/appcheck/firebase-appcheck-debug/src/main/AndroidManifest.xml index 8a9c55b0be3..1366ef5d185 100644 --- a/appcheck/firebase-appcheck-debug/src/main/AndroidManifest.xml +++ b/appcheck/firebase-appcheck-debug/src/main/AndroidManifest.xml @@ -13,8 +13,14 @@ - + - + + + + + + diff --git a/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/DebugAppCheckProviderFactory.java b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/DebugAppCheckProviderFactory.java index d2135dc78d8..a2f4b148439 100644 --- a/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/DebugAppCheckProviderFactory.java +++ b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/DebugAppCheckProviderFactory.java @@ -18,35 +18,23 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.appcheck.AppCheckProvider; import com.google.firebase.appcheck.AppCheckProviderFactory; +import com.google.firebase.appcheck.FirebaseAppCheck; import com.google.firebase.appcheck.debug.internal.DebugAppCheckProvider; /** - * Implementation of an {@link AppCheckProviderFactory} that builds {@link DebugAppCheckProvider}s. + * Implementation of an {@link AppCheckProviderFactory} that builds {@code DebugAppCheckProvider}s. */ public class DebugAppCheckProviderFactory implements AppCheckProviderFactory { private static final DebugAppCheckProviderFactory instance = new DebugAppCheckProviderFactory(); - private String debugSecret; - - private DebugAppCheckProviderFactory() { - this.debugSecret = null; - } - - /** - * This constructor is package-private in order to prevent debug secrets from being hard-coded in - * application logic. This constructor is used by the firebase-appcheck-debug-testing SDK, to - * inject debug secrets in integration tests. - */ - DebugAppCheckProviderFactory(String debugSecret) { - this.debugSecret = debugSecret; - } + private DebugAppCheckProviderFactory() {} /** - * Gets an instance of this class for installation into a {@link - * com.google.firebase.appcheck.FirebaseAppCheck} instance. If no debug secret is found in {@link - * android.content.SharedPreferences}, a new debug secret will be generated and printed to the - * logcat. The debug secret should then be added to the allow list in the Firebase Console. + * Gets an instance of this class for installation into a {@link FirebaseAppCheck} instance. If no + * debug secret is found in {@link android.content.SharedPreferences}, a new debug secret will be + * generated and printed to the logcat. The debug secret should then be added to the allow list in + * the Firebase Console. */ @NonNull public static DebugAppCheckProviderFactory getInstance() { @@ -55,7 +43,8 @@ public static DebugAppCheckProviderFactory getInstance() { @NonNull @Override + @SuppressWarnings("FirebaseUseExplicitDependencies") public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) { - return new DebugAppCheckProvider(firebaseApp, debugSecret); + return firebaseApp.get(DebugAppCheckProvider.class); } } diff --git a/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/FirebaseAppCheckDebugRegistrar.java b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/FirebaseAppCheckDebugRegistrar.java new file mode 100644 index 00000000000..9eb96cd0891 --- /dev/null +++ b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/FirebaseAppCheckDebugRegistrar.java @@ -0,0 +1,67 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.appcheck.debug; + +import com.google.android.gms.common.annotation.KeepForSdk; +import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.appcheck.debug.internal.DebugAppCheckProvider; +import com.google.firebase.components.Component; +import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; +import com.google.firebase.platforminfo.LibraryVersionComponent; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * {@link ComponentRegistrar} for setting up FirebaseAppCheck debug's dependency injections in + * Firebase Android Components. + * + * @hide + */ +@KeepForSdk +public class FirebaseAppCheckDebugRegistrar implements ComponentRegistrar { + private static final String LIBRARY_NAME = "fire-app-check-debug"; + + @Override + public List> getComponents() { + Qualified liteExecutor = Qualified.qualified(Lightweight.class, Executor.class); + Qualified backgroundExecutor = Qualified.qualified(Background.class, Executor.class); + Qualified blockingExecutor = Qualified.qualified(Blocking.class, Executor.class); + + return Arrays.asList( + Component.builder(DebugAppCheckProvider.class) + .name(LIBRARY_NAME) + .add(Dependency.required(FirebaseApp.class)) + .add(Dependency.optionalProvider(InternalDebugSecretProvider.class)) + .add(Dependency.required(liteExecutor)) + .add(Dependency.required(backgroundExecutor)) + .add(Dependency.required(blockingExecutor)) + .factory( + (container) -> + new DebugAppCheckProvider( + container.get(FirebaseApp.class), + container.getProvider(InternalDebugSecretProvider.class), + container.get(liteExecutor), + container.get(backgroundExecutor), + container.get(blockingExecutor))) + .build(), + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); + } +} diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/model/Settings.java b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/InternalDebugSecretProvider.java similarity index 61% rename from firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/model/Settings.java rename to appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/InternalDebugSecretProvider.java index c74b2e6d851..7b3a196abc4 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/model/Settings.java +++ b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/InternalDebugSecretProvider.java @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,18 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.crashlytics.internal.settings.model; +package com.google.firebase.appcheck.debug; -public interface Settings { - SessionSettingsData getSessionData(); +import androidx.annotation.Nullable; - FeaturesSettingsData getFeaturesData(); - - long getExpiresAtMillis(); - - int getSettingsVersion(); - - int getCacheDuration(); - - boolean isExpired(long currentTimeMillis); +/** + * An interface for obtaining a debug secret to be used with {@link + * com.google.firebase.appcheck.debug.internal.DebugAppCheckProvider}. + * + * @hide + */ +public interface InternalDebugSecretProvider { + @Nullable + String getDebugSecret(); } diff --git a/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProvider.java b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProvider.java index 4f23d52951a..443b126db92 100644 --- a/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProvider.java +++ b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProvider.java @@ -18,22 +18,23 @@ import android.util.Log; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; import com.google.firebase.appcheck.AppCheckProvider; import com.google.firebase.appcheck.AppCheckToken; -import com.google.firebase.appcheck.internal.AppCheckTokenResponse; +import com.google.firebase.appcheck.debug.InternalDebugSecretProvider; import com.google.firebase.appcheck.internal.DefaultAppCheckToken; import com.google.firebase.appcheck.internal.NetworkClient; import com.google.firebase.appcheck.internal.RetryManager; +import com.google.firebase.inject.Provider; import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.Executor; public class DebugAppCheckProvider implements AppCheckProvider { @@ -41,18 +42,30 @@ public class DebugAppCheckProvider implements AppCheckProvider { private static final String UTF_8 = "UTF-8"; private final NetworkClient networkClient; - private final ExecutorService backgroundExecutor; + private final Executor liteExecutor; + private final Executor blockingExecutor; private final RetryManager retryManager; private final Task debugSecretTask; - public DebugAppCheckProvider(@NonNull FirebaseApp firebaseApp, @Nullable String debugSecret) { + public DebugAppCheckProvider( + @NonNull FirebaseApp firebaseApp, + @NonNull Provider debugSecretProvider, + @Lightweight Executor liteExecutor, + @Background Executor backgroundExecutor, + @Blocking Executor blockingExecutor) { checkNotNull(firebaseApp); this.networkClient = new NetworkClient(firebaseApp); - this.backgroundExecutor = Executors.newCachedThreadPool(); + this.liteExecutor = liteExecutor; + this.blockingExecutor = blockingExecutor; this.retryManager = new RetryManager(); + + String debugSecret = null; + if (debugSecretProvider.get() != null) { + debugSecret = debugSecretProvider.get().getDebugSecret(); + } this.debugSecretTask = debugSecret == null - ? determineDebugSecret(firebaseApp, this.backgroundExecutor) + ? determineDebugSecret(firebaseApp, backgroundExecutor) : Tasks.forResult(debugSecret); } @@ -60,10 +73,12 @@ public DebugAppCheckProvider(@NonNull FirebaseApp firebaseApp, @Nullable String DebugAppCheckProvider( @NonNull String debugSecret, @NonNull NetworkClient networkClient, - @NonNull ExecutorService backgroundExecutor, + @NonNull Executor liteExecutor, + @NonNull Executor blockingExecutor, @NonNull RetryManager retryManager) { this.networkClient = networkClient; - this.backgroundExecutor = backgroundExecutor; + this.liteExecutor = liteExecutor; + this.blockingExecutor = blockingExecutor; this.retryManager = retryManager; this.debugSecretTask = Tasks.forResult(debugSecret); } @@ -71,7 +86,7 @@ public DebugAppCheckProvider(@NonNull FirebaseApp firebaseApp, @Nullable String @VisibleForTesting @NonNull static Task determineDebugSecret( - @NonNull FirebaseApp firebaseApp, @NonNull ExecutorService executor) { + @NonNull FirebaseApp firebaseApp, @NonNull Executor executor) { TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); executor.execute( () -> { @@ -96,31 +111,21 @@ static Task determineDebugSecret( @Override public Task getToken() { return debugSecretTask - .continueWithTask( - new Continuation>() { - @Override - public Task then(@NonNull Task task) throws Exception { - ExchangeDebugTokenRequest request = new ExchangeDebugTokenRequest(task.getResult()); - return Tasks.call( - backgroundExecutor, - () -> - networkClient.exchangeAttestationForAppCheckToken( - request.toJsonString().getBytes(UTF_8), - NetworkClient.DEBUG, - retryManager)); - } + .onSuccessTask( + liteExecutor, + debugSecret -> { + ExchangeDebugTokenRequest request = new ExchangeDebugTokenRequest(debugSecret); + return Tasks.call( + blockingExecutor, + () -> + networkClient.exchangeAttestationForAppCheckToken( + request.toJsonString().getBytes(UTF_8), + NetworkClient.DEBUG, + retryManager)); }) - .continueWithTask( - new Continuation>() { - @Override - public Task then(@NonNull Task task) { - if (task.isSuccessful()) { - return Tasks.forResult( - DefaultAppCheckToken.constructFromAppCheckTokenResponse(task.getResult())); - } - // TODO: Surface more error details. - return Tasks.forException(task.getException()); - } - }); + .onSuccessTask( + liteExecutor, + response -> + Tasks.forResult(DefaultAppCheckToken.constructFromAppCheckTokenResponse(response))); } } diff --git a/appcheck/firebase-appcheck-debug/src/test/java/com/google/firebase/appcheck/debug/FirebaseAppCheckDebugRegistrarTest.java b/appcheck/firebase-appcheck-debug/src/test/java/com/google/firebase/appcheck/debug/FirebaseAppCheckDebugRegistrarTest.java new file mode 100644 index 00000000000..70301ee2eab --- /dev/null +++ b/appcheck/firebase-appcheck-debug/src/test/java/com/google/firebase/appcheck/debug/FirebaseAppCheckDebugRegistrarTest.java @@ -0,0 +1,51 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.appcheck.debug; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.components.Component; +import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; +import java.util.List; +import java.util.concurrent.Executor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link FirebaseAppCheckDebugRegistrar}. */ +@RunWith(RobolectricTestRunner.class) +public class FirebaseAppCheckDebugRegistrarTest { + @Test + public void testGetComponents() { + FirebaseAppCheckDebugRegistrar registrar = new FirebaseAppCheckDebugRegistrar(); + List> components = registrar.getComponents(); + assertThat(components).isNotEmpty(); + assertThat(components).hasSize(2); + Component appCheckDebugComponent = components.get(0); + assertThat(appCheckDebugComponent.getDependencies()) + .containsExactly( + Dependency.required(FirebaseApp.class), + Dependency.optionalProvider(InternalDebugSecretProvider.class), + Dependency.required(Qualified.qualified(Lightweight.class, Executor.class)), + Dependency.required(Qualified.qualified(Background.class, Executor.class)), + Dependency.required(Qualified.qualified(Blocking.class, Executor.class))); + assertThat(appCheckDebugComponent.isLazy()).isTrue(); + } +} diff --git a/appcheck/firebase-appcheck-debug/src/test/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProviderTest.java b/appcheck/firebase-appcheck-debug/src/test/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProviderTest.java index c3c3b200db5..d7680b94b56 100644 --- a/appcheck/firebase-appcheck-debug/src/test/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProviderTest.java +++ b/appcheck/firebase-appcheck-debug/src/test/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProviderTest.java @@ -33,8 +33,9 @@ import com.google.firebase.appcheck.internal.DefaultAppCheckToken; import com.google.firebase.appcheck.internal.NetworkClient; import com.google.firebase.appcheck.internal.RetryManager; +import com.google.firebase.concurrent.TestOnlyExecutors; import java.io.IOException; -import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executor; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -43,14 +44,16 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; /** Tests for {@link DebugAppCheckProvider}. */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) +@LooperMode(LooperMode.Mode.LEGACY) public class DebugAppCheckProviderTest { private static final String DEBUG_SECRET = "debugSecret"; - private static final String ATTESTATION_TOKEN = "token"; + private static final String APP_CHECK_TOKEN = "appCheckToken"; private static final String TIME_TO_LIVE = "3600s"; private static final String API_KEY = "apiKey"; private static final String APP_ID = "appId"; @@ -70,7 +73,10 @@ public class DebugAppCheckProviderTest { private StorageHelper storageHelper; private SharedPreferences sharedPreferences; - private ExecutorService backgroundExecutor = MoreExecutors.newDirectExecutorService(); + // TODO(b/258273630): Use TestOnlyExecutors instead of MoreExecutors.directExecutor(). + private Executor liteExecutor = MoreExecutors.directExecutor(); + private Executor backgroundExecutor = MoreExecutors.directExecutor(); + private Executor blockingExecutor = MoreExecutors.directExecutor(); @Before public void setup() { @@ -98,7 +104,12 @@ public void testPublicConstructor_nullFirebaseApp_expectThrows() { assertThrows( NullPointerException.class, () -> { - new DebugAppCheckProvider(null, null); + new DebugAppCheckProvider( + null, + null, + TestOnlyExecutors.lite(), + TestOnlyExecutors.background(), + TestOnlyExecutors.blocking()); }); } @@ -128,12 +139,12 @@ public void exchangeDebugToken_onSuccess_setsTaskResult() throws Exception { when(mockNetworkClient.exchangeAttestationForAppCheckToken( any(), eq(NetworkClient.DEBUG), eq(mockRetryManager))) .thenReturn(mockAppCheckTokenResponse); - when(mockAppCheckTokenResponse.getAttestationToken()).thenReturn(ATTESTATION_TOKEN); + when(mockAppCheckTokenResponse.getToken()).thenReturn(APP_CHECK_TOKEN); when(mockAppCheckTokenResponse.getTimeToLive()).thenReturn(TIME_TO_LIVE); DebugAppCheckProvider provider = new DebugAppCheckProvider( - DEBUG_SECRET, mockNetworkClient, backgroundExecutor, mockRetryManager); + DEBUG_SECRET, mockNetworkClient, liteExecutor, blockingExecutor, mockRetryManager); Task task = provider.getToken(); verify(mockNetworkClient) @@ -141,7 +152,7 @@ public void exchangeDebugToken_onSuccess_setsTaskResult() throws Exception { AppCheckToken token = task.getResult(); assertThat(token).isInstanceOf(DefaultAppCheckToken.class); - assertThat(token.getToken()).isEqualTo(ATTESTATION_TOKEN); + assertThat(token.getToken()).isEqualTo(APP_CHECK_TOKEN); } @Test @@ -152,7 +163,7 @@ public void exchangeDebugToken_onFailure_setsTaskException() throws Exception { DebugAppCheckProvider provider = new DebugAppCheckProvider( - DEBUG_SECRET, mockNetworkClient, backgroundExecutor, mockRetryManager); + DEBUG_SECRET, mockNetworkClient, liteExecutor, blockingExecutor, mockRetryManager); Task task = provider.getToken(); verify(mockNetworkClient) diff --git a/appcheck/firebase-appcheck-interop/CHANGELOG.md b/appcheck/firebase-appcheck-interop/CHANGELOG.md new file mode 100644 index 00000000000..f514bbb890e --- /dev/null +++ b/appcheck/firebase-appcheck-interop/CHANGELOG.md @@ -0,0 +1,3 @@ +# Unreleased + + diff --git a/appcheck/firebase-appcheck-interop/api.txt b/appcheck/firebase-appcheck-interop/api.txt new file mode 100644 index 00000000000..4ad12343fcf --- /dev/null +++ b/appcheck/firebase-appcheck-interop/api.txt @@ -0,0 +1,19 @@ +// Signature format: 2.0 +package com.google.firebase.appcheck { + + public abstract class AppCheckTokenResult { + ctor public AppCheckTokenResult(); + method @Nullable public abstract Exception getError(); + method @NonNull public abstract String getToken(); + } + +} + +package com.google.firebase.appcheck.interop { + + public interface AppCheckTokenListener { + method public void onAppCheckTokenChanged(@NonNull com.google.firebase.appcheck.AppCheckTokenResult); + } + +} + diff --git a/appcheck/firebase-appcheck-interop/firebase-appcheck-interop.gradle b/appcheck/firebase-appcheck-interop/firebase-appcheck-interop.gradle index a916b61250f..eeadcd3ea16 100644 --- a/appcheck/firebase-appcheck-interop/firebase-appcheck-interop.gradle +++ b/appcheck/firebase-appcheck-interop/firebase-appcheck-interop.gradle @@ -19,6 +19,7 @@ plugins { firebaseLibrary { publishSources = true publishJavadoc = false + publishReleaseNotes = false } android { @@ -26,7 +27,8 @@ android { timeOutInMs 60 * 1000 } - compileSdkVersion project.targetSdkVersion + namespace "com.google.firebase.appcheck.interop" + compileSdkVersion project.compileSdkVersion defaultConfig { targetSdkVersion project.targetSdkVersion minSdkVersion project.minSdkVersion @@ -42,15 +44,13 @@ android { } dependencies { - implementation project(':firebase-common') - implementation project(':firebase-components') implementation 'com.google.android.gms:play-services-base:18.0.1' implementation 'com.google.android.gms:play-services-tasks:18.0.1' + testImplementation "androidx.test:core:$androidxTestCoreVersion" + testImplementation 'androidx.test:rules:1.2.0' + testImplementation "com.google.truth:truth:$googleTruthVersion" testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.25.0' testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation "com.google.truth:truth:$googleTruthVersion" - testImplementation 'androidx.test:core:1.2.0' - testImplementation 'androidx.test:rules:1.2.0' } diff --git a/appcheck/firebase-appcheck-interop/gradle.properties b/appcheck/firebase-appcheck-interop/gradle.properties index 6d884a28d65..0b498a0491d 100644 --- a/appcheck/firebase-appcheck-interop/gradle.properties +++ b/appcheck/firebase-appcheck-interop/gradle.properties @@ -1,2 +1,2 @@ -version=16.0.1 -latestReleasedVersion=16.0.0-beta02 +version=18.0.0 +latestReleasedVersion=17.1.0 diff --git a/appcheck/firebase-appcheck-interop/src/main/AndroidManifest.xml b/appcheck/firebase-appcheck-interop/src/main/AndroidManifest.xml index 4e8353021db..bcdf9a2c8cb 100644 --- a/appcheck/firebase-appcheck-interop/src/main/AndroidManifest.xml +++ b/appcheck/firebase-appcheck-interop/src/main/AndroidManifest.xml @@ -13,9 +13,8 @@ - + - + - \ No newline at end of file + diff --git a/appcheck/firebase-appcheck-interop/src/main/java/com/google/firebase/appcheck/AppCheckTokenResult.java b/appcheck/firebase-appcheck-interop/src/main/java/com/google/firebase/appcheck/AppCheckTokenResult.java index 06d49d163a7..fd183d40292 100644 --- a/appcheck/firebase-appcheck-interop/src/main/java/com/google/firebase/appcheck/AppCheckTokenResult.java +++ b/appcheck/firebase-appcheck-interop/src/main/java/com/google/firebase/appcheck/AppCheckTokenResult.java @@ -16,13 +16,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.firebase.FirebaseException; /** * Class to hold the result emitted by a Firebase App Check service application verification * attempt. The result will always contain a token, which will either be a valid raw JWT attesting - * application identity, or a dummy value. The result may optionally contain a {@link - * FirebaseException} if application verification does not succeed. + * application identity, or a dummy value. The result may optionally contain an {@link Exception} if + * application verification does not succeed. */ public abstract class AppCheckTokenResult { @@ -34,9 +33,9 @@ public abstract class AppCheckTokenResult { public abstract String getToken(); /** - * Returns the {@link FirebaseException} if the {@link - * com.google.firebase.appcheck.interop.InternalAppCheckTokenProvider} failed to obtain a token. + * Returns the {@link Exception} if the {@link + * com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider} failed to obtain a token. */ @Nullable - public abstract FirebaseException getError(); + public abstract Exception getError(); } diff --git a/appcheck/firebase-appcheck-interop/src/main/java/com/google/firebase/appcheck/interop/InternalAppCheckTokenProvider.java b/appcheck/firebase-appcheck-interop/src/main/java/com/google/firebase/appcheck/interop/InteropAppCheckTokenProvider.java similarity index 78% rename from appcheck/firebase-appcheck-interop/src/main/java/com/google/firebase/appcheck/interop/InternalAppCheckTokenProvider.java rename to appcheck/firebase-appcheck-interop/src/main/java/com/google/firebase/appcheck/interop/InteropAppCheckTokenProvider.java index c8179322994..1fde5c7c8cb 100644 --- a/appcheck/firebase-appcheck-interop/src/main/java/com/google/firebase/appcheck/interop/InternalAppCheckTokenProvider.java +++ b/appcheck/firebase-appcheck-interop/src/main/java/com/google/firebase/appcheck/interop/InteropAppCheckTokenProvider.java @@ -18,7 +18,8 @@ import com.google.android.gms.tasks.Task; import com.google.firebase.appcheck.AppCheckTokenResult; -public interface InternalAppCheckTokenProvider { +/** @hide */ +public interface InteropAppCheckTokenProvider { /** * Requests an {@link AppCheckTokenResult} from the installed {@code AppCheckFactory}. This will @@ -28,6 +29,15 @@ public interface InternalAppCheckTokenProvider { @NonNull Task getToken(boolean forceRefresh); + /** + * Requests an {@link AppCheckTokenResult} from the installed {@code AppCheckFactory}. This will + * always return a successful task, with an {@link AppCheckTokenResult} that contains either a + * valid token, or a dummy token and an error string. The token returned from this method will be + * a one-time use token. + */ + @NonNull + Task getLimitedUseToken(); + /** * Registers a listener to changes in the token state. There can be more than one listener * registered at the same time for one or more FirebaseAppAuth instances. The listeners call back diff --git a/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md b/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md new file mode 100644 index 00000000000..391a2d955f3 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md @@ -0,0 +1,46 @@ +# Unreleased + + +# 18.0.0 +* [changed] Bump internal dependencies + +# 17.1.2 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + +# 17.1.1 +* [fixed] Fixed client-side throttling in Play Integrity flows. +* [changed] Bumped Play Integrity API Library dependency version. + +# 17.1.0 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + +# 17.0.0 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + +# 16.1.2 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + +# 16.1.1 +* [changed] Migrated [app_check] SDKs to use standard Firebase executors. + (GitHub [#4431](//github.com/firebase/firebase-android-sdk/issues/4431){: .external} + and + [#4449](//github.com/firebase/firebase-android-sdk/issues/4449){: .external}) +* [changed] Integrated the [app_check] Play integrity SDK with Firebase + components. + (GitHub [#4436](//github.com/firebase/firebase-android-sdk/issues/4436){: .external}) +* [changed] Moved Task continuations off the main thread. + (GitHub [#4453](//github.com/firebase/firebase-android-sdk/issues/4453){: .external}) + +# 16.1.0 +* [unchanged] Updated to accommodate the release of the updated + [app_check] Kotlin extensions library. + +# 16.0.1 +* [changed] Updated dependency of `play-services-basement` to its latest + version (v18.1.0). + +# 16.0.0 +* [feature] Added support for + [Play Integrity](https://developer.android.com/google/play/integrity) as an + attestation provider. + diff --git a/appcheck/firebase-appcheck-playintegrity/api.txt b/appcheck/firebase-appcheck-playintegrity/api.txt new file mode 100644 index 00000000000..41646391f7b --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/api.txt @@ -0,0 +1,11 @@ +// Signature format: 2.0 +package com.google.firebase.appcheck.playintegrity { + + public class PlayIntegrityAppCheckProviderFactory implements com.google.firebase.appcheck.AppCheckProviderFactory { + ctor public PlayIntegrityAppCheckProviderFactory(); + method @NonNull public com.google.firebase.appcheck.AppCheckProvider create(@NonNull com.google.firebase.FirebaseApp); + method @NonNull public static com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory getInstance(); + } + +} + diff --git a/appcheck/firebase-appcheck-safetynet/firebase-appcheck-safetynet.gradle b/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle similarity index 63% rename from appcheck/firebase-appcheck-safetynet/firebase-appcheck-safetynet.gradle rename to appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle index fee97970023..634360cc079 100644 --- a/appcheck/firebase-appcheck-safetynet/firebase-appcheck-safetynet.gradle +++ b/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ plugins { } firebaseLibrary { + libraryGroup "appcheck" publishSources = true } @@ -25,7 +26,8 @@ android { timeOutInMs 60 * 1000 } - compileSdkVersion project.targetSdkVersion + namespace "com.google.firebase.appcheck.playintegrity" + compileSdkVersion project.compileSdkVersion defaultConfig { targetSdkVersion project.targetSdkVersion minSdkVersion project.minSdkVersion @@ -41,19 +43,25 @@ android { } dependencies { - implementation project(':firebase-common') - implementation project(':firebase-components') - implementation project(':appcheck:firebase-appcheck') + javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' + + api project(':appcheck:firebase-appcheck') + api 'com.google.firebase:firebase-annotations:16.2.0' + api 'com.google.firebase:firebase-common:21.0.0' + api 'com.google.firebase:firebase-common-ktx:21.0.0' + api 'com.google.firebase:firebase-components:18.0.0' + implementation 'com.google.android.gms:play-services-base:18.0.1' implementation 'com.google.android.gms:play-services-tasks:18.0.1' - implementation 'com.google.android.gms:play-services-safetynet:18.0.0' - - javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' + implementation 'com.google.android.play:integrity:1.2.0' - testImplementation 'junit:junit:4.13-beta-2' - testImplementation 'org.mockito:mockito-core:2.25.0' - testImplementation "org.robolectric:robolectric:$robolectricVersion" + testImplementation(project(":integ-testing")){ + exclude group: 'com.google.firebase', module: 'firebase-common' + exclude group: 'com.google.firebase', module: 'firebase-components' + } + testImplementation "androidx.test:core:$androidxTestCoreVersion" testImplementation "com.google.truth:truth:$googleTruthVersion" - testImplementation 'androidx.test:core:1.2.0' - testImplementation 'androidx.test:rules:1.2.0' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:3.4.6' + testImplementation "org.robolectric:robolectric:$robolectricVersion" } diff --git a/appcheck/firebase-appcheck-playintegrity/gradle.properties b/appcheck/firebase-appcheck-playintegrity/gradle.properties new file mode 100644 index 00000000000..9b7be4891d1 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/gradle.properties @@ -0,0 +1,2 @@ +version=18.0.1 +latestReleasedVersion=18.0.0 diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/AndroidManifest.xml b/appcheck/firebase-appcheck-playintegrity/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..928ea77e3e8 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/FirebaseAppCheckPlayIntegrityRegistrar.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/FirebaseAppCheckPlayIntegrityRegistrar.java new file mode 100644 index 00000000000..56f0e091907 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/FirebaseAppCheckPlayIntegrityRegistrar.java @@ -0,0 +1,61 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.appcheck.playintegrity; + +import com.google.android.gms.common.annotation.KeepForSdk; +import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.appcheck.playintegrity.internal.PlayIntegrityAppCheckProvider; +import com.google.firebase.components.Component; +import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; +import com.google.firebase.platforminfo.LibraryVersionComponent; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * {@link ComponentRegistrar} for setting up FirebaseAppCheck play integrity's dependency injections + * in Firebase Android Components. + * + * @hide + */ +@KeepForSdk +public class FirebaseAppCheckPlayIntegrityRegistrar implements ComponentRegistrar { + private static final String LIBRARY_NAME = "fire-app-check-play-integrity"; + + @Override + public List> getComponents() { + Qualified liteExecutor = Qualified.qualified(Lightweight.class, Executor.class); + Qualified blockingExecutor = Qualified.qualified(Blocking.class, Executor.class); + + return Arrays.asList( + Component.builder(PlayIntegrityAppCheckProvider.class) + .name(LIBRARY_NAME) + .add(Dependency.required(FirebaseApp.class)) + .add(Dependency.required(liteExecutor)) + .add(Dependency.required(blockingExecutor)) + .factory( + (container) -> + new PlayIntegrityAppCheckProvider( + container.get(FirebaseApp.class), + container.get(liteExecutor), + container.get(blockingExecutor))) + .build(), + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); + } +} diff --git a/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/SafetyNetAppCheckProviderFactory.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java similarity index 54% rename from appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/SafetyNetAppCheckProviderFactory.java rename to appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java index f6b2e019bed..6a43548eb6e 100644 --- a/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/SafetyNetAppCheckProviderFactory.java +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,37 +12,34 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.appcheck.safetynet; +package com.google.firebase.appcheck.playintegrity; import androidx.annotation.NonNull; import com.google.firebase.FirebaseApp; import com.google.firebase.appcheck.AppCheckProvider; import com.google.firebase.appcheck.AppCheckProviderFactory; -import com.google.firebase.appcheck.safetynet.internal.SafetyNetAppCheckProvider; +import com.google.firebase.appcheck.FirebaseAppCheck; +import com.google.firebase.appcheck.playintegrity.internal.PlayIntegrityAppCheckProvider; /** - * Implementation of an {@link AppCheckProviderFactory} that builds {@link - * SafetyNetAppCheckProvider}s. This is the default implementation. + * Implementation of an {@link AppCheckProviderFactory} that builds
+ * {@link PlayIntegrityAppCheckProvider}s. This is the default implementation. */ -public class SafetyNetAppCheckProviderFactory implements AppCheckProviderFactory { +public class PlayIntegrityAppCheckProviderFactory implements AppCheckProviderFactory { - private static final SafetyNetAppCheckProviderFactory instance = - new SafetyNetAppCheckProviderFactory(); + private static final PlayIntegrityAppCheckProviderFactory instance = + new PlayIntegrityAppCheckProviderFactory(); - private SafetyNetAppCheckProviderFactory() {} - - /** - * Gets an instance of this class for installation into a {@link - * com.google.firebase.appcheck.FirebaseAppCheck} instance. - */ + /** Gets an instance of this class for installation into a {@link FirebaseAppCheck} instance. */ @NonNull - public static SafetyNetAppCheckProviderFactory getInstance() { + public static PlayIntegrityAppCheckProviderFactory getInstance() { return instance; } @NonNull @Override + @SuppressWarnings("FirebaseUseExplicitDependencies") public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) { - return new SafetyNetAppCheckProvider(firebaseApp); + return firebaseApp.get(PlayIntegrityAppCheckProvider.class); } } diff --git a/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/internal/ExchangeSafetyNetTokenRequest.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequest.java similarity index 59% rename from appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/internal/ExchangeSafetyNetTokenRequest.java rename to appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequest.java index 33240ca4a2f..cc2e68d3054 100644 --- a/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/internal/ExchangeSafetyNetTokenRequest.java +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequest.java @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.appcheck.safetynet.internal; +package com.google.firebase.appcheck.playintegrity.internal; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -20,23 +20,23 @@ import org.json.JSONObject; /** - * Client-side model of the ExchangeSafetyNetTokenRequest payload from the Firebase App Check Token - * Exchange API. + * Client-side model of the ExchangePlayIntegrityTokenRequest payload from the Firebase App Check + * Token Exchange API. */ -public class ExchangeSafetyNetTokenRequest { +class ExchangePlayIntegrityTokenRequest { - @VisibleForTesting static final String SAFETY_NET_TOKEN_KEY = "safetyNetToken"; + @VisibleForTesting static final String PLAY_INTEGRITY_TOKEN_KEY = "playIntegrityToken"; - private final String safetyNetToken; + private final String playIntegrityToken; - public ExchangeSafetyNetTokenRequest(@NonNull String safetyNetToken) { - this.safetyNetToken = safetyNetToken; + public ExchangePlayIntegrityTokenRequest(@NonNull String playIntegrityToken) { + this.playIntegrityToken = playIntegrityToken; } @NonNull public String toJsonString() throws JSONException { JSONObject jsonObject = new JSONObject(); - jsonObject.put(SAFETY_NET_TOKEN_KEY, safetyNetToken); + jsonObject.put(PLAY_INTEGRITY_TOKEN_KEY, playIntegrityToken); return jsonObject.toString(); } diff --git a/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/DebugAppCheckProviderFactoryHelper.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequest.java similarity index 53% rename from appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/DebugAppCheckProviderFactoryHelper.java rename to appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequest.java index b484fe89d63..5e213d13189 100644 --- a/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/DebugAppCheckProviderFactoryHelper.java +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequest.java @@ -1,4 +1,4 @@ -// Copyright 2021 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,21 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.appcheck.debug; +package com.google.firebase.appcheck.playintegrity.internal; import androidx.annotation.NonNull; +import org.json.JSONObject; /** - * Helper class used by {@link com.google.firebase.appcheck.debug.testing.DebugAppCheckTestHelper} - * in order to access the package-private {@link DebugAppCheckProviderFactory} constructor that - * takes in a debug secret. - * - * @hide + * Client-side model of the GeneratePlayIntegrityChallengeRequest payload from the Firebase App + * Check Token Exchange API. */ -public class DebugAppCheckProviderFactoryHelper { +class GeneratePlayIntegrityChallengeRequest { + + public GeneratePlayIntegrityChallengeRequest() {} + @NonNull - public static DebugAppCheckProviderFactory createDebugAppCheckProviderFactory( - @NonNull String debugSecret) { - return new DebugAppCheckProviderFactory(debugSecret); + public String toJsonString() { + JSONObject jsonObject = new JSONObject(); + + // GeneratePlayIntegrityChallenge takes an empty POST body since the app ID is in the URL. + return jsonObject.toString(); } } diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponse.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponse.java new file mode 100644 index 00000000000..52d91827276 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponse.java @@ -0,0 +1,67 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.appcheck.playintegrity.internal; + +import static com.google.android.gms.common.internal.Preconditions.checkNotNull; +import static com.google.android.gms.common.util.Strings.emptyToNull; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import com.google.firebase.FirebaseException; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Client-side model of the GeneratePlayIntegrityChallengeResponse payload from the Firebase App + * Check Token Exchange API. + */ +class GeneratePlayIntegrityChallengeResponse { + + @VisibleForTesting static final String CHALLENGE_KEY = "challenge"; + @VisibleForTesting static final String TIME_TO_LIVE_KEY = "ttl"; + + private String challenge; + private String timeToLive; + + @NonNull + public static GeneratePlayIntegrityChallengeResponse fromJsonString(@NonNull String jsonString) + throws FirebaseException, JSONException { + JSONObject jsonObject = new JSONObject(jsonString); + String challenge = emptyToNull(jsonObject.optString(CHALLENGE_KEY)); + String timeToLive = emptyToNull(jsonObject.optString(TIME_TO_LIVE_KEY)); + if (challenge == null || timeToLive == null) { + throw new FirebaseException("Unexpected server response."); + } + return new GeneratePlayIntegrityChallengeResponse(challenge, timeToLive); + } + + private GeneratePlayIntegrityChallengeResponse( + @NonNull String challenge, @NonNull String timeToLive) { + checkNotNull(challenge); + checkNotNull(timeToLive); + this.challenge = challenge; + this.timeToLive = timeToLive; + } + + @NonNull + public String getChallenge() { + return challenge; + } + + @NonNull + public String getTimeToLive() { + return timeToLive; + } +} diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java new file mode 100644 index 00000000000..910aa3be2c7 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java @@ -0,0 +1,119 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.appcheck.playintegrity.internal; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.android.play.core.integrity.IntegrityManager; +import com.google.android.play.core.integrity.IntegrityManagerFactory; +import com.google.android.play.core.integrity.IntegrityTokenRequest; +import com.google.android.play.core.integrity.IntegrityTokenResponse; +import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.appcheck.AppCheckProvider; +import com.google.firebase.appcheck.AppCheckToken; +import com.google.firebase.appcheck.internal.DefaultAppCheckToken; +import com.google.firebase.appcheck.internal.NetworkClient; +import com.google.firebase.appcheck.internal.RetryManager; +import java.util.concurrent.Executor; + +public class PlayIntegrityAppCheckProvider implements AppCheckProvider { + + private static final String UTF_8 = "UTF-8"; + + private final String projectNumber; + private final IntegrityManager integrityManager; + private final NetworkClient networkClient; + private final Executor liteExecutor; + private final Executor blockingExecutor; + private final RetryManager retryManager; + + public PlayIntegrityAppCheckProvider( + @NonNull FirebaseApp firebaseApp, + @Lightweight Executor liteExecutor, + @Blocking Executor blockingExecutor) { + this( + firebaseApp.getOptions().getGcmSenderId(), + IntegrityManagerFactory.create(firebaseApp.getApplicationContext()), + new NetworkClient(firebaseApp), + liteExecutor, + blockingExecutor, + new RetryManager()); + } + + @VisibleForTesting + PlayIntegrityAppCheckProvider( + @NonNull String projectNumber, + @NonNull IntegrityManager integrityManager, + @NonNull NetworkClient networkClient, + @NonNull Executor liteExecutor, + @NonNull Executor blockingExecutor, + @NonNull RetryManager retryManager) { + this.projectNumber = projectNumber; + this.integrityManager = integrityManager; + this.networkClient = networkClient; + this.liteExecutor = liteExecutor; + this.blockingExecutor = blockingExecutor; + this.retryManager = retryManager; + } + + @NonNull + @Override + public Task getToken() { + return getPlayIntegrityAttestation() + .onSuccessTask( + liteExecutor, + integrityTokenResponse -> { + ExchangePlayIntegrityTokenRequest request = + new ExchangePlayIntegrityTokenRequest(integrityTokenResponse.token()); + return Tasks.call( + blockingExecutor, + () -> + networkClient.exchangeAttestationForAppCheckToken( + request.toJsonString().getBytes(UTF_8), + NetworkClient.PLAY_INTEGRITY, + retryManager)); + }) + .onSuccessTask( + liteExecutor, + appCheckTokenResponse -> + Tasks.forResult( + DefaultAppCheckToken.constructFromAppCheckTokenResponse( + appCheckTokenResponse))); + } + + @NonNull + private Task getPlayIntegrityAttestation() { + GeneratePlayIntegrityChallengeRequest generateChallengeRequest = + new GeneratePlayIntegrityChallengeRequest(); + return Tasks.call( + blockingExecutor, + () -> + GeneratePlayIntegrityChallengeResponse.fromJsonString( + networkClient.generatePlayIntegrityChallenge( + generateChallengeRequest.toJsonString().getBytes(UTF_8), retryManager))) + .onSuccessTask( + liteExecutor, + generatePlayIntegrityChallengeResponse -> + integrityManager.requestIntegrityToken( + IntegrityTokenRequest.builder() + .setCloudProjectNumber(Long.parseLong(projectNumber)) + .setNonce(generatePlayIntegrityChallengeResponse.getChallenge()) + .build())); + } +} diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/package-info.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/package-info.java new file mode 100644 index 00000000000..3431597172a --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/package-info.java @@ -0,0 +1,16 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** @hide */ +package com.google.firebase.appcheck.playintegrity.internal; diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/FirebaseAppCheckPlayIntegrityRegistrarTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/FirebaseAppCheckPlayIntegrityRegistrarTest.java new file mode 100644 index 00000000000..4b2f7a632ba --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/FirebaseAppCheckPlayIntegrityRegistrarTest.java @@ -0,0 +1,48 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.appcheck.playintegrity; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.components.Component; +import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; +import java.util.List; +import java.util.concurrent.Executor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link FirebaseAppCheckPlayIntegrityRegistrar}. */ +@RunWith(RobolectricTestRunner.class) +public class FirebaseAppCheckPlayIntegrityRegistrarTest { + @Test + public void testGetComponents() { + FirebaseAppCheckPlayIntegrityRegistrar registrar = new FirebaseAppCheckPlayIntegrityRegistrar(); + List> components = registrar.getComponents(); + assertThat(components).isNotEmpty(); + assertThat(components).hasSize(2); + Component appCheckPlayIntegrityComponent = components.get(0); + assertThat(appCheckPlayIntegrityComponent.getDependencies()) + .containsExactly( + Dependency.required(FirebaseApp.class), + Dependency.required(Qualified.qualified(Lightweight.class, Executor.class)), + Dependency.required(Qualified.qualified(Blocking.class, Executor.class))); + assertThat(appCheckPlayIntegrityComponent.isLazy()).isTrue(); + } +} diff --git a/appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/SafetyNetAppCheckProviderFactoryTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactoryTest.java similarity index 68% rename from appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/SafetyNetAppCheckProviderFactoryTest.java rename to appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactoryTest.java index cf4446a92a0..ed1ebddda96 100644 --- a/appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/SafetyNetAppCheckProviderFactoryTest.java +++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactoryTest.java @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.appcheck.safetynet; +package com.google.firebase.appcheck.playintegrity; import static com.google.common.truth.Truth.assertThat; @@ -21,16 +21,17 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -/** Tests for {@link SafetyNetAppCheckProviderFactory}. */ +/** Tests for {@link PlayIntegrityAppCheckProviderFactory}. */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class SafetyNetAppCheckProviderFactoryTest { +public class PlayIntegrityAppCheckProviderFactoryTest { @Test public void testGetInstance_callTwice_sameInstance() { - SafetyNetAppCheckProviderFactory firstInstance = SafetyNetAppCheckProviderFactory.getInstance(); - SafetyNetAppCheckProviderFactory secondInstance = - SafetyNetAppCheckProviderFactory.getInstance(); + PlayIntegrityAppCheckProviderFactory firstInstance = + PlayIntegrityAppCheckProviderFactory.getInstance(); + PlayIntegrityAppCheckProviderFactory secondInstance = + PlayIntegrityAppCheckProviderFactory.getInstance(); assertThat(firstInstance).isEqualTo(secondInstance); } } diff --git a/appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/internal/ExchangeSafetyNetTokenRequestTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequestTest.java similarity index 61% rename from appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/internal/ExchangeSafetyNetTokenRequestTest.java rename to appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequestTest.java index 06aac6e9016..8c01f97f887 100644 --- a/appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/internal/ExchangeSafetyNetTokenRequestTest.java +++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequestTest.java @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.appcheck.safetynet.internal; +package com.google.firebase.appcheck.playintegrity.internal; import static com.google.common.truth.Truth.assertThat; @@ -22,21 +22,21 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -/** Tests for {@link ExchangeSafetyNetTokenRequest}. */ +/** Tests for {@link ExchangePlayIntegrityTokenRequest}. */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class ExchangeSafetyNetTokenRequestTest { - private static final String SAFETY_NET_TOKEN = "safetyNetToken"; +public class ExchangePlayIntegrityTokenRequestTest { + private static final String PLAY_INTEGRITY_TOKEN = "playIntegrityToken"; @Test public void toJsonString_expectSerialized() throws Exception { - ExchangeSafetyNetTokenRequest exchangeSafetyNetTokenRequest = - new ExchangeSafetyNetTokenRequest(SAFETY_NET_TOKEN); + ExchangePlayIntegrityTokenRequest exchangePlayIntegrityTokenRequest = + new ExchangePlayIntegrityTokenRequest(PLAY_INTEGRITY_TOKEN); - String jsonString = exchangeSafetyNetTokenRequest.toJsonString(); + String jsonString = exchangePlayIntegrityTokenRequest.toJsonString(); JSONObject jsonObject = new JSONObject(jsonString); - assertThat(jsonObject.getString(ExchangeSafetyNetTokenRequest.SAFETY_NET_TOKEN_KEY)) - .isEqualTo(SAFETY_NET_TOKEN); + assertThat(jsonObject.getString(ExchangePlayIntegrityTokenRequest.PLAY_INTEGRITY_TOKEN_KEY)) + .isEqualTo(PLAY_INTEGRITY_TOKEN); } } diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequestTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequestTest.java new file mode 100644 index 00000000000..abd86ae8258 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequestTest.java @@ -0,0 +1,37 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.appcheck.playintegrity.internal; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for {@link GeneratePlayIntegrityChallengeRequest}. */ +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class GeneratePlayIntegrityChallengeRequestTest { + private static final String EMPTY_JSON = "{}"; + + @Test + public void toJsonString_expectSerialized() throws Exception { + GeneratePlayIntegrityChallengeRequest generatePlayIntegrityChallengeRequest = + new GeneratePlayIntegrityChallengeRequest(); + + assertThat(generatePlayIntegrityChallengeRequest.toJsonString()).isEqualTo(EMPTY_JSON); + } +} diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponseTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponseTest.java new file mode 100644 index 00000000000..dcc48034b00 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponseTest.java @@ -0,0 +1,65 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.appcheck.playintegrity.internal; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.firebase.FirebaseException; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for {@link GeneratePlayIntegrityChallengeResponse}. */ +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class GeneratePlayIntegrityChallengeResponseTest { + private static final String CHALLENGE = "testChallenge"; + private static final String TIME_TO_LIVE = "3600s"; + + @Test + public void fromJsonString_expectDeserialized() throws Exception { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(GeneratePlayIntegrityChallengeResponse.CHALLENGE_KEY, CHALLENGE); + jsonObject.put(GeneratePlayIntegrityChallengeResponse.TIME_TO_LIVE_KEY, TIME_TO_LIVE); + + GeneratePlayIntegrityChallengeResponse generatePlayIntegrityChallengeResponse = + GeneratePlayIntegrityChallengeResponse.fromJsonString(jsonObject.toString()); + assertThat(generatePlayIntegrityChallengeResponse.getChallenge()).isEqualTo(CHALLENGE); + assertThat(generatePlayIntegrityChallengeResponse.getTimeToLive()).isEqualTo(TIME_TO_LIVE); + } + + @Test + public void fromJsonString_nullChallenge_throwsException() throws Exception { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(GeneratePlayIntegrityChallengeResponse.TIME_TO_LIVE_KEY, TIME_TO_LIVE); + + assertThrows( + FirebaseException.class, + () -> GeneratePlayIntegrityChallengeResponse.fromJsonString(jsonObject.toString())); + } + + @Test + public void fromJsonString_nullTimeToLive_throwsException() throws Exception { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(GeneratePlayIntegrityChallengeResponse.CHALLENGE_KEY, CHALLENGE); + + assertThrows( + FirebaseException.class, + () -> GeneratePlayIntegrityChallengeResponse.fromJsonString(jsonObject.toString())); + } +} diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java new file mode 100644 index 00000000000..e0f80ceb2a5 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java @@ -0,0 +1,236 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.appcheck.playintegrity.internal; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.android.play.core.integrity.IntegrityManager; +import com.google.android.play.core.integrity.IntegrityTokenRequest; +import com.google.android.play.core.integrity.IntegrityTokenResponse; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.appcheck.AppCheckToken; +import com.google.firebase.appcheck.internal.AppCheckTokenResponse; +import com.google.firebase.appcheck.internal.DefaultAppCheckToken; +import com.google.firebase.appcheck.internal.NetworkClient; +import com.google.firebase.appcheck.internal.RetryManager; +import com.google.firebase.concurrent.TestOnlyExecutors; +import java.io.IOException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeoutException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +/** Tests for {@link PlayIntegrityAppCheckProvider}. */ +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +@LooperMode(LooperMode.Mode.LEGACY) +public class PlayIntegrityAppCheckProviderTest { + + private static final String PROJECT_NUMBER = "123456"; + private static final String APP_CHECK_TOKEN = "appCheckToken"; + private static final String TIME_TO_LIVE = "3600s"; + private static final String CHALLENGE = "testChallenge"; + private static final String INTEGRITY_TOKEN = "integrityToken"; + + @Mock private IntegrityManager mockIntegrityManager; + @Mock private NetworkClient mockNetworkClient; + @Mock private RetryManager mockRetryManager; + @Mock private IntegrityTokenResponse mockIntegrityTokenResponse; + @Mock private AppCheckTokenResponse mockAppCheckTokenResponse; + + @Captor private ArgumentCaptor integrityTokenRequestCaptor; + @Captor private ArgumentCaptor exchangePlayIntegrityTokenRequestCaptor; + + // TODO(b/258273630): Use TestOnlyExecutors instead of MoreExecutors.directExecutor(). + private Executor liteExecutor = MoreExecutors.directExecutor(); + private Executor blockingExecutor = MoreExecutors.directExecutor(); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(mockIntegrityTokenResponse.token()).thenReturn(INTEGRITY_TOKEN); + when(mockAppCheckTokenResponse.getToken()).thenReturn(APP_CHECK_TOKEN); + when(mockAppCheckTokenResponse.getTimeToLive()).thenReturn(TIME_TO_LIVE); + } + + @Test + public void testPublicConstructor_nullFirebaseApp_expectThrows() { + assertThrows( + NullPointerException.class, + () -> { + new PlayIntegrityAppCheckProvider( + null, TestOnlyExecutors.lite(), TestOnlyExecutors.blocking()); + }); + } + + @Test + public void getToken_onSuccess_setsTaskResult() throws Exception { + when(mockNetworkClient.generatePlayIntegrityChallenge(any(), eq(mockRetryManager))) + .thenReturn(createGeneratePlayIntegrityChallengeResponse()); + when(mockIntegrityManager.requestIntegrityToken(any())) + .thenReturn(Tasks.forResult(mockIntegrityTokenResponse)); + when(mockNetworkClient.exchangeAttestationForAppCheckToken( + any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager))) + .thenReturn(mockAppCheckTokenResponse); + + PlayIntegrityAppCheckProvider provider = + new PlayIntegrityAppCheckProvider( + PROJECT_NUMBER, + mockIntegrityManager, + mockNetworkClient, + liteExecutor, + blockingExecutor, + mockRetryManager); + Task task = provider.getToken(); + + AppCheckToken token = task.getResult(); + assertThat(token).isInstanceOf(DefaultAppCheckToken.class); + assertThat(token.getToken()).isEqualTo(APP_CHECK_TOKEN); + + verify(mockNetworkClient).generatePlayIntegrityChallenge(any(), eq(mockRetryManager)); + + verify(mockIntegrityManager).requestIntegrityToken(integrityTokenRequestCaptor.capture()); + assertThat(integrityTokenRequestCaptor.getValue().cloudProjectNumber()) + .isEqualTo(Long.parseLong(PROJECT_NUMBER)); + assertThat(integrityTokenRequestCaptor.getValue().nonce()).isEqualTo(CHALLENGE); + + verify(mockNetworkClient) + .exchangeAttestationForAppCheckToken( + exchangePlayIntegrityTokenRequestCaptor.capture(), + eq(NetworkClient.PLAY_INTEGRITY), + eq(mockRetryManager)); + String exchangePlayIntegrityTokenRequestJsonString = + new String(exchangePlayIntegrityTokenRequestCaptor.getValue()); + assertThat(exchangePlayIntegrityTokenRequestJsonString).contains(INTEGRITY_TOKEN); + } + + @Test + public void getToken_generateChallengeFails_setsTaskException() throws Exception { + when(mockNetworkClient.generatePlayIntegrityChallenge(any(), eq(mockRetryManager))) + .thenThrow(new IOException()); + + PlayIntegrityAppCheckProvider provider = + new PlayIntegrityAppCheckProvider( + PROJECT_NUMBER, + mockIntegrityManager, + mockNetworkClient, + liteExecutor, + blockingExecutor, + mockRetryManager); + Task task = provider.getToken(); + + assertThat(task.isSuccessful()).isFalse(); + assertThat(task.getException()).isInstanceOf(IOException.class); + + verify(mockNetworkClient).generatePlayIntegrityChallenge(any(), eq(mockRetryManager)); + verify(mockNetworkClient, never()).exchangeAttestationForAppCheckToken(any(), anyInt(), any()); + verify(mockIntegrityManager, never()).requestIntegrityToken(any()); + } + + @Test + public void getToken_requestIntegrityTokenFails_setsTaskException() throws Exception { + when(mockNetworkClient.generatePlayIntegrityChallenge(any(), eq(mockRetryManager))) + .thenReturn(createGeneratePlayIntegrityChallengeResponse()); + when(mockIntegrityManager.requestIntegrityToken(any())) + .thenReturn(Tasks.forException(new TimeoutException())); + + PlayIntegrityAppCheckProvider provider = + new PlayIntegrityAppCheckProvider( + PROJECT_NUMBER, + mockIntegrityManager, + mockNetworkClient, + liteExecutor, + blockingExecutor, + mockRetryManager); + Task task = provider.getToken(); + + assertThat(task.isSuccessful()).isFalse(); + assertThat(task.getException()).isInstanceOf(TimeoutException.class); + + verify(mockNetworkClient).generatePlayIntegrityChallenge(any(), eq(mockRetryManager)); + verify(mockNetworkClient, never()).exchangeAttestationForAppCheckToken(any(), anyInt(), any()); + + verify(mockIntegrityManager).requestIntegrityToken(integrityTokenRequestCaptor.capture()); + assertThat(integrityTokenRequestCaptor.getValue().cloudProjectNumber()) + .isEqualTo(Long.parseLong(PROJECT_NUMBER)); + assertThat(integrityTokenRequestCaptor.getValue().nonce()).isEqualTo(CHALLENGE); + } + + @Test + public void getToken_tokenExchangeFails_setsTaskException() throws Exception { + when(mockNetworkClient.generatePlayIntegrityChallenge(any(), eq(mockRetryManager))) + .thenReturn(createGeneratePlayIntegrityChallengeResponse()); + when(mockIntegrityManager.requestIntegrityToken(any())) + .thenReturn(Tasks.forResult(mockIntegrityTokenResponse)); + when(mockNetworkClient.exchangeAttestationForAppCheckToken( + any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager))) + .thenThrow(new IOException()); + + PlayIntegrityAppCheckProvider provider = + new PlayIntegrityAppCheckProvider( + PROJECT_NUMBER, + mockIntegrityManager, + mockNetworkClient, + liteExecutor, + blockingExecutor, + mockRetryManager); + Task task = provider.getToken(); + + assertThat(task.isSuccessful()).isFalse(); + assertThat(task.getException()).isInstanceOf(IOException.class); + + verify(mockNetworkClient).generatePlayIntegrityChallenge(any(), eq(mockRetryManager)); + + verify(mockIntegrityManager).requestIntegrityToken(integrityTokenRequestCaptor.capture()); + assertThat(integrityTokenRequestCaptor.getValue().cloudProjectNumber()) + .isEqualTo(Long.parseLong(PROJECT_NUMBER)); + assertThat(integrityTokenRequestCaptor.getValue().nonce()).isEqualTo(CHALLENGE); + + verify(mockNetworkClient) + .exchangeAttestationForAppCheckToken( + exchangePlayIntegrityTokenRequestCaptor.capture(), + eq(NetworkClient.PLAY_INTEGRITY), + eq(mockRetryManager)); + String exchangePlayIntegrityTokenRequestJsonString = + new String(exchangePlayIntegrityTokenRequestCaptor.getValue()); + assertThat(exchangePlayIntegrityTokenRequestJsonString).contains(INTEGRITY_TOKEN); + } + + private static String createGeneratePlayIntegrityChallengeResponse() throws Exception { + JSONObject responseBodyJson = new JSONObject(); + responseBodyJson.put(GeneratePlayIntegrityChallengeResponse.CHALLENGE_KEY, CHALLENGE); + responseBodyJson.put(GeneratePlayIntegrityChallengeResponse.TIME_TO_LIVE_KEY, TIME_TO_LIVE); + + return responseBodyJson.toString(); + } +} diff --git a/appcheck/firebase-appcheck-safetynet/gradle.properties b/appcheck/firebase-appcheck-safetynet/gradle.properties deleted file mode 100644 index 671ce6a5ae5..00000000000 --- a/appcheck/firebase-appcheck-safetynet/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -version=16.0.1 diff --git a/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/internal/SafetyNetAppCheckProvider.java b/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/internal/SafetyNetAppCheckProvider.java deleted file mode 100644 index f44867c39ee..00000000000 --- a/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/internal/SafetyNetAppCheckProvider.java +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.firebase.appcheck.safetynet.internal; - -import static com.google.android.gms.common.internal.Preconditions.checkNotEmpty; -import static com.google.android.gms.common.internal.Preconditions.checkNotNull; - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GoogleApiAvailability; -import com.google.android.gms.safetynet.SafetyNet; -import com.google.android.gms.safetynet.SafetyNetApi; -import com.google.android.gms.safetynet.SafetyNetClient; -import com.google.android.gms.tasks.Continuation; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.TaskCompletionSource; -import com.google.android.gms.tasks.Tasks; -import com.google.firebase.FirebaseApp; -import com.google.firebase.appcheck.AppCheckProvider; -import com.google.firebase.appcheck.AppCheckToken; -import com.google.firebase.appcheck.internal.AppCheckTokenResponse; -import com.google.firebase.appcheck.internal.DefaultAppCheckToken; -import com.google.firebase.appcheck.internal.NetworkClient; -import com.google.firebase.appcheck.internal.RetryManager; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class SafetyNetAppCheckProvider implements AppCheckProvider { - - // The SafetyNet nonce is used to associate a SafetyNet attestation with a request. However, since - // we do not have any fields in ExchangeSafetyNetTokenRequest that are not already covered in the - // AttestationResponse, the nonce does not provide much additional benefit. Because of this, we - // will leave the nonce empty in the v1 flow. - private static final String NONCE = ""; - private static final String UTF_8 = "UTF-8"; - - private final Context context; - private final Task safetyNetClientTask; - private final NetworkClient networkClient; - private final ExecutorService backgroundExecutor; - private final RetryManager retryManager; - private final String apiKey; - - /** @param firebaseApp the FirebaseApp to which this Factory is tied. */ - public SafetyNetAppCheckProvider(@NonNull FirebaseApp firebaseApp) { - this( - firebaseApp, - new NetworkClient(firebaseApp), - GoogleApiAvailability.getInstance(), - Executors.newCachedThreadPool()); - } - - @VisibleForTesting - SafetyNetAppCheckProvider( - @NonNull FirebaseApp firebaseApp, - @NonNull NetworkClient networkClient, - @NonNull GoogleApiAvailability googleApiAvailability, - @NonNull ExecutorService backgroundExecutor) { - checkNotNull(firebaseApp); - checkNotNull(networkClient); - checkNotNull(googleApiAvailability); - checkNotNull(backgroundExecutor); - this.context = firebaseApp.getApplicationContext(); - this.apiKey = firebaseApp.getOptions().getApiKey(); - this.backgroundExecutor = backgroundExecutor; - this.safetyNetClientTask = initSafetyNetClient(googleApiAvailability, this.backgroundExecutor); - this.networkClient = networkClient; - this.retryManager = new RetryManager(); - } - - @VisibleForTesting - SafetyNetAppCheckProvider( - @NonNull FirebaseApp firebaseApp, - @NonNull SafetyNetClient safetyNetClient, - @NonNull NetworkClient networkClient, - @NonNull ExecutorService backgroundExecutor, - @NonNull RetryManager retryManager) { - this.context = firebaseApp.getApplicationContext(); - this.apiKey = firebaseApp.getOptions().getApiKey(); - this.safetyNetClientTask = Tasks.forResult(safetyNetClient); - this.networkClient = networkClient; - this.backgroundExecutor = backgroundExecutor; - this.retryManager = retryManager; - } - - private Task initSafetyNetClient( - GoogleApiAvailability googleApiAvailability, ExecutorService executor) { - TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); - executor.execute( - () -> { - int connectionResult = googleApiAvailability.isGooglePlayServicesAvailable(context); - if (connectionResult == ConnectionResult.SUCCESS) { - taskCompletionSource.setResult(SafetyNet.getClient(context)); - } else { - taskCompletionSource.setException( - new IllegalStateException( - "SafetyNet unavailable; unable to connect to Google Play Services: " - + getGooglePlayServicesConnectionErrorString(connectionResult))); - } - }); - return taskCompletionSource.getTask(); - } - - private String getGooglePlayServicesConnectionErrorString(int connectionResult) { - switch (connectionResult) { - case ConnectionResult.SERVICE_MISSING: - return "Google Play services is missing on this device."; - case ConnectionResult.SERVICE_UPDATING: - return "Google Play services is currently being updated on this device."; - case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED: - return "The installed version of Google Play services is out of date."; - case ConnectionResult.SERVICE_DISABLED: - return "The installed version of Google Play services has been disabled on this device."; - case ConnectionResult.SERVICE_INVALID: - return "The version of the Google Play services installed on this device is not authentic."; - default: - return "Unknown error."; - } - } - - @VisibleForTesting - Task getSafetyNetClientTask() { - return safetyNetClientTask; - } - - @NonNull - @Override - public Task getToken() { - return safetyNetClientTask - .continueWithTask( - new Continuation>() { - @Override - public Task then( - @NonNull Task task) { - if (task.isSuccessful()) { - return task.getResult().attest(NONCE.getBytes(), apiKey); - } - return Tasks.forException(task.getException()); - } - }) - .continueWithTask( - new Continuation>() { - @Override - public Task then( - @NonNull Task task) { - if (!task.isSuccessful()) { - // Proxies errors to the client directly; need to wrap to get the - // types right. - // TODO: more specific error mapping to help clients debug more - // easily. - return Tasks.forException(task.getException()); - } else { - return exchangeSafetyNetAttestationResponseForToken(task.getResult()); - } - } - }); - } - - @NonNull - Task exchangeSafetyNetAttestationResponseForToken( - @NonNull SafetyNetApi.AttestationResponse attestationResponse) { - checkNotNull(attestationResponse); - String safetyNetJwsResult = attestationResponse.getJwsResult(); - checkNotEmpty(safetyNetJwsResult); - - ExchangeSafetyNetTokenRequest request = new ExchangeSafetyNetTokenRequest(safetyNetJwsResult); - - Task networkTask = - Tasks.call( - backgroundExecutor, - () -> - networkClient.exchangeAttestationForAppCheckToken( - request.toJsonString().getBytes(UTF_8), - NetworkClient.SAFETY_NET, - retryManager)); - return networkTask.continueWithTask( - new Continuation>() { - @Override - public Task then(@NonNull Task task) { - if (task.isSuccessful()) { - return Tasks.forResult( - DefaultAppCheckToken.constructFromAppCheckTokenResponse(task.getResult())); - } - // TODO: Surface more error details. - return Tasks.forException(task.getException()); - } - }); - } -} diff --git a/appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/internal/SafetyNetAppCheckProviderTest.java b/appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/internal/SafetyNetAppCheckProviderTest.java deleted file mode 100644 index 088ff764496..00000000000 --- a/appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/internal/SafetyNetAppCheckProviderTest.java +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.firebase.appcheck.safetynet.internal; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import androidx.test.core.app.ApplicationProvider; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GoogleApiAvailability; -import com.google.android.gms.safetynet.SafetyNetApi; -import com.google.android.gms.safetynet.SafetyNetClient; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.TaskCompletionSource; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseOptions; -import com.google.firebase.appcheck.AppCheckToken; -import com.google.firebase.appcheck.internal.AppCheckTokenResponse; -import com.google.firebase.appcheck.internal.DefaultAppCheckToken; -import com.google.firebase.appcheck.internal.NetworkClient; -import com.google.firebase.appcheck.internal.RetryManager; -import java.io.IOException; -import java.util.concurrent.ExecutorService; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -/** Tests for {@link SafetyNetAppCheckProvider}. */ -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class SafetyNetAppCheckProviderTest { - - private static final String API_KEY = "apiKey"; - private static final String APP_ID = "appId"; - private static final String PROJECT_ID = "projectId"; - private static final String SAFETY_NET_TOKEN = "safetyNetToken"; - private static final String ATTESTATION_TOKEN = "token"; - private static final String TIME_TO_LIVE = "3600s"; - - private FirebaseApp firebaseApp; - private ExecutorService backgroundExecutor = MoreExecutors.newDirectExecutorService(); - @Mock private GoogleApiAvailability mockGoogleApiAvailability; - @Mock private SafetyNetClient mockSafetyNetClient; - @Mock private NetworkClient mockNetworkClient; - @Mock private RetryManager mockRetryManager; - @Mock private SafetyNetApi.AttestationResponse mockSafetyNetAttestationResponse; - @Mock private AppCheckTokenResponse mockAppCheckTokenResponse; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - FirebaseApp.clearInstancesForTest(); - firebaseApp = - initializeFirebaseApp( - ApplicationProvider.getApplicationContext(), FirebaseApp.DEFAULT_APP_NAME); - } - - @Test - public void testPublicConstructor_nullFirebaseApp_expectThrows() { - assertThrows( - NullPointerException.class, - () -> { - new SafetyNetAppCheckProvider(null); - }); - } - - @Test - public void - testConstructor_googlePlayServicesIsNotAvailable_expectSafetyNetClientTaskException() { - when(mockGoogleApiAvailability.isGooglePlayServicesAvailable(any())) - .thenReturn(ConnectionResult.SERVICE_MISSING); - SafetyNetAppCheckProvider provider = - new SafetyNetAppCheckProvider( - firebaseApp, mockNetworkClient, mockGoogleApiAvailability, backgroundExecutor); - assertThat(provider.getSafetyNetClientTask().isSuccessful()).isFalse(); - } - - @Test - public void testGetToken_googlePlayServicesIsNotAvailable_expectGetTokenTaskException() { - when(mockGoogleApiAvailability.isGooglePlayServicesAvailable(any())) - .thenReturn(ConnectionResult.SERVICE_MISSING); - SafetyNetAppCheckProvider provider = - new SafetyNetAppCheckProvider( - firebaseApp, mockNetworkClient, mockGoogleApiAvailability, backgroundExecutor); - assertThat(provider.getSafetyNetClientTask().isSuccessful()).isFalse(); - - Task tokenTask = provider.getToken(); - assertThat(tokenTask).isNotNull(); - assertThat(tokenTask.isSuccessful()).isFalse(); - } - - @Test - public void testGetToken_nonNullSafetyNetClient_expectCallsSafetyNetForAttestation() { - SafetyNetAppCheckProvider provider = - new SafetyNetAppCheckProvider( - firebaseApp, - mockSafetyNetClient, - mockNetworkClient, - backgroundExecutor, - mockRetryManager); - assertThat(provider.getSafetyNetClientTask().getResult()).isEqualTo(mockSafetyNetClient); - - Task safetyNetTask = - new TaskCompletionSource().getTask(); - - when(mockSafetyNetClient.attest(any(), any())).thenReturn(safetyNetTask); - - Task tokenTask = provider.getToken(); - assertThat(tokenTask).isNotNull(); - - verify(mockSafetyNetClient).attest(any(), any()); - } - - @Test - public void testExchangeSafetyNetJwsForToken_nullAttestationResponse_expectThrows() { - SafetyNetAppCheckProvider provider = - new SafetyNetAppCheckProvider( - firebaseApp, - mockSafetyNetClient, - mockNetworkClient, - backgroundExecutor, - mockRetryManager); - assertThrows( - NullPointerException.class, - () -> { - provider.exchangeSafetyNetAttestationResponseForToken( - /* attestationResponse= */ (SafetyNetApi.AttestationResponse) null); - }); - } - - @Test - public void testExchangeSafetyNetJwsForToken_emptySafetyNetJwsResult_expectThrows() { - when(mockSafetyNetAttestationResponse.getJwsResult()).thenReturn(""); - SafetyNetAppCheckProvider provider = - new SafetyNetAppCheckProvider( - firebaseApp, - mockSafetyNetClient, - mockNetworkClient, - backgroundExecutor, - mockRetryManager); - assertThrows( - IllegalArgumentException.class, - () -> { - provider.exchangeSafetyNetAttestationResponseForToken(mockSafetyNetAttestationResponse); - }); - } - - @Test - public void testExchangeSafetyNetJwsForToken_validFields_expectReturnsTask() { - when(mockSafetyNetAttestationResponse.getJwsResult()).thenReturn(SAFETY_NET_TOKEN); - SafetyNetAppCheckProvider provider = - new SafetyNetAppCheckProvider( - firebaseApp, - mockSafetyNetClient, - mockNetworkClient, - backgroundExecutor, - mockRetryManager); - Task task = - provider.exchangeSafetyNetAttestationResponseForToken(mockSafetyNetAttestationResponse); - assertThat(task).isNotNull(); - } - - @Test - public void exchangeSafetyNetJwsForToken_onSuccess_setsTaskResult() throws Exception { - when(mockSafetyNetAttestationResponse.getJwsResult()).thenReturn(SAFETY_NET_TOKEN); - when(mockNetworkClient.exchangeAttestationForAppCheckToken( - any(), eq(NetworkClient.SAFETY_NET), eq(mockRetryManager))) - .thenReturn(mockAppCheckTokenResponse); - when(mockAppCheckTokenResponse.getAttestationToken()).thenReturn(ATTESTATION_TOKEN); - when(mockAppCheckTokenResponse.getTimeToLive()).thenReturn(TIME_TO_LIVE); - - SafetyNetAppCheckProvider provider = - new SafetyNetAppCheckProvider( - firebaseApp, - mockSafetyNetClient, - mockNetworkClient, - backgroundExecutor, - mockRetryManager); - Task task = - provider.exchangeSafetyNetAttestationResponseForToken(mockSafetyNetAttestationResponse); - - verify(mockNetworkClient) - .exchangeAttestationForAppCheckToken( - any(), eq(NetworkClient.SAFETY_NET), eq(mockRetryManager)); - - AppCheckToken token = task.getResult(); - assertThat(token).isInstanceOf(DefaultAppCheckToken.class); - assertThat(token.getToken()).isEqualTo(ATTESTATION_TOKEN); - } - - @Test - public void exchangeSafetyNetJwsForToken_onFailure_setsTaskException() throws Exception { - when(mockSafetyNetAttestationResponse.getJwsResult()).thenReturn(SAFETY_NET_TOKEN); - when(mockNetworkClient.exchangeAttestationForAppCheckToken( - any(), eq(NetworkClient.SAFETY_NET), eq(mockRetryManager))) - .thenThrow(new IOException()); - - SafetyNetAppCheckProvider provider = - new SafetyNetAppCheckProvider( - firebaseApp, - mockSafetyNetClient, - mockNetworkClient, - backgroundExecutor, - mockRetryManager); - Task task = - provider.exchangeSafetyNetAttestationResponseForToken(mockSafetyNetAttestationResponse); - - verify(mockNetworkClient) - .exchangeAttestationForAppCheckToken( - any(), eq(NetworkClient.SAFETY_NET), eq(mockRetryManager)); - - assertThat(task.isSuccessful()).isFalse(); - Exception exception = task.getException(); - assertThat(exception).isInstanceOf(IOException.class); - } - - private static FirebaseApp initializeFirebaseApp(Context context, String name) { - return FirebaseApp.initializeApp( - context, - new FirebaseOptions.Builder() - .setApiKey(API_KEY) - .setApplicationId(APP_ID) - .setProjectId(PROJECT_ID) - .build(), - name); - } -} diff --git a/appcheck/firebase-appcheck/CHANGELOG.md b/appcheck/firebase-appcheck/CHANGELOG.md new file mode 100644 index 00000000000..19e29699175 --- /dev/null +++ b/appcheck/firebase-appcheck/CHANGELOG.md @@ -0,0 +1,141 @@ +# Unreleased + + +# 18.0.0 +* [changed] Bump internal dependencies +* [changed] Internal support for `SafetyNet` has been dropped, as the [SafetyNet Attestation API +has been deprecated.](https://developer.android.com/privacy-and-security/safetynet/deprecation-timeline#safetynet_attestation_deprecation_timeline) + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-appcheck` library. The Kotlin extensions library has no additional +updates. + +# 17.1.2 +* [changed] Bump internal dependencies. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-appcheck` library. The Kotlin extensions library has no additional +updates. + +# 17.1.1 +* [fixed] Fixed a bug causing internal tests to depend directly on `firebase-common`. +* [fixed] Fixed client-side throttling in Play Integrity flows. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-appcheck` library. The Kotlin extensions library has no additional +updates. + +# 17.1.0 +* [changed] Added Kotlin extensions (KTX) APIs from `com.google.firebase:firebase-appcheck-ktx` + to `com.google.firebase:firebase-appcheck` under the `com.google.firebase.appcheck` package. + For details, see the + [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) +* [deprecated] All the APIs from `com.google.firebase:firebase-appcheck-ktx` have been added to + `com.google.firebase:firebase-appcheck` under the `com.google.firebase.appcheck` package, + and all the Kotlin extensions (KTX) APIs in `com.google.firebase:firebase-appcheck-ktx` are + now deprecated. As early as April 2024, we'll no longer release KTX modules. For details, see the + [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-appcheck` library. The Kotlin extensions library has no additional +updates. + +# 17.0.1 +* [changed] Internal updates to allow Firebase SDKs to obtain limited-use tokens. + +# 17.0.0 +* [feature] Added [`getLimitedUseAppCheckToken()`](/docs/reference/android/com/google/firebase/appcheck/FirebaseAppCheck#getLimitedUseAppCheckToken()) + for obtaining limited-use tokens for protecting non-Firebase backends. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-appcheck` library. The Kotlin extensions library has no additional +updates. + +# 16.1.2 +* [unchanged] Updated to keep [app_check] SDK versions aligned. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-appcheck` library. The Kotlin extensions library has no additional +updates. + +# 16.1.1 +* [changed] Migrated [app_check] SDKs to use standard Firebase executors. + (GitHub [#4431](//github.com/firebase/firebase-android-sdk/issues/4431){: .external} + and + [#4449](//github.com/firebase/firebase-android-sdk/issues/4449){: .external}) +* [changed] Moved Task continuations off the main thread. + (GitHub [#4453](//github.com/firebase/firebase-android-sdk/issues/4453){: .external}) + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-appcheck` library. The Kotlin extensions library has no additional +updates. + +# 16.1.0 +* [unchanged] Updated to accommodate the release of the updated + [app_check] Kotlin extensions library. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-appcheck` library. The Kotlin extensions library has the following +additional updates: + +* [feature] Firebase now supports Kotlin coroutines. + With this release, we added + [`kotlinx-coroutines-play-services`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-play-services/){: .external} + to `firebase-appcheck-ktx` as a transitive dependency, which exposes the + `Task.await()` suspend function to convert a + [`Task`](https://developers.google.com/android/guides/tasks) into a Kotlin + coroutine. + +# 16.0.1 +* [changed] Updated dependency of `play-services-basement` to its latest + version (v18.1.0). + +# 16.0.0 +* [changed] [app_check] has exited beta and is now generally available for + use. +* [feature] Added support for + [Play Integrity](https://developer.android.com/google/play/integrity) as an + attestation provider. + +# 16.0.0-beta06 +* [fixed] Fixed a bug in the [app_check] token refresh flow when using a + custom provider. + +# 16.0.0-beta05 +* [changed] Internal improvements. + +# 16.0.0-beta04 +* [changed] Improved error handling logic by minimizing the amount of requests + that are unlikely to succeed. +* [fixed] Fixed heartbeat reporting. + +# 16.0.0-beta03 +* [changed] Added `X-Android-Package` and `X-Android-Cert` request headers to + [app_check] network calls. + +# 16.0.0-beta02 +* [feature] Added [`getAppCheckToken()`](/docs/reference/android/com/google/firebase/appcheck/FirebaseAppCheck#getAppCheckToken(boolean)), + [`AppCheckTokenListener`](/docs/reference/android/com/google/firebase/appcheck/FirebaseAppCheck.AppCheckListener), + and associated setters and removers for developers to request and observe + changes to the [app_check] token. + +# 16.0.0-beta01 +* [feature] Initial beta release of the [app_check] SDK with abuse reduction + features. + diff --git a/appcheck/firebase-appcheck/api.txt b/appcheck/firebase-appcheck/api.txt new file mode 100644 index 00000000000..e818b1ecd65 --- /dev/null +++ b/appcheck/firebase-appcheck/api.txt @@ -0,0 +1,54 @@ +// Signature format: 2.0 +package com.google.firebase.appcheck { + + public interface AppCheckProvider { + method @NonNull public com.google.android.gms.tasks.Task getToken(); + } + + public interface AppCheckProviderFactory { + method @NonNull public com.google.firebase.appcheck.AppCheckProvider create(@NonNull com.google.firebase.FirebaseApp); + } + + public abstract class AppCheckToken { + ctor public AppCheckToken(); + method public abstract long getExpireTimeMillis(); + method @NonNull public abstract String getToken(); + } + + public abstract class FirebaseAppCheck implements com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider { + ctor public FirebaseAppCheck(); + method public abstract void addAppCheckListener(@NonNull com.google.firebase.appcheck.FirebaseAppCheck.AppCheckListener); + method @NonNull public abstract com.google.android.gms.tasks.Task getAppCheckToken(boolean); + method @NonNull public static com.google.firebase.appcheck.FirebaseAppCheck getInstance(); + method @NonNull public static com.google.firebase.appcheck.FirebaseAppCheck getInstance(@NonNull com.google.firebase.FirebaseApp); + method @NonNull public abstract com.google.android.gms.tasks.Task getLimitedUseAppCheckToken(); + method public abstract void installAppCheckProviderFactory(@NonNull com.google.firebase.appcheck.AppCheckProviderFactory); + method public abstract void installAppCheckProviderFactory(@NonNull com.google.firebase.appcheck.AppCheckProviderFactory, boolean); + method public abstract void removeAppCheckListener(@NonNull com.google.firebase.appcheck.FirebaseAppCheck.AppCheckListener); + method public abstract void setTokenAutoRefreshEnabled(boolean); + } + + public static interface FirebaseAppCheck.AppCheckListener { + method public void onAppCheckTokenChanged(@NonNull com.google.firebase.appcheck.AppCheckToken); + } + + public final class FirebaseAppCheckKt { + method @NonNull public static com.google.firebase.appcheck.FirebaseAppCheck appCheck(@NonNull com.google.firebase.Firebase, @NonNull com.google.firebase.FirebaseApp app); + method @NonNull public static operator String component1(@NonNull com.google.firebase.appcheck.AppCheckToken); + method public static operator long component2(@NonNull com.google.firebase.appcheck.AppCheckToken); + method @NonNull public static com.google.firebase.appcheck.FirebaseAppCheck getAppCheck(@NonNull com.google.firebase.Firebase); + } + +} + +package com.google.firebase.appcheck.ktx { + + public final class FirebaseAppCheckKt { + method @Deprecated @NonNull public static com.google.firebase.appcheck.FirebaseAppCheck appCheck(@NonNull com.google.firebase.ktx.Firebase, @NonNull com.google.firebase.FirebaseApp app); + method @Deprecated @NonNull public static operator String component1(@NonNull com.google.firebase.appcheck.AppCheckToken); + method @Deprecated public static operator long component2(@NonNull com.google.firebase.appcheck.AppCheckToken); + method @Deprecated @NonNull public static com.google.firebase.appcheck.FirebaseAppCheck getAppCheck(@NonNull com.google.firebase.ktx.Firebase); + } + +} + diff --git a/appcheck/firebase-appcheck/firebase-appcheck.gradle b/appcheck/firebase-appcheck/firebase-appcheck.gradle index 3f04fff97ad..0da65d53d49 100644 --- a/appcheck/firebase-appcheck/firebase-appcheck.gradle +++ b/appcheck/firebase-appcheck/firebase-appcheck.gradle @@ -14,9 +14,11 @@ plugins { id 'firebase-library' + id("kotlin-android") } firebaseLibrary { + libraryGroup "appcheck" publishSources = true } @@ -25,10 +27,12 @@ android { timeOutInMs 60 * 1000 } - compileSdkVersion project.targetSdkVersion + namespace "com.google.firebase.appcheck" + compileSdkVersion project.compileSdkVersion defaultConfig { targetSdkVersion project.targetSdkVersion minSdkVersion project.minSdkVersion + multiDexEnabled = true versionName version testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -36,24 +40,48 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - + kotlinOptions { jvmTarget = "1.8" } testOptions.unitTests.includeAndroidResources = true } dependencies { - implementation project(':firebase-common') - implementation project(':firebase-components') - implementation project(":appcheck:firebase-appcheck-interop") - implementation 'com.google.android.gms:play-services-base:18.0.1' - implementation 'com.google.android.gms:play-services-tasks:18.0.1' - javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' + api 'com.google.android.gms:play-services-tasks:18.1.0' + api 'com.google.firebase:firebase-annotations:16.2.0' + api "com.google.firebase:firebase-appcheck-interop:17.1.0" + api("com.google.firebase:firebase-common:21.0.0") + api("com.google.firebase:firebase-common-ktx:21.0.0") + api("com.google.firebase:firebase-components:18.0.0") + + implementation 'androidx.annotation:annotation:1.1.0' + implementation 'com.google.android.gms:play-services-base:18.1.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + + testImplementation(project(":integ-testing")){ + exclude group: 'com.google.firebase', module: 'firebase-common' + exclude group: 'com.google.firebase', module: 'firebase-components' + } + testImplementation "androidx.test:core:$androidxTestCoreVersion" + testImplementation 'androidx.test:rules:1.2.0' + testImplementation "com.google.truth:truth:$googleTruthVersion" + testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13-beta-2' - testImplementation 'org.mockito:mockito-core:2.25.0' - testImplementation 'org.mockito:mockito-inline:2.25.0' + testImplementation 'org.mockito:mockito-core:5.2.0' + testImplementation 'org.mockito:mockito-inline:5.2.0' testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation "com.google.truth:truth:$googleTruthVersion" - testImplementation 'androidx.test:core:1.2.0' - testImplementation 'androidx.test:rules:1.2.0' + + androidTestImplementation project(':appcheck:firebase-appcheck') + androidTestImplementation(project(":integ-testing")){ + exclude group: 'com.google.firebase', module: 'firebase-common' + exclude group: 'com.google.firebase', module: 'firebase-components' + } + androidTestImplementation "androidx.annotation:annotation:1.0.0" + androidTestImplementation "androidx.test:core:$androidxTestCoreVersion" + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation "androidx.test.ext:junit:$androidxTestJUnitVersion" + androidTestImplementation "com.google.truth:truth:$googleTruthVersion" + androidTestImplementation 'junit:junit:4.12' + androidTestImplementation 'org.mockito:mockito-core:2.25.0' + androidTestImplementation 'org.mockito:mockito-inline:2.25.0' } diff --git a/appcheck/firebase-appcheck/gradle.properties b/appcheck/firebase-appcheck/gradle.properties index 671ce6a5ae5..9b7be4891d1 100644 --- a/appcheck/firebase-appcheck/gradle.properties +++ b/appcheck/firebase-appcheck/gradle.properties @@ -1 +1,2 @@ -version=16.0.1 +version=18.0.1 +latestReleasedVersion=18.0.0 diff --git a/appcheck/firebase-appcheck/ktx/api.txt b/appcheck/firebase-appcheck/ktx/api.txt new file mode 100644 index 00000000000..c951388dd72 --- /dev/null +++ b/appcheck/firebase-appcheck/ktx/api.txt @@ -0,0 +1,8 @@ +// Signature format: 2.0 +package com.google.firebase.appcheck.ktx { + + public final class LoggingKt { + } + +} + diff --git a/firebase-appdistribution/ktx/gradle.properties b/appcheck/firebase-appcheck/ktx/gradle.properties similarity index 100% rename from firebase-appdistribution/ktx/gradle.properties rename to appcheck/firebase-appcheck/ktx/gradle.properties diff --git a/firebase-functions/ktx/ktx.gradle b/appcheck/firebase-appcheck/ktx/ktx.gradle similarity index 62% rename from firebase-functions/ktx/ktx.gradle rename to appcheck/firebase-appcheck/ktx/ktx.gradle index ea6e2575ed7..1de7ceea1cf 100644 --- a/firebase-functions/ktx/ktx.gradle +++ b/appcheck/firebase-appcheck/ktx/ktx.gradle @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,20 +14,22 @@ plugins { id 'firebase-library' - id 'kotlin-android' + id("kotlin-android") } firebaseLibrary { - releaseWith project(':firebase-functions') - publishJavadoc = true - publishSources = true + libraryGroup "appcheck" testLab.enabled = true + publishJavadoc = false + publishReleaseNotes = false + publishSources = true } android { - compileSdkVersion project.targetSdkVersion + namespace "com.google.firebase.appcheck.ktx" + compileSdkVersion project.compileSdkVersion defaultConfig { - minSdkVersion 16 + minSdkVersion project.minSdkVersion multiDexEnabled true targetSdkVersion project.targetSdkVersion versionName version @@ -40,25 +42,31 @@ android { } androidTest.java.srcDirs += 'src/androidTest/kotlin' } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } testOptions.unitTests.includeAndroidResources = true } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + api(project(":appcheck:firebase-appcheck")) + api("com.google.firebase:firebase-common:21.0.0") + api("com.google.firebase:firebase-common-ktx:21.0.0") - implementation project(':firebase-common') - implementation project(':firebase-components') - implementation project(':firebase-common:ktx') - implementation project(':firebase-functions') - implementation 'androidx.annotation:annotation:1.1.0' - implementation 'com.google.android.gms:play-services-tasks:18.0.1' - - androidTestImplementation 'junit:junit:4.12' - androidTestImplementation "com.google.truth:truth:$googleTruthVersion" - androidTestImplementation 'androidx.test:runner:1.2.0' + implementation("com.google.firebase:firebase-components:18.0.0") - testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation 'junit:junit:4.12' testImplementation "com.google.truth:truth:$googleTruthVersion" - testImplementation 'androidx.test:core:1.2.0' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:2.25.0' + testImplementation "org.robolectric:robolectric:$robolectricVersion" + + androidTestImplementation "androidx.test:core:$androidxTestCoreVersion" + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'com.google.firebase:firebase-appcheck-interop:17.1.0' + androidTestImplementation "com.google.truth:truth:$googleTruthVersion" + androidTestImplementation 'junit:junit:4.12' } diff --git a/firebase-appdistribution/ktx/src/androidTest/AndroidManifest.xml b/appcheck/firebase-appcheck/ktx/src/androidTest/AndroidManifest.xml similarity index 64% rename from firebase-appdistribution/ktx/src/androidTest/AndroidManifest.xml rename to appcheck/firebase-appcheck/ktx/src/androidTest/AndroidManifest.xml index c9068143ed6..1b0d1dff1e4 100644 --- a/firebase-appdistribution/ktx/src/androidTest/AndroidManifest.xml +++ b/appcheck/firebase-appcheck/ktx/src/androidTest/AndroidManifest.xml @@ -1,7 +1,6 @@ - + - + @@ -9,5 +8,5 @@ - \ No newline at end of file + android:targetPackage="com.google.firebase.appcheck.ktx" /> + diff --git a/appcheck/firebase-appcheck/ktx/src/androidTest/kotlin/com/google/firebase/appcheck/ktx/FirebaseAppCheckTests.kt b/appcheck/firebase-appcheck/ktx/src/androidTest/kotlin/com/google/firebase/appcheck/ktx/FirebaseAppCheckTests.kt new file mode 100644 index 00000000000..2989924a2c5 --- /dev/null +++ b/appcheck/firebase-appcheck/ktx/src/androidTest/kotlin/com/google/firebase/appcheck/ktx/FirebaseAppCheckTests.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.appcheck.ktx + +import androidx.test.core.app.ApplicationProvider +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import com.google.common.truth.Truth.assertThat +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import com.google.firebase.appcheck.AppCheckToken +import com.google.firebase.appcheck.FirebaseAppCheck +import com.google.firebase.ktx.Firebase +import com.google.firebase.ktx.app +import com.google.firebase.ktx.initialize +import com.google.firebase.platforminfo.UserAgentPublisher +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +const val APP_ID = "APP_ID" +const val API_KEY = "API_KEY" + +const val EXISTING_APP = "existing" + +@RunWith(AndroidJUnit4ClassRunner::class) +abstract class BaseTestCase { + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) + + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } + + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } +} + +@RunWith(AndroidJUnit4ClassRunner::class) +class FirebaseAppCheckTests : BaseTestCase() { + @Test + fun appCheck_default_callsDefaultGetInstance() { + assertThat(Firebase.appCheck).isSameInstanceAs(FirebaseAppCheck.getInstance()) + } + + @Test + fun appCheck_with_custom_firebaseapp_calls_GetInstance() { + val app = Firebase.app(EXISTING_APP) + assertThat(Firebase.appCheck(app)).isSameInstanceAs(FirebaseAppCheck.getInstance(app)) + } + + @Test + fun appCheckToken_destructuring_declaration_works() { + val mockAppCheckToken = + object : AppCheckToken() { + override fun getToken(): String = "randomToken" + + override fun getExpireTimeMillis(): Long = 23121997 + } + + val (token, expiration) = mockAppCheckToken + + assertThat(token).isEqualTo(mockAppCheckToken.token) + assertThat(expiration).isEqualTo(mockAppCheckToken.expireTimeMillis) + } +} + +internal const val LIBRARY_NAME: String = "fire-app-check-ktx" + +@RunWith(AndroidJUnit4ClassRunner::class) +class LibraryVersionTest : BaseTestCase() { + @Test + fun libraryRegistrationAtRuntime() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } +} diff --git a/appcheck/firebase-appcheck/ktx/src/main/AndroidManifest.xml b/appcheck/firebase-appcheck/ktx/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..e2af9507263 --- /dev/null +++ b/appcheck/firebase-appcheck/ktx/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appcheck/firebase-appcheck/ktx/src/main/kotlin/com/google/firebase/appcheck/ktx/Logging.kt b/appcheck/firebase-appcheck/ktx/src/main/kotlin/com/google/firebase/appcheck/ktx/Logging.kt new file mode 100644 index 00000000000..56050655f1f --- /dev/null +++ b/appcheck/firebase-appcheck/ktx/src/main/kotlin/com/google/firebase/appcheck/ktx/Logging.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.appcheck.ktx + +import androidx.annotation.Keep +import com.google.firebase.appcheck.BuildConfig +import com.google.firebase.components.Component +import com.google.firebase.components.ComponentRegistrar +import com.google.firebase.platforminfo.LibraryVersionComponent + +internal const val LIBRARY_NAME: String = "fire-app-check-ktx" + +/** @suppress */ +@Keep +class FirebaseAppcheckLegacyRegistrar : ComponentRegistrar { + override fun getComponents(): List> { + return listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + } +} diff --git a/health-metrics/macrobenchmark/template/benchmark/src/main/AndroidManifest.xml.mustache b/appcheck/firebase-appcheck/src/androidTest/java/AndroidManifest.xml similarity index 67% rename from health-metrics/macrobenchmark/template/benchmark/src/main/AndroidManifest.xml.mustache rename to appcheck/firebase-appcheck/src/androidTest/java/AndroidManifest.xml index 15cff250146..31f57005096 100644 --- a/health-metrics/macrobenchmark/template/benchmark/src/main/AndroidManifest.xml.mustache +++ b/appcheck/firebase-appcheck/src/androidTest/java/AndroidManifest.xml @@ -1,5 +1,5 @@ - + @@ -13,13 +13,13 @@ - + - - - - + + + + + diff --git a/appcheck/firebase-appcheck/src/androidTest/java/com/google/firebase/appcheck/FirebaseAppCheckTests.kt b/appcheck/firebase-appcheck/src/androidTest/java/com/google/firebase/appcheck/FirebaseAppCheckTests.kt new file mode 100644 index 00000000000..ee159b7de2d --- /dev/null +++ b/appcheck/firebase-appcheck/src/androidTest/java/com/google/firebase/appcheck/FirebaseAppCheckTests.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.appcheck + +import androidx.test.core.app.ApplicationProvider +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import com.google.common.truth.Truth.assertThat +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import com.google.firebase.app +import com.google.firebase.initialize +import com.google.firebase.platforminfo.UserAgentPublisher +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +const val APP_ID = "APP_ID" +const val API_KEY = "API_KEY" + +const val EXISTING_APP = "existing" + +@RunWith(AndroidJUnit4ClassRunner::class) +abstract class BaseTestCase { + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) + + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } + + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } +} + +@RunWith(AndroidJUnit4ClassRunner::class) +class FirebaseAppCheckTests : BaseTestCase() { + @Test + fun appCheck_default_callsDefaultGetInstance() { + assertThat(Firebase.appCheck).isSameInstanceAs(FirebaseAppCheck.getInstance()) + } + + @Test + fun appCheck_with_custom_firebaseapp_calls_GetInstance() { + val app = Firebase.app(EXISTING_APP) + assertThat(Firebase.appCheck(app)).isSameInstanceAs(FirebaseAppCheck.getInstance(app)) + } + + @Test + fun appCheckToken_destructuring_declaration_works() { + val mockAppCheckToken = + object : AppCheckToken() { + override fun getToken(): String = "randomToken" + + override fun getExpireTimeMillis(): Long = 23121997 + } + + val (token, expiration) = mockAppCheckToken + + assertThat(token).isEqualTo(mockAppCheckToken.token) + assertThat(expiration).isEqualTo(mockAppCheckToken.expireTimeMillis) + } +} + +@RunWith(AndroidJUnit4ClassRunner::class) +class LibraryVersionTest : BaseTestCase() { + @Test + fun libraryRegistrationAtRuntime() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + } +} diff --git a/appcheck/firebase-appcheck/src/androidTest/java/com/google/firebase/appcheck/StrictModeTest.java b/appcheck/firebase-appcheck/src/androidTest/java/com/google/firebase/appcheck/StrictModeTest.java new file mode 100644 index 00000000000..6fefe4abea1 --- /dev/null +++ b/appcheck/firebase-appcheck/src/androidTest/java/com/google/firebase/appcheck/StrictModeTest.java @@ -0,0 +1,49 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.appcheck; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider; +import com.google.firebase.testing.integ.StrictModeRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class StrictModeTest { + + @Rule public StrictModeRule strictMode = new StrictModeRule(); + + @Test + public void initializingFirebaseAppcheck_shouldNotViolateStrictMode() { + strictMode.runOnMainThread( + () -> { + FirebaseApp app = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder() + .setApiKey("api") + .setProjectId("123") + .setApplicationId("appId") + .build(), + "hello"); + app.get(FirebaseAppCheck.class); + app.get(InteropAppCheckTokenProvider.class); + }); + } +} diff --git a/appcheck/firebase-appcheck/src/androidTest/java/com/google/firebase/appcheck/ktx/FirebaseAppCheckTests.kt b/appcheck/firebase-appcheck/src/androidTest/java/com/google/firebase/appcheck/ktx/FirebaseAppCheckTests.kt new file mode 100644 index 00000000000..969b0230ed5 --- /dev/null +++ b/appcheck/firebase-appcheck/src/androidTest/java/com/google/firebase/appcheck/ktx/FirebaseAppCheckTests.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.appcheck.ktx + +import androidx.test.core.app.ApplicationProvider +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import com.google.common.truth.Truth.assertThat +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import com.google.firebase.appcheck.AppCheckToken +import com.google.firebase.appcheck.FirebaseAppCheck +import com.google.firebase.ktx.Firebase +import com.google.firebase.ktx.app +import com.google.firebase.ktx.initialize +import com.google.firebase.platforminfo.UserAgentPublisher +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +const val APP_ID = "APP_ID" +const val API_KEY = "API_KEY" + +const val EXISTING_APP = "existing" + +@RunWith(AndroidJUnit4ClassRunner::class) +abstract class BaseTestCase { + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) + + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } + + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } +} + +@RunWith(AndroidJUnit4ClassRunner::class) +class FirebaseAppCheckTests : BaseTestCase() { + @Test + fun appCheck_default_callsDefaultGetInstance() { + assertThat(Firebase.appCheck).isSameInstanceAs(FirebaseAppCheck.getInstance()) + } + + @Test + fun appCheck_with_custom_firebaseapp_calls_GetInstance() { + val app = Firebase.app(EXISTING_APP) + assertThat(Firebase.appCheck(app)).isSameInstanceAs(FirebaseAppCheck.getInstance(app)) + } + + @Test + fun appCheckToken_destructuring_declaration_works() { + val mockAppCheckToken = + object : AppCheckToken() { + override fun getToken(): String = "randomToken" + + override fun getExpireTimeMillis(): Long = 23121997 + } + + val (token, expiration) = mockAppCheckToken + + assertThat(token).isEqualTo(mockAppCheckToken.token) + assertThat(expiration).isEqualTo(mockAppCheckToken.expireTimeMillis) + } +} + +@RunWith(AndroidJUnit4ClassRunner::class) +class LibraryVersionTest : BaseTestCase() { + @Test + fun libraryRegistrationAtRuntime() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + } +} diff --git a/appcheck/firebase-appcheck/src/main/AndroidManifest.xml b/appcheck/firebase-appcheck/src/main/AndroidManifest.xml index d531146c6bf..991bdbcc53a 100644 --- a/appcheck/firebase-appcheck/src/main/AndroidManifest.xml +++ b/appcheck/firebase-appcheck/src/main/AndroidManifest.xml @@ -12,16 +12,14 @@ - + - + + android:exported="false"> - \ No newline at end of file + diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/AppCheckProvider.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/AppCheckProvider.java index eeda91bdecb..6bfd4157a86 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/AppCheckProvider.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/AppCheckProvider.java @@ -19,9 +19,9 @@ /** * Interface for a provider that generates {@link AppCheckToken}s. This provider can be called at - * any time by any Firebase library that depends (optionally or otherwise) on {@link - * AppCheckToken}s. This provider is responsible for determining if it can create a new token at the - * time of the call and returning that new token if it can. + * any time by any Firebase library that depends (optionally or otherwise) on
+ * {@link AppCheckToken}s. This provider is responsible for determining if it can create a new token + * at the time of the call and returning that new token if it can. */ public interface AppCheckProvider { diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheck.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheck.java index b42394f14d0..21d2dc1078c 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheck.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheck.java @@ -19,9 +19,9 @@ import com.google.android.gms.tasks.Task; import com.google.firebase.FirebaseApp; import com.google.firebase.appcheck.interop.AppCheckTokenListener; -import com.google.firebase.appcheck.interop.InternalAppCheckTokenProvider; +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider; -public abstract class FirebaseAppCheck implements InternalAppCheckTokenProvider { +public abstract class FirebaseAppCheck implements InteropAppCheckTokenProvider { /** Gets the default instance of {@code FirebaseAppCheck}. */ @NonNull @@ -75,10 +75,29 @@ public abstract void installAppCheckProviderFactory( * Requests a Firebase App Check token. This method should be used ONLY if you need to authorize * requests to a non-Firebase backend. Requests to Firebase backends are authorized automatically * if configured. + * + *

If your non-Firebase backend exposes a sensitive or expensive endpoint that has low traffic + * volume, consider protecting it with Replay + * Protection. In this case, use {@link #getLimitedUseAppCheckToken()} instead to obtain a + * limited-use token. */ @NonNull public abstract Task getAppCheckToken(boolean forceRefresh); + /** + * Requests a Firebase App Check token. This method should be used ONLY if you need to authorize + * requests to a non-Firebase backend. + * + *

Returns limited-use tokens that are intended for use with your non-Firebase backend + * endpoints that are protected with Replay + * Protection. This method does not affect the token generation behavior of the {@link + * #getAppCheckToken(boolean forceRefresh)} method. + */ + @NonNull + public abstract Task getLimitedUseAppCheckToken(); + /** * Registers an {@link AppCheckListener} to changes in the token state. This method should be used * ONLY if you need to authorize requests to a non-Firebase backend. Requests to Firebase backends diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheck.kt b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheck.kt new file mode 100644 index 00000000000..b0f69179205 --- /dev/null +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheck.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.appcheck + +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.components.Component +import com.google.firebase.components.ComponentRegistrar + +/** Returns the [FirebaseAppCheck] instance of the default [FirebaseApp]. */ +val Firebase.appCheck: FirebaseAppCheck + get() = FirebaseAppCheck.getInstance() + +/** Returns the [FirebaseAppCheck] instance of a given [FirebaseApp]. */ +fun Firebase.appCheck(app: FirebaseApp) = FirebaseAppCheck.getInstance(app) + +/** + * Destructuring declaration for [AppCheckToken] to provide token. + * + * @return the token of the [AppCheckToken] + */ +operator fun AppCheckToken.component1() = token + +/** + * Destructuring declaration for [AppCheckToken] to provide expireTimeMillis. + * + * @return the expireTimeMillis of the [AppCheckToken] + */ +operator fun AppCheckToken.component2() = expireTimeMillis + +/** @suppress */ +class FirebaseAppCheckKtxRegistrar : ComponentRegistrar { + override fun getComponents(): List> = listOf() +} diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrar.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrar.java index 4fd70eb4a17..51a3482fb29 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrar.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrar.java @@ -16,15 +16,23 @@ import com.google.android.gms.common.annotation.KeepForSdk; import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.annotations.concurrent.UiThread; import com.google.firebase.appcheck.internal.DefaultFirebaseAppCheck; -import com.google.firebase.appcheck.interop.InternalAppCheckTokenProvider; +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider; import com.google.firebase.components.Component; import com.google.firebase.components.ComponentRegistrar; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; +import com.google.firebase.heartbeatinfo.HeartBeatConsumerComponent; import com.google.firebase.heartbeatinfo.HeartBeatController; import com.google.firebase.platforminfo.LibraryVersionComponent; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; /** * {@link ComponentRegistrar} for setting up FirebaseAppCheck's dependency injections in Firebase @@ -34,20 +42,37 @@ */ @KeepForSdk public class FirebaseAppCheckRegistrar implements ComponentRegistrar { + private static final String LIBRARY_NAME = "fire-app-check"; @Override public List> getComponents() { + Qualified uiExecutor = Qualified.qualified(UiThread.class, Executor.class); + Qualified liteExecutor = Qualified.qualified(Lightweight.class, Executor.class); + Qualified backgroundExecutor = Qualified.qualified(Background.class, Executor.class); + Qualified blockingScheduledExecutorService = + Qualified.qualified(Blocking.class, ScheduledExecutorService.class); + return Arrays.asList( - Component.builder(FirebaseAppCheck.class, (InternalAppCheckTokenProvider.class)) + Component.builder(FirebaseAppCheck.class, (InteropAppCheckTokenProvider.class)) + .name(LIBRARY_NAME) .add(Dependency.required(FirebaseApp.class)) + .add(Dependency.required(uiExecutor)) + .add(Dependency.required(liteExecutor)) + .add(Dependency.required(backgroundExecutor)) + .add(Dependency.required(blockingScheduledExecutorService)) .add(Dependency.optionalProvider(HeartBeatController.class)) .factory( (container) -> new DefaultFirebaseAppCheck( container.get(FirebaseApp.class), - container.getProvider(HeartBeatController.class))) + container.getProvider(HeartBeatController.class), + container.get(uiExecutor), + container.get(liteExecutor), + container.get(backgroundExecutor), + container.get(blockingScheduledExecutorService))) .alwaysEager() .build(), - LibraryVersionComponent.create("fire-app-check", BuildConfig.VERSION_NAME)); + HeartBeatConsumerComponent.create(), + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); } } diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/AppCheckTokenResponse.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/AppCheckTokenResponse.java index f4975322d85..407ad603c3d 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/AppCheckTokenResponse.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/AppCheckTokenResponse.java @@ -19,40 +19,43 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import com.google.firebase.FirebaseException; import org.json.JSONException; import org.json.JSONObject; /** - * Client-side model of the AttestationTokenResponse payload from the Firebase App Check Token - * Exchange API. + * Client-side model of the AppCheckToken payload from the Firebase App Check Token Exchange API. */ public class AppCheckTokenResponse { - @VisibleForTesting static final String ATTESTATION_TOKEN_KEY = "attestationToken"; + @VisibleForTesting static final String TOKEN_KEY = "token"; @VisibleForTesting static final String TIME_TO_LIVE_KEY = "ttl"; - private String attestationToken; + private String token; private String timeToLive; @NonNull public static AppCheckTokenResponse fromJsonString(@NonNull String jsonString) - throws JSONException { + throws FirebaseException, JSONException { JSONObject jsonObject = new JSONObject(jsonString); - String attestationToken = emptyToNull(jsonObject.optString(ATTESTATION_TOKEN_KEY)); + String token = emptyToNull(jsonObject.optString(TOKEN_KEY)); String timeToLive = emptyToNull(jsonObject.optString(TIME_TO_LIVE_KEY)); - return new AppCheckTokenResponse(attestationToken, timeToLive); + if (token == null || timeToLive == null) { + throw new FirebaseException("Unexpected server response."); + } + return new AppCheckTokenResponse(token, timeToLive); } - private AppCheckTokenResponse(@NonNull String attestationToken, @NonNull String timeToLive) { - checkNotNull(attestationToken); + private AppCheckTokenResponse(@NonNull String token, @NonNull String timeToLive) { + checkNotNull(token); checkNotNull(timeToLive); - this.attestationToken = attestationToken; + this.token = token; this.timeToLive = timeToLive; } @NonNull - public String getAttestationToken() { - return attestationToken; + public String getToken() { + return token; } @NonNull diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultAppCheckToken.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultAppCheckToken.java index fcd99e1a840..0b1d1b651a3 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultAppCheckToken.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultAppCheckToken.java @@ -41,6 +41,8 @@ public final class DefaultAppCheckToken extends AppCheckToken { @VisibleForTesting static final String ISSUED_AT_KEY = "iat"; @VisibleForTesting static final String EXPIRATION_TIME_KEY = "exp"; + private static final long ONE_SECOND_MILLIS = 1000L; + // Raw token value private final String token; // Timestamp in MS at which this token was generated @@ -75,14 +77,13 @@ public static DefaultAppCheckToken constructFromAppCheckTokenResponse( } catch (NumberFormatException e) { // If parsing the duration string returned by the server fails for any reason, fall back to // computing the timeToLive from the token claims directly. - Map claimsMap = - TokenParser.parseTokenClaims(tokenResponse.getAttestationToken()); + Map claimsMap = TokenParser.parseTokenClaims(tokenResponse.getToken()); long iat = getLongFromClaimsSafely(claimsMap, ISSUED_AT_KEY); long exp = getLongFromClaimsSafely(claimsMap, EXPIRATION_TIME_KEY); - expiresInMillis = exp - iat; + expiresInMillis = (exp - iat) * ONE_SECOND_MILLIS; } - return new DefaultAppCheckToken(tokenResponse.getAttestationToken(), expiresInMillis); + return new DefaultAppCheckToken(tokenResponse.getToken(), expiresInMillis); } @NonNull @@ -138,10 +139,10 @@ public static DefaultAppCheckToken constructFromRawToken(@NonNull String token) Map claimsMap = TokenParser.parseTokenClaims(token); long iat = getLongFromClaimsSafely(claimsMap, ISSUED_AT_KEY); long exp = getLongFromClaimsSafely(claimsMap, EXPIRATION_TIME_KEY); - long expiresInMillis = exp - iat; + long expiresInMillis = (exp - iat) * ONE_SECOND_MILLIS; // We use iat for receivedAtTimestamp as an approximation since we have to guess for raw JWTs // that we recovered from storage - return new DefaultAppCheckToken(token, expiresInMillis, iat); + return new DefaultAppCheckToken(token, expiresInMillis, iat * ONE_SECOND_MILLIS); } private static long getLongFromClaimsSafely( diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultAppCheckTokenResult.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultAppCheckTokenResult.java index 02bddb94e23..4398d0a6a70 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultAppCheckTokenResult.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultAppCheckTokenResult.java @@ -58,7 +58,7 @@ public String getToken() { @Nullable @Override - public FirebaseException getError() { + public Exception getError() { return error; } } diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheck.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheck.java index 087d10b9868..737b314457a 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheck.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheck.java @@ -19,11 +19,15 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseException; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.annotations.concurrent.UiThread; import com.google.firebase.appcheck.AppCheckProvider; import com.google.firebase.appcheck.AppCheckProviderFactory; import com.google.firebase.appcheck.AppCheckToken; @@ -35,6 +39,8 @@ import com.google.firebase.inject.Provider; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; public class DefaultFirebaseAppCheck extends FirebaseAppCheck { @@ -46,15 +52,24 @@ public class DefaultFirebaseAppCheck extends FirebaseAppCheck { private final List appCheckListenerList; private final StorageHelper storageHelper; private final TokenRefreshManager tokenRefreshManager; + private final Executor uiExecutor; + private final Executor liteExecutor; + private final Executor backgroundExecutor; + private final Task retrieveStoredTokenTask; private final Clock clock; private AppCheckProviderFactory appCheckProviderFactory; private AppCheckProvider appCheckProvider; private AppCheckToken cachedToken; + private Task cachedTokenTask; public DefaultFirebaseAppCheck( @NonNull FirebaseApp firebaseApp, - @NonNull Provider heartBeatController) { + @NonNull Provider heartBeatController, + @UiThread Executor uiExecutor, + @Lightweight Executor liteExecutor, + @Background Executor backgroundExecutor, + @Blocking ScheduledExecutorService scheduledExecutorService) { checkNotNull(firebaseApp); checkNotNull(heartBeatController); this.firebaseApp = firebaseApp; @@ -64,9 +79,29 @@ public DefaultFirebaseAppCheck( this.storageHelper = new StorageHelper(firebaseApp.getApplicationContext(), firebaseApp.getPersistenceKey()); this.tokenRefreshManager = - new TokenRefreshManager(firebaseApp.getApplicationContext(), /* firebaseAppCheck= */ this); + new TokenRefreshManager( + firebaseApp.getApplicationContext(), + /* firebaseAppCheck= */ this, + liteExecutor, + scheduledExecutorService); + this.uiExecutor = uiExecutor; + this.liteExecutor = liteExecutor; + this.backgroundExecutor = backgroundExecutor; + this.retrieveStoredTokenTask = retrieveStoredAppCheckTokenInBackground(backgroundExecutor); this.clock = new Clock.DefaultClock(); - setCachedToken(storageHelper.retrieveAppCheckToken()); + } + + private Task retrieveStoredAppCheckTokenInBackground(@NonNull Executor executor) { + TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); + executor.execute( + () -> { + AppCheckToken token = storageHelper.retrieveAppCheckToken(); + if (token != null) { + setCachedToken(token); + } + taskCompletionSource.setResult(null); + }); + return taskCompletionSource.getTask(); } @Override @@ -146,68 +181,115 @@ public void removeAppCheckListener(@NonNull AppCheckListener listener) { @NonNull @Override public Task getToken(boolean forceRefresh) { - if (!forceRefresh && hasValidToken()) { - return Tasks.forResult(DefaultAppCheckTokenResult.constructFromAppCheckToken(cachedToken)); - } - if (appCheckProvider == null) { - return Tasks.forResult( - DefaultAppCheckTokenResult.constructFromError( - new FirebaseException("No AppCheckProvider installed."))); - } - // TODO: Cache the in-flight task. - return fetchTokenFromProvider() - .continueWithTask( - new Continuation>() { - @Override - public Task then(@NonNull Task task) { - if (task.isSuccessful()) { + return retrieveStoredTokenTask.continueWithTask( + liteExecutor, + unused -> { + if (!forceRefresh && hasValidToken()) { + return Tasks.forResult( + DefaultAppCheckTokenResult.constructFromAppCheckToken(cachedToken)); + } + if (appCheckProvider == null) { + return Tasks.forResult( + DefaultAppCheckTokenResult.constructFromError( + new FirebaseException("No AppCheckProvider installed."))); + } + if (cachedTokenTask == null + || cachedTokenTask.isComplete() + || cachedTokenTask.isCanceled()) { + cachedTokenTask = fetchTokenFromProvider(); + } + return cachedTokenTask.continueWithTask( + liteExecutor, + appCheckTokenTask -> { + if (appCheckTokenTask.isSuccessful()) { return Tasks.forResult( - DefaultAppCheckTokenResult.constructFromAppCheckToken(task.getResult())); + DefaultAppCheckTokenResult.constructFromAppCheckToken( + appCheckTokenTask.getResult())); } - // If the token exchange failed, return a dummy token for integrators to attach in - // their headers. + // If the token exchange failed, return a dummy token for integrators to attach + // in their headers. return Tasks.forResult( DefaultAppCheckTokenResult.constructFromError( new FirebaseException( - task.getException().getMessage(), task.getException()))); + appCheckTokenTask.getException().getMessage(), + appCheckTokenTask.getException()))); + }); + }); + } + + @NonNull + @Override + public Task getLimitedUseToken() { + return getLimitedUseAppCheckToken() + .continueWithTask( + liteExecutor, + appCheckTokenTask -> { + if (appCheckTokenTask.isSuccessful()) { + return Tasks.forResult( + DefaultAppCheckTokenResult.constructFromAppCheckToken( + appCheckTokenTask.getResult())); } + // If the token exchange failed, return a dummy token for integrators to attach + // in their headers. + return Tasks.forResult( + DefaultAppCheckTokenResult.constructFromError( + new FirebaseException( + appCheckTokenTask.getException().getMessage(), + appCheckTokenTask.getException()))); }); } @NonNull @Override public Task getAppCheckToken(boolean forceRefresh) { - if (!forceRefresh && hasValidToken()) { - return Tasks.forResult(cachedToken); - } + return retrieveStoredTokenTask.continueWithTask( + liteExecutor, + unused -> { + if (!forceRefresh && hasValidToken()) { + return Tasks.forResult(cachedToken); + } + if (appCheckProvider == null) { + return Tasks.forException(new FirebaseException("No AppCheckProvider installed.")); + } + if (cachedTokenTask == null + || cachedTokenTask.isComplete() + || cachedTokenTask.isCanceled()) { + cachedTokenTask = fetchTokenFromProvider(); + } + return cachedTokenTask; + }); + } + + @NonNull + @Override + public Task getLimitedUseAppCheckToken() { if (appCheckProvider == null) { return Tasks.forException(new FirebaseException("No AppCheckProvider installed.")); } - return fetchTokenFromProvider(); + + // We explicitly do not call the fetchTokenFromProvider helper method, as that method includes + // side effects such as notifying listeners, updating the cached token, and scheduling token + // refresh. + return appCheckProvider.getToken(); } /** Fetches an {@link AppCheckToken} via the installed {@link AppCheckProvider}. */ Task fetchTokenFromProvider() { return appCheckProvider .getToken() - .continueWithTask( - new Continuation>() { - @Override - public Task then(@NonNull Task task) { - if (task.isSuccessful()) { - AppCheckToken token = task.getResult(); - updateStoredToken(token); - for (AppCheckListener listener : appCheckListenerList) { - listener.onAppCheckTokenChanged(token); - } - AppCheckTokenResult tokenResult = - DefaultAppCheckTokenResult.constructFromAppCheckToken(token); - for (AppCheckTokenListener listener : appCheckTokenListenerList) { - listener.onAppCheckTokenChanged(tokenResult); - } - } - return task; + .onSuccessTask( + uiExecutor, + token -> { + updateStoredToken(token); + for (AppCheckListener listener : appCheckListenerList) { + listener.onAppCheckTokenChanged(token); + } + AppCheckTokenResult tokenResult = + DefaultAppCheckTokenResult.constructFromAppCheckToken(token); + for (AppCheckTokenListener listener : appCheckTokenListenerList) { + listener.onAppCheckTokenChanged(tokenResult); } + return Tasks.forResult(token); }); } @@ -227,7 +309,7 @@ void setCachedToken(@NonNull AppCheckToken token) { * well as the in-memory cached {@link AppCheckToken}. */ private void updateStoredToken(@NonNull AppCheckToken token) { - storageHelper.saveAppCheckToken(token); + backgroundExecutor.execute(() -> storageHelper.saveAppCheckToken(token)); setCachedToken(token); tokenRefreshManager.maybeScheduleTokenRefresh(token); diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultTokenRefresher.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultTokenRefresher.java index 8e7f6722046..5098b8a6d53 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultTokenRefresher.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultTokenRefresher.java @@ -20,8 +20,9 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import com.google.android.gms.tasks.OnFailureListener; -import java.util.concurrent.Executors; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -36,19 +37,18 @@ public class DefaultTokenRefresher { @VisibleForTesting static final long MAX_DELAY_SECONDS = 16 * 60; // 16 minutes private final DefaultFirebaseAppCheck firebaseAppCheck; + private final Executor liteExecutor; private final ScheduledExecutorService scheduledExecutorService; private volatile ScheduledFuture refreshFuture; private volatile long delayAfterFailureSeconds; - DefaultTokenRefresher(@NonNull DefaultFirebaseAppCheck firebaseAppCheck) { - this(checkNotNull(firebaseAppCheck), Executors.newScheduledThreadPool(/* corePoolSize= */ 1)); - } - - @VisibleForTesting DefaultTokenRefresher( - DefaultFirebaseAppCheck firebaseAppCheck, ScheduledExecutorService scheduledExecutorService) { - this.firebaseAppCheck = firebaseAppCheck; + @NonNull DefaultFirebaseAppCheck firebaseAppCheck, + @Lightweight Executor liteExecutor, + @Blocking ScheduledExecutorService scheduledExecutorService) { + this.firebaseAppCheck = checkNotNull(firebaseAppCheck); + this.liteExecutor = liteExecutor; this.scheduledExecutorService = scheduledExecutorService; this.delayAfterFailureSeconds = UNSET_DELAY; } @@ -93,13 +93,7 @@ private long getNextRefreshMillis() { private void onRefresh() { firebaseAppCheck .fetchTokenFromProvider() - .addOnFailureListener( - new OnFailureListener() { - @Override - public void onFailure(@NonNull Exception e) { - scheduleRefreshAfterFailure(); - } - }); + .addOnFailureListener(liteExecutor, e -> scheduleRefreshAfterFailure()); } /** Cancels the in-flight scheduled refresh. */ diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java index 4cf4de5c602..959a64efda2 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java @@ -51,10 +51,12 @@ public class NetworkClient { private static final String TAG = NetworkClient.class.getName(); - private static final String SAFETY_NET_EXCHANGE_URL_TEMPLATE = - "https://firebaseappcheck.googleapis.com/v1beta/projects/%s/apps/%s:exchangeSafetyNetToken?key=%s"; private static final String DEBUG_EXCHANGE_URL_TEMPLATE = - "https://firebaseappcheck.googleapis.com/v1beta/projects/%s/apps/%s:exchangeDebugToken?key=%s"; + "https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:exchangeDebugToken?key=%s"; + private static final String PLAY_INTEGRITY_EXCHANGE_URL_TEMPLATE = + "https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:exchangePlayIntegrityToken?key=%s"; + private static final String PLAY_INTEGRITY_CHALLENGE_URL_TEMPLATE = + "https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:generatePlayIntegrityChallenge?key=%s"; private static final String CONTENT_TYPE = "Content-Type"; private static final String APPLICATION_JSON = "application/json"; private static final String UTF_8 = "UTF-8"; @@ -69,12 +71,12 @@ public class NetworkClient { private final Provider heartBeatControllerProvider; @Retention(RetentionPolicy.SOURCE) - @IntDef({UNKNOWN, SAFETY_NET, DEBUG}) + @IntDef({UNKNOWN, DEBUG, PLAY_INTEGRITY}) public @interface AttestationTokenType {} public static final int UNKNOWN = 0; - public static final int SAFETY_NET = 1; public static final int DEBUG = 2; + public static final int PLAY_INTEGRITY = 3; public NetworkClient(@NonNull FirebaseApp firebaseApp) { this( @@ -116,6 +118,34 @@ public AppCheckTokenResponse exchangeAttestationForAppCheckToken( throw new FirebaseException("Too many attempts."); } URL url = new URL(String.format(getUrlTemplate(tokenType), projectId, appId, apiKey)); + String response = + makeNetworkRequest(url, requestBytes, retryManager, /* resetRetryManagerOnSuccess= */ true); + return AppCheckTokenResponse.fromJsonString(response); + } + + /** + * Calls the App Check backend using {@link HttpURLConnection} in order to generate a challenge + * nonce for the Play Integrity attestation flow. + */ + @NonNull + public String generatePlayIntegrityChallenge( + @NonNull byte[] requestBytes, @NonNull RetryManager retryManager) + throws FirebaseException, IOException, JSONException { + if (!retryManager.canRetry()) { + throw new FirebaseException("Too many attempts."); + } + URL url = + new URL(String.format(PLAY_INTEGRITY_CHALLENGE_URL_TEMPLATE, projectId, appId, apiKey)); + return makeNetworkRequest( + url, requestBytes, retryManager, /* resetRetryManagerOnSuccess= */ false); + } + + private String makeNetworkRequest( + @NonNull URL url, + @NonNull byte[] requestBytes, + @NonNull RetryManager retryManager, + boolean resetRetryManagerOnSuccess) + throws FirebaseException, IOException, JSONException { HttpURLConnection urlConnection = createHttpUrlConnection(url); try { @@ -159,8 +189,10 @@ public AppCheckTokenResponse exchangeAttestationForAppCheckToken( + " body: " + httpErrorResponse.getErrorMessage()); } - retryManager.resetBackoffOnSuccess(); - return AppCheckTokenResponse.fromJsonString(responseBody); + if (resetRetryManagerOnSuccess) { + retryManager.resetBackoffOnSuccess(); + } + return responseBody; } finally { urlConnection.disconnect(); } @@ -199,10 +231,10 @@ private String getFingerprintHashForPackage() { private static String getUrlTemplate(@AttestationTokenType int tokenType) { switch (tokenType) { - case SAFETY_NET: - return SAFETY_NET_EXCHANGE_URL_TEMPLATE; case DEBUG: return DEBUG_EXCHANGE_URL_TEMPLATE; + case PLAY_INTEGRITY: + return PLAY_INTEGRITY_EXCHANGE_URL_TEMPLATE; default: throw new IllegalArgumentException("Unknown token type."); } diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/RetryManager.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/RetryManager.java index ea8c6cde029..8798c655512 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/RetryManager.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/RetryManager.java @@ -26,7 +26,6 @@ public class RetryManager { @VisibleForTesting static final int BAD_REQUEST_ERROR_CODE = 400; - @VisibleForTesting static final int FORBIDDEN_ERROR_CODE = 403; @VisibleForTesting static final int NOT_FOUND_ERROR_CODE = 404; @VisibleForTesting static final long MAX_EXP_BACKOFF_MILLIS = 4 * 60 * 60 * 1000; // 4 hours @VisibleForTesting static final long ONE_DAY_MILLIS = 24 * 60 * 60 * 1000; // 24 hours @@ -91,9 +90,7 @@ public void updateBackoffOnFailure(int errorCode) { @BackoffStrategyType private static int getBackoffStrategyByErrorCode(int errorCode) { - if (errorCode == BAD_REQUEST_ERROR_CODE - || errorCode == FORBIDDEN_ERROR_CODE - || errorCode == NOT_FOUND_ERROR_CODE) { + if (errorCode == BAD_REQUEST_ERROR_CODE || errorCode == NOT_FOUND_ERROR_CODE) { return ONE_DAY; } return EXPONENTIAL; diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/StorageHelper.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/StorageHelper.java index 5537b636cf9..ae821de0d39 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/StorageHelper.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/StorageHelper.java @@ -24,6 +24,7 @@ import androidx.annotation.VisibleForTesting; import com.google.firebase.appcheck.AppCheckToken; import com.google.firebase.appcheck.internal.util.Logger; +import com.google.firebase.components.Lazy; /** * Internal class used to persist {@link com.google.firebase.appcheck.AppCheckToken}s. Uses {@link @@ -47,24 +48,27 @@ enum TokenType { UNKNOWN_APP_CHECK_TOKEN } - private SharedPreferences sharedPreferences; + private Lazy sharedPreferences; public StorageHelper(@NonNull Context context, @NonNull String persistenceKey) { checkNotNull(context); checkNotEmpty(persistenceKey); String prefsName = String.format(PREFS_TEMPLATE, persistenceKey); - this.sharedPreferences = context.getSharedPreferences(prefsName, Context.MODE_PRIVATE); + this.sharedPreferences = + new Lazy(() -> context.getSharedPreferences(prefsName, Context.MODE_PRIVATE)); } public void saveAppCheckToken(@NonNull AppCheckToken appCheckToken) { if (appCheckToken instanceof DefaultAppCheckToken) { sharedPreferences + .get() .edit() .putString(TOKEN_KEY, ((DefaultAppCheckToken) appCheckToken).serializeTokenToString()) .putString(TOKEN_TYPE_KEY, TokenType.DEFAULT_APP_CHECK_TOKEN.name()) .apply(); } else { sharedPreferences + .get() .edit() .putString(TOKEN_KEY, appCheckToken.getToken()) .putString(TOKEN_TYPE_KEY, TokenType.UNKNOWN_APP_CHECK_TOKEN.name()) @@ -74,8 +78,8 @@ public void saveAppCheckToken(@NonNull AppCheckToken appCheckToken) { @Nullable public AppCheckToken retrieveAppCheckToken() { - String tokenType = sharedPreferences.getString(TOKEN_TYPE_KEY, null); - String serializedToken = sharedPreferences.getString(TOKEN_KEY, null); + String tokenType = sharedPreferences.get().getString(TOKEN_TYPE_KEY, null); + String serializedToken = sharedPreferences.get().getString(TOKEN_KEY, null); if (tokenType == null || serializedToken == null) { return null; } @@ -101,6 +105,6 @@ public AppCheckToken retrieveAppCheckToken() { } void clearSharedPrefs() { - sharedPreferences.edit().remove(TOKEN_KEY).remove(TOKEN_TYPE_KEY).apply(); + sharedPreferences.get().edit().remove(TOKEN_KEY).remove(TOKEN_TYPE_KEY).apply(); } } diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/TokenRefreshManager.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/TokenRefreshManager.java index ac116dd0ca7..4ac16a05c5b 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/TokenRefreshManager.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/TokenRefreshManager.java @@ -22,8 +22,12 @@ import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.api.internal.BackgroundDetector; import com.google.android.gms.common.api.internal.BackgroundDetector.BackgroundStateChangeListener; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; import com.google.firebase.appcheck.AppCheckToken; import com.google.firebase.appcheck.internal.util.Clock; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; /** Class to manage whether or not to schedule an {@link AppCheckToken} refresh attempt. */ public final class TokenRefreshManager { @@ -41,10 +45,15 @@ public final class TokenRefreshManager { private volatile long nextRefreshTimeMillis; private volatile boolean isAutoRefreshEnabled; - TokenRefreshManager(@NonNull Context context, @NonNull DefaultFirebaseAppCheck firebaseAppCheck) { + TokenRefreshManager( + @NonNull Context context, + @NonNull DefaultFirebaseAppCheck firebaseAppCheck, + @Lightweight Executor liteExecutor, + @Blocking ScheduledExecutorService scheduledExecutorService) { this( checkNotNull(context), - new DefaultTokenRefresher(checkNotNull(firebaseAppCheck)), + new DefaultTokenRefresher( + checkNotNull(firebaseAppCheck), liteExecutor, scheduledExecutorService), new Clock.DefaultClock()); } diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/ktx/FirebaseAppCheck.kt b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/ktx/FirebaseAppCheck.kt new file mode 100644 index 00000000000..15e1f5b2189 --- /dev/null +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/ktx/FirebaseAppCheck.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.appcheck.ktx + +import com.google.firebase.FirebaseApp +import com.google.firebase.appcheck.AppCheckToken +import com.google.firebase.appcheck.FirebaseAppCheck +import com.google.firebase.components.Component +import com.google.firebase.components.ComponentRegistrar +import com.google.firebase.ktx.Firebase +import com.google.firebase.ktx.app + +/** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * + * Returns the [FirebaseAppCheck] instance of the default [FirebaseApp]. + * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their + * respective main modules, and the Kotlin extension (KTX) APIs in + * `com.google.firebase.firebase-appcheck-ktx` are now deprecated. As early as April 2024, we'll no + * longer release KTX modules. For details, see the + * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) + */ +val Firebase.appCheck: FirebaseAppCheck + get() = FirebaseAppCheck.getInstance() + +/** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * + * Returns the [FirebaseAppCheck] instance of a given [FirebaseApp]. + * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their + * respective main modules, and the Kotlin extension (KTX) APIs in + * `com.google.firebase.firebase-appcheck-ktx` are now deprecated. As early as April 2024, we'll no + * longer release KTX modules. For details, see the + * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) + */ +fun Firebase.appCheck(app: FirebaseApp) = FirebaseAppCheck.getInstance(app) + +/** + * Destructuring declaration for [AppCheckToken] to provide token. + * + * @return the token of the [AppCheckToken] + * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their + * respective main modules, and the Kotlin extension (KTX) APIs in + * `com.google.firebase.firebase-appcheck-ktx` are now deprecated. As early as April 2024, we'll no + * longer release KTX modules. For details, see the + * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) + */ +@Deprecated( + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") +) +operator fun AppCheckToken.component1() = token + +/** + * Destructuring declaration for [AppCheckToken] to provide expireTimeMillis. + * + * @return the expireTimeMillis of the [AppCheckToken] + * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their + * respective main modules, and the Kotlin extension (KTX) APIs in + * `com.google.firebase.firebase-appcheck-ktx` are now deprecated. As early as April 2024, we'll no + * longer release KTX modules. For details, see the + * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) + */ +@Deprecated( + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") +) +operator fun AppCheckToken.component2() = expireTimeMillis + +/** + * @suppress + * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their + * respective main modules, and the Kotlin extension (KTX) APIs in + * `com.google.firebase.firebase-appcheck-ktx` are now deprecated. As early as April 2024, we'll no + * longer release KTX modules. For details, see the + * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) + */ +@Deprecated( + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") +) +class FirebaseAppCheckKtxRegistrar : ComponentRegistrar { + override fun getComponents(): List> = listOf() +} diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrarTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrarTest.java index 3b235c14ce4..c5e6a35d112 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrarTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrarTest.java @@ -17,10 +17,17 @@ import static com.google.common.truth.Truth.assertThat; import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.annotations.concurrent.UiThread; import com.google.firebase.components.Component; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.heartbeatinfo.HeartBeatController; import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -34,11 +41,16 @@ public void testGetComponents() { FirebaseAppCheckRegistrar firebaseAppCheckRegistrar = new FirebaseAppCheckRegistrar(); List> components = firebaseAppCheckRegistrar.getComponents(); assertThat(components).isNotEmpty(); - assertThat(components).hasSize(2); + assertThat(components).hasSize(3); Component firebaseAppCheckComponent = components.get(0); assertThat(firebaseAppCheckComponent.getDependencies()) .containsExactly( Dependency.required(FirebaseApp.class), + Dependency.required(Qualified.qualified(UiThread.class, Executor.class)), + Dependency.required(Qualified.qualified(Lightweight.class, Executor.class)), + Dependency.required(Qualified.qualified(Background.class, Executor.class)), + Dependency.required( + Qualified.qualified(Blocking.class, ScheduledExecutorService.class)), Dependency.optionalProvider(HeartBeatController.class)); assertThat(firebaseAppCheckComponent.isAlwaysEager()).isTrue(); } diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/AppCheckTokenResponseTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/AppCheckTokenResponseTest.java index 8a25459692b..db2255d151f 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/AppCheckTokenResponseTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/AppCheckTokenResponseTest.java @@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import com.google.firebase.FirebaseException; import org.json.JSONObject; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,38 +29,36 @@ @Config(manifest = Config.NONE) public class AppCheckTokenResponseTest { - private static final String ATTESTATION_TOKEN = "attestationToken"; - private static final String TIME_TO_LIVE = "ttl"; + private static final String APP_CHECK_TOKEN = "appCheckToken"; + private static final String TIME_TO_LIVE = "3600s"; @Test public void fromJsonString_expectDeserialized() throws Exception { JSONObject jsonObject = new JSONObject(); - jsonObject.put(AppCheckTokenResponse.ATTESTATION_TOKEN_KEY, ATTESTATION_TOKEN); + jsonObject.put(AppCheckTokenResponse.TOKEN_KEY, APP_CHECK_TOKEN); jsonObject.put(AppCheckTokenResponse.TIME_TO_LIVE_KEY, TIME_TO_LIVE); AppCheckTokenResponse appCheckTokenResponse = AppCheckTokenResponse.fromJsonString(jsonObject.toString()); - assertThat(appCheckTokenResponse.getAttestationToken()).isEqualTo(ATTESTATION_TOKEN); + assertThat(appCheckTokenResponse.getToken()).isEqualTo(APP_CHECK_TOKEN); assertThat(appCheckTokenResponse.getTimeToLive()).isEqualTo(TIME_TO_LIVE); } @Test - public void fromJsonString_nullAttestationToken_throwsException() throws Exception { + public void fromJsonString_nullToken_throwsException() throws Exception { JSONObject jsonObject = new JSONObject(); jsonObject.put(AppCheckTokenResponse.TIME_TO_LIVE_KEY, TIME_TO_LIVE); assertThrows( - NullPointerException.class, - () -> AppCheckTokenResponse.fromJsonString(jsonObject.toString())); + FirebaseException.class, () -> AppCheckTokenResponse.fromJsonString(jsonObject.toString())); } @Test public void fromJsonString_nullTimeToLive_throwsException() throws Exception { JSONObject jsonObject = new JSONObject(); - jsonObject.put(AppCheckTokenResponse.ATTESTATION_TOKEN_KEY, ATTESTATION_TOKEN); + jsonObject.put(AppCheckTokenResponse.TOKEN_KEY, APP_CHECK_TOKEN); assertThrows( - NullPointerException.class, - () -> AppCheckTokenResponse.fromJsonString(jsonObject.toString())); + FirebaseException.class, () -> AppCheckTokenResponse.fromJsonString(jsonObject.toString())); } } diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultAppCheckTokenTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultAppCheckTokenTest.java index 1f98a079c9f..b9be2652d1e 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultAppCheckTokenTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultAppCheckTokenTest.java @@ -37,6 +37,7 @@ public class DefaultAppCheckTokenTest { private static final String INVALID_TIME_TO_LIVE = "notanumber"; private static final long EXPIRES_IN_ONE_HOUR = 60L * 60L * 1000L; // 1 hour in millis private static final long RECEIVED_AT_TIMESTAMP = 1L; + private static final long ONE_SECOND_MILLIS = 1000L; private static final long IAT = 10L; private static final long EXP = 30L; private static final String TOKEN_PREFIX = "prefix"; @@ -104,13 +105,14 @@ public void testConstructFromRawToken_normalToken_expectSuccess() throws Excepti assertThat(defaultAppCheckToken).isNotNull(); assertThat(defaultAppCheckToken.getToken()).isEqualTo(rawToken); - assertThat(defaultAppCheckToken.getReceivedAtTimestamp()).isEqualTo(IAT); - assertThat(defaultAppCheckToken.getExpiresInMillis()).isEqualTo(EXP - IAT); + assertThat(defaultAppCheckToken.getReceivedAtTimestamp()).isEqualTo(IAT * ONE_SECOND_MILLIS); + assertThat(defaultAppCheckToken.getExpiresInMillis()) + .isEqualTo((EXP - IAT) * ONE_SECOND_MILLIS); } @Test public void testConstructFromAppCheckTokenResponse_success() { - when(mockAppCheckTokenResponse.getAttestationToken()).thenReturn(TOKEN_PAYLOAD); + when(mockAppCheckTokenResponse.getToken()).thenReturn(TOKEN_PAYLOAD); when(mockAppCheckTokenResponse.getTimeToLive()).thenReturn(TIME_TO_LIVE_ONE_HOUR); DefaultAppCheckToken defaultAppCheckToken = @@ -122,7 +124,7 @@ public void testConstructFromAppCheckTokenResponse_success() { @Test public void testConstructFromAppCheckTokenResponse_withNanoSecondsDuration_success() { - when(mockAppCheckTokenResponse.getAttestationToken()).thenReturn(TOKEN_PAYLOAD); + when(mockAppCheckTokenResponse.getToken()).thenReturn(TOKEN_PAYLOAD); when(mockAppCheckTokenResponse.getTimeToLive()).thenReturn(TIME_TO_LIVE_ONE_HOUR_PLUS_NANOS); DefaultAppCheckToken defaultAppCheckToken = @@ -136,14 +138,15 @@ public void testConstructFromAppCheckTokenResponse_withNanoSecondsDuration_succe public void testConstructFromAppCheckTokenResponse_invalidTimeToLiveFormat_fallbackToTokenClaims() throws Exception { String rawToken = constructFakeRawToken(); - when(mockAppCheckTokenResponse.getAttestationToken()).thenReturn(rawToken); + when(mockAppCheckTokenResponse.getToken()).thenReturn(rawToken); when(mockAppCheckTokenResponse.getTimeToLive()).thenReturn(INVALID_TIME_TO_LIVE); DefaultAppCheckToken defaultAppCheckToken = DefaultAppCheckToken.constructFromAppCheckTokenResponse(mockAppCheckTokenResponse); assertThat(defaultAppCheckToken.getToken()).isEqualTo(rawToken); - assertThat(defaultAppCheckToken.getExpiresInMillis()).isEqualTo(EXP - IAT); + assertThat(defaultAppCheckToken.getExpiresInMillis()) + .isEqualTo((EXP - IAT) * ONE_SECOND_MILLIS); } private String constructFakeRawToken() throws Exception { diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheckTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheckTest.java index 320dbd9d1c2..5d5974036e6 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheckTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheckTest.java @@ -24,6 +24,7 @@ import androidx.test.core.app.ApplicationProvider; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; +import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.FirebaseApp; import com.google.firebase.appcheck.AppCheckProvider; import com.google.firebase.appcheck.AppCheckProviderFactory; @@ -31,6 +32,7 @@ import com.google.firebase.appcheck.AppCheckTokenResult; import com.google.firebase.appcheck.FirebaseAppCheck.AppCheckListener; import com.google.firebase.appcheck.interop.AppCheckTokenListener; +import com.google.firebase.concurrent.TestOnlyExecutors; import com.google.firebase.heartbeatinfo.HeartBeatController; import org.junit.Before; import org.junit.Test; @@ -40,10 +42,12 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; /** Tests for {@link DefaultFirebaseAppCheck}. */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) +@LooperMode(LooperMode.Mode.LEGACY) public class DefaultFirebaseAppCheckTest { private static final String EXCEPTION_TEXT = "exceptionText"; @@ -73,8 +77,15 @@ public void setup() { when(mockAppCheckProviderFactory.create(any())).thenReturn(mockAppCheckProvider); when(mockAppCheckProvider.getToken()).thenReturn(Tasks.forResult(validDefaultAppCheckToken)); + // TODO(b/258273630): Use TestOnlyExecutors instead of MoreExecutors.directExecutor(). defaultFirebaseAppCheck = - new DefaultFirebaseAppCheck(mockFirebaseApp, () -> mockHeartBeatController); + new DefaultFirebaseAppCheck( + mockFirebaseApp, + () -> mockHeartBeatController, + TestOnlyExecutors.ui(), + /* liteExecutor= */ MoreExecutors.directExecutor(), + /* backgroundExecutor= */ MoreExecutors.directExecutor(), + TestOnlyExecutors.blocking()); } @Test @@ -82,7 +93,13 @@ public void testConstructor_nullFirebaseApp_expectThrows() { assertThrows( NullPointerException.class, () -> { - new DefaultFirebaseAppCheck(null, () -> mockHeartBeatController); + new DefaultFirebaseAppCheck( + null, + () -> mockHeartBeatController, + TestOnlyExecutors.ui(), + TestOnlyExecutors.lite(), + TestOnlyExecutors.background(), + TestOnlyExecutors.blocking()); }); } @@ -91,7 +108,13 @@ public void testConstructor_nullHeartBeatControllerProvider_expectThrows() { assertThrows( NullPointerException.class, () -> { - new DefaultFirebaseAppCheck(mockFirebaseApp, null); + new DefaultFirebaseAppCheck( + mockFirebaseApp, + null, + TestOnlyExecutors.ui(), + TestOnlyExecutors.lite(), + TestOnlyExecutors.background(), + TestOnlyExecutors.blocking()); }); } @@ -205,6 +228,22 @@ public void testGetAppCheckToken_noFactoryInstalled_taskFails() throws Exception assertThat(tokenTask.isSuccessful()).isFalse(); } + @Test + public void testGetLimitedUseAppCheckToken_noFactoryInstalled_taskFails() throws Exception { + Task tokenTask = defaultFirebaseAppCheck.getLimitedUseAppCheckToken(); + assertThat(tokenTask.isComplete()).isTrue(); + assertThat(tokenTask.isSuccessful()).isFalse(); + } + + @Test + public void testGetLimitedUseToken_noFactoryInstalled_returnResultWithError() throws Exception { + Task tokenTask = defaultFirebaseAppCheck.getLimitedUseToken(); + assertThat(tokenTask.isComplete()).isTrue(); + assertThat(tokenTask.isSuccessful()).isTrue(); + assertThat(tokenTask.getResult().getToken()).isNotNull(); + assertThat(tokenTask.getResult().getError()).isNotNull(); + } + @Test public void testGetToken_factoryInstalled_proxiesToAppCheckFactory() { defaultFirebaseAppCheck.installAppCheckProviderFactory(mockAppCheckProviderFactory); @@ -373,4 +412,42 @@ public void testGetAppCheckToken_existingInvalidToken_requestsNewToken() { verify(mockAppCheckProvider).getToken(); } + + @Test + public void testGetLimitedUseAppCheckToken_noExistingToken_requestsNewToken() { + defaultFirebaseAppCheck.installAppCheckProviderFactory(mockAppCheckProviderFactory); + + defaultFirebaseAppCheck.getLimitedUseAppCheckToken(); + + verify(mockAppCheckProvider).getToken(); + } + + @Test + public void testGetLimitedUseAppCheckToken_existingToken_requestsNewToken() { + defaultFirebaseAppCheck.setCachedToken(validDefaultAppCheckToken); + defaultFirebaseAppCheck.installAppCheckProviderFactory(mockAppCheckProviderFactory); + + defaultFirebaseAppCheck.getLimitedUseAppCheckToken(); + + verify(mockAppCheckProvider).getToken(); + } + + @Test + public void testGetLimitedUseToken_noExistingToken_requestsNewToken() { + defaultFirebaseAppCheck.installAppCheckProviderFactory(mockAppCheckProviderFactory); + + defaultFirebaseAppCheck.getLimitedUseToken(); + + verify(mockAppCheckProvider).getToken(); + } + + @Test + public void testGetLimitedUseToken_existingToken_requestsNewToken() { + defaultFirebaseAppCheck.setCachedToken(validDefaultAppCheckToken); + defaultFirebaseAppCheck.installAppCheckProviderFactory(mockAppCheckProviderFactory); + + defaultFirebaseAppCheck.getLimitedUseToken(); + + verify(mockAppCheckProvider).getToken(); + } } diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultTokenRefresherTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultTokenRefresherTest.java index 67b7fd5805b..0a3f31ddb3f 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultTokenRefresherTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultTokenRefresherTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.when; import com.google.android.gms.tasks.Tasks; +import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.appcheck.AppCheckToken; import java.util.concurrent.ScheduledExecutorService; import org.junit.Before; @@ -33,8 +34,10 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.LooperMode; @RunWith(RobolectricTestRunner.class) +@LooperMode(LooperMode.Mode.LEGACY) public class DefaultTokenRefresherTest { private static final long TIME_TO_REFRESH_MILLIS = 1000L; @@ -42,7 +45,6 @@ public class DefaultTokenRefresherTest { private static final long TWO_MINUTES_SECONDS = 2L * 60L; private static final long FOUR_MINUTES_SECONDS = 4L * 60L; private static final long EIGHT_MINUTES_SECONDS = 8L * 60L; - private static final String ERROR = "error"; @Mock DefaultFirebaseAppCheck mockFirebaseAppCheck; @Mock ScheduledExecutorService mockScheduledExecutorService; @@ -57,8 +59,10 @@ public void setUp() { when(mockFirebaseAppCheck.fetchTokenFromProvider()) .thenReturn(Tasks.forResult(mockAppCheckToken)); + // TODO(b/258273630): Use TestOnlyExecutors. defaultTokenRefresher = - new DefaultTokenRefresher(mockFirebaseAppCheck, mockScheduledExecutorService); + new DefaultTokenRefresher( + mockFirebaseAppCheck, MoreExecutors.directExecutor(), mockScheduledExecutorService); } @Test diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java index 3d9c5965480..852a6f0e6d0 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java @@ -15,8 +15,8 @@ package com.google.firebase.appcheck.internal; import static com.google.common.truth.Truth.assertThat; -import static com.google.firebase.appcheck.internal.AppCheckTokenResponse.ATTESTATION_TOKEN_KEY; import static com.google.firebase.appcheck.internal.AppCheckTokenResponse.TIME_TO_LIVE_KEY; +import static com.google.firebase.appcheck.internal.AppCheckTokenResponse.TOKEN_KEY; import static com.google.firebase.appcheck.internal.HttpErrorResponse.CODE_KEY; import static com.google.firebase.appcheck.internal.HttpErrorResponse.ERROR_KEY; import static com.google.firebase.appcheck.internal.HttpErrorResponse.MESSAGE_KEY; @@ -65,17 +65,20 @@ public class NetworkClientTest { .setApplicationId(APP_ID) .setProjectId(PROJECT_ID) .build(); - private static final String SAFETY_NET_EXPECTED_URL = - "https://firebaseappcheck.googleapis.com/v1beta/projects/projectId/apps/appId:exchangeSafetyNetToken?key=apiKey"; private static final String DEBUG_EXPECTED_URL = - "https://firebaseappcheck.googleapis.com/v1beta/projects/projectId/apps/appId:exchangeDebugToken?key=apiKey"; + "https://firebaseappcheck.googleapis.com/v1/projects/projectId/apps/appId:exchangeDebugToken?key=apiKey"; + private static final String PLAY_INTEGRITY_CHALLENGE_EXPECTED_URL = + "https://firebaseappcheck.googleapis.com/v1/projects/projectId/apps/appId:generatePlayIntegrityChallenge?key=apiKey"; + private static final String PLAY_INTEGRITY_EXCHANGE_EXPECTED_URL = + "https://firebaseappcheck.googleapis.com/v1/projects/projectId/apps/appId:exchangePlayIntegrityToken?key=apiKey"; private static final String JSON_REQUEST = "jsonRequest"; private static final int SUCCESS_CODE = 200; private static final int ERROR_CODE = 404; - private static final String ATTESTATION_TOKEN = "token"; + private static final String APP_CHECK_TOKEN = "token"; private static final String TIME_TO_LIVE = "3600s"; private static final String ERROR_MESSAGE = "error message"; private static final String HEART_BEAT_HEADER_TEST = "test-header"; + private static final String CHALLENGE_RESPONSE = "challengeResponse"; @Mock HeartBeatController mockHeartBeatController; @Mock HttpURLConnection mockHttpUrlConnection; @@ -110,8 +113,7 @@ public void init_nullFirebaseApp_throwsException() { } @Test - public void exchangeSafetyNetToken_successResponse_returnsAppCheckTokenResponse() - throws Exception { + public void exchangeDebugToken_successResponse_returnsAppCheckTokenResponse() throws Exception { JSONObject responseBodyJson = createAttestationResponse(); when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream); @@ -121,11 +123,11 @@ public void exchangeSafetyNetToken_successResponse_returnsAppCheckTokenResponse( AppCheckTokenResponse tokenResponse = networkClient.exchangeAttestationForAppCheckToken( - JSON_REQUEST.getBytes(), NetworkClient.SAFETY_NET, mockRetryManager); - assertThat(tokenResponse.getAttestationToken()).isEqualTo(ATTESTATION_TOKEN); + JSON_REQUEST.getBytes(), NetworkClient.DEBUG, mockRetryManager); + assertThat(tokenResponse.getToken()).isEqualTo(APP_CHECK_TOKEN); assertThat(tokenResponse.getTimeToLive()).isEqualTo(TIME_TO_LIVE); - URL expectedUrl = new URL(SAFETY_NET_EXPECTED_URL); + URL expectedUrl = new URL(DEBUG_EXPECTED_URL); verify(networkClient).createHttpUrlConnection(expectedUrl); verify(mockOutputStream) .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length); @@ -135,7 +137,7 @@ public void exchangeSafetyNetToken_successResponse_returnsAppCheckTokenResponse( } @Test - public void exchangeSafetyNetToken_errorResponse_throwsException() throws Exception { + public void exchangeDebugToken_errorResponse_throwsException() throws Exception { JSONObject responseBodyJson = createHttpErrorResponse(); when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream); @@ -148,10 +150,10 @@ public void exchangeSafetyNetToken_errorResponse_throwsException() throws Except FirebaseException.class, () -> networkClient.exchangeAttestationForAppCheckToken( - JSON_REQUEST.getBytes(), NetworkClient.SAFETY_NET, mockRetryManager)); + JSON_REQUEST.getBytes(), NetworkClient.DEBUG, mockRetryManager)); assertThat(exception.getMessage()).contains(ERROR_MESSAGE); - URL expectedUrl = new URL(SAFETY_NET_EXPECTED_URL); + URL expectedUrl = new URL(DEBUG_EXPECTED_URL); verify(networkClient).createHttpUrlConnection(expectedUrl); verify(mockOutputStream) .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length); @@ -161,7 +163,8 @@ public void exchangeSafetyNetToken_errorResponse_throwsException() throws Except } @Test - public void exchangeDebugToken_successResponse_returnsAppCheckTokenResponse() throws Exception { + public void exchangePlayIntegrityToken_successResponse_returnsAppCheckTokenResponse() + throws Exception { JSONObject responseBodyJson = createAttestationResponse(); when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream); @@ -171,11 +174,11 @@ public void exchangeDebugToken_successResponse_returnsAppCheckTokenResponse() th AppCheckTokenResponse tokenResponse = networkClient.exchangeAttestationForAppCheckToken( - JSON_REQUEST.getBytes(), NetworkClient.DEBUG, mockRetryManager); - assertThat(tokenResponse.getAttestationToken()).isEqualTo(ATTESTATION_TOKEN); + JSON_REQUEST.getBytes(), NetworkClient.PLAY_INTEGRITY, mockRetryManager); + assertThat(tokenResponse.getToken()).isEqualTo(APP_CHECK_TOKEN); assertThat(tokenResponse.getTimeToLive()).isEqualTo(TIME_TO_LIVE); - URL expectedUrl = new URL(DEBUG_EXPECTED_URL); + URL expectedUrl = new URL(PLAY_INTEGRITY_EXCHANGE_EXPECTED_URL); verify(networkClient).createHttpUrlConnection(expectedUrl); verify(mockOutputStream) .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length); @@ -185,7 +188,7 @@ public void exchangeDebugToken_successResponse_returnsAppCheckTokenResponse() th } @Test - public void exchangeDebugToken_errorResponse_throwsException() throws Exception { + public void exchangePlayIntegrityToken_errorResponse_throwsException() throws Exception { JSONObject responseBodyJson = createHttpErrorResponse(); when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream); @@ -198,10 +201,10 @@ public void exchangeDebugToken_errorResponse_throwsException() throws Exception FirebaseException.class, () -> networkClient.exchangeAttestationForAppCheckToken( - JSON_REQUEST.getBytes(), NetworkClient.DEBUG, mockRetryManager)); + JSON_REQUEST.getBytes(), NetworkClient.PLAY_INTEGRITY, mockRetryManager)); assertThat(exception.getMessage()).contains(ERROR_MESSAGE); - URL expectedUrl = new URL(DEBUG_EXPECTED_URL); + URL expectedUrl = new URL(PLAY_INTEGRITY_EXCHANGE_EXPECTED_URL); verify(networkClient).createHttpUrlConnection(expectedUrl); verify(mockOutputStream) .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length); @@ -210,6 +213,18 @@ public void exchangeDebugToken_errorResponse_throwsException() throws Exception verifyRequestHeaders(); } + @Test + public void exchangeUnknownAttestation_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + networkClient.exchangeAttestationForAppCheckToken( + JSON_REQUEST.getBytes(), NetworkClient.UNKNOWN, mockRetryManager)); + + verify(mockRetryManager, never()).updateBackoffOnFailure(anyInt()); + verify(mockRetryManager, never()).resetBackoffOnSuccess(); + } + @Test public void exchangeAttestation_heartbeatNone_doesNotAttachHeader() throws Exception { JSONObject responseBodyJson = createAttestationResponse(); @@ -220,33 +235,83 @@ public void exchangeAttestation_heartbeatNone_doesNotAttachHeader() throws Excep when(mockHttpUrlConnection.getResponseCode()).thenReturn(SUCCESS_CODE); // The heartbeat request header should not be attached when the heartbeat is HeartBeat.NONE. networkClient.exchangeAttestationForAppCheckToken( - JSON_REQUEST.getBytes(), NetworkClient.SAFETY_NET, mockRetryManager); + JSON_REQUEST.getBytes(), NetworkClient.DEBUG, mockRetryManager); verifyRequestHeaders(); } @Test - public void exchangeUnknownAttestation_throwsException() { - assertThrows( - IllegalArgumentException.class, - () -> - networkClient.exchangeAttestationForAppCheckToken( - JSON_REQUEST.getBytes(), NetworkClient.UNKNOWN, mockRetryManager)); + public void exchangeAttestation_cannotRetry_throwsException() { + when(mockRetryManager.canRetry()).thenReturn(false); + + FirebaseException exception = + assertThrows( + FirebaseException.class, + () -> + networkClient.exchangeAttestationForAppCheckToken( + JSON_REQUEST.getBytes(), NetworkClient.DEBUG, mockRetryManager)); + assertThat(exception.getMessage()).contains("Too many attempts"); verify(mockRetryManager, never()).updateBackoffOnFailure(anyInt()); verify(mockRetryManager, never()).resetBackoffOnSuccess(); } @Test - public void exchangeAttestation_cannotRetry_throwsException() { + public void generatePlayIntegrityChallenge_successResponse_returnsJsonString() throws Exception { + when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream); + when(mockHttpUrlConnection.getInputStream()) + .thenReturn(new ByteArrayInputStream(CHALLENGE_RESPONSE.getBytes())); + when(mockHttpUrlConnection.getResponseCode()).thenReturn(SUCCESS_CODE); + + String challengeResponse = + networkClient.generatePlayIntegrityChallenge(JSON_REQUEST.getBytes(), mockRetryManager); + assertThat(challengeResponse).isEqualTo(CHALLENGE_RESPONSE); + + URL expectedUrl = new URL(PLAY_INTEGRITY_CHALLENGE_EXPECTED_URL); + verify(networkClient).createHttpUrlConnection(expectedUrl); + verify(mockOutputStream) + .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length); + verify(mockRetryManager, never()).updateBackoffOnFailure(anyInt()); + verify(mockRetryManager, never()).resetBackoffOnSuccess(); + verifyRequestHeaders(); + } + + @Test + public void generatePlayIntegrityChallenge_errorResponse_throwsException() throws Exception { + JSONObject responseBodyJson = createHttpErrorResponse(); + + when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream); + when(mockHttpUrlConnection.getErrorStream()) + .thenReturn(new ByteArrayInputStream(responseBodyJson.toString().getBytes())); + when(mockHttpUrlConnection.getResponseCode()).thenReturn(ERROR_CODE); + + FirebaseException exception = + assertThrows( + FirebaseException.class, + () -> + networkClient.generatePlayIntegrityChallenge( + JSON_REQUEST.getBytes(), mockRetryManager)); + + assertThat(exception.getMessage()).contains(ERROR_MESSAGE); + URL expectedUrl = new URL(PLAY_INTEGRITY_CHALLENGE_EXPECTED_URL); + verify(networkClient).createHttpUrlConnection(expectedUrl); + verify(mockOutputStream) + .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length); + verify(mockRetryManager).updateBackoffOnFailure(ERROR_CODE); + verify(mockRetryManager, never()).resetBackoffOnSuccess(); + verifyRequestHeaders(); + } + + @Test + public void generatePlayIntegrityChallenge_cannotRetry_throwsException() { when(mockRetryManager.canRetry()).thenReturn(false); FirebaseException exception = assertThrows( FirebaseException.class, () -> - networkClient.exchangeAttestationForAppCheckToken( - JSON_REQUEST.getBytes(), NetworkClient.DEBUG, mockRetryManager)); + networkClient.generatePlayIntegrityChallenge( + JSON_REQUEST.getBytes(), mockRetryManager)); assertThat(exception.getMessage()).contains("Too many attempts"); verify(mockRetryManager, never()).updateBackoffOnFailure(anyInt()); @@ -264,7 +329,7 @@ private void verifyRequestHeaders() { private static JSONObject createAttestationResponse() throws Exception { JSONObject responseBodyJson = new JSONObject(); - responseBodyJson.put(ATTESTATION_TOKEN_KEY, ATTESTATION_TOKEN); + responseBodyJson.put(TOKEN_KEY, APP_CHECK_TOKEN); responseBodyJson.put(TIME_TO_LIVE_KEY, TIME_TO_LIVE); return responseBodyJson; diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/RetryManagerTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/RetryManagerTest.java index 92c87f70c91..aaaf6750662 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/RetryManagerTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/RetryManagerTest.java @@ -16,7 +16,6 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.firebase.appcheck.internal.RetryManager.BAD_REQUEST_ERROR_CODE; -import static com.google.firebase.appcheck.internal.RetryManager.FORBIDDEN_ERROR_CODE; import static com.google.firebase.appcheck.internal.RetryManager.MAX_EXP_BACKOFF_MILLIS; import static com.google.firebase.appcheck.internal.RetryManager.NOT_FOUND_ERROR_CODE; import static com.google.firebase.appcheck.internal.RetryManager.ONE_DAY_MILLIS; @@ -60,14 +59,6 @@ public void updateBackoffOnFailure_badRequestError_oneDayRetryStrategy() { .isEqualTo(CURRENT_TIME_MILLIS + ONE_DAY_MILLIS); } - @Test - public void updateBackoffOnFailure_forbiddenError_oneDayRetryStrategy() { - retryManager.updateBackoffOnFailure(FORBIDDEN_ERROR_CODE); - - assertThat(retryManager.getNextRetryTimeMillis()) - .isEqualTo(CURRENT_TIME_MILLIS + ONE_DAY_MILLIS); - } - @Test public void updateBackoffOnFailure_notFoundError_oneDayRetryStrategy() { retryManager.updateBackoffOnFailure(NOT_FOUND_ERROR_CODE); @@ -78,9 +69,9 @@ public void updateBackoffOnFailure_notFoundError_oneDayRetryStrategy() { @Test public void updateBackoffOnFailure_oneDayRetryStrategy_multipleRetries() { - retryManager.updateBackoffOnFailure(FORBIDDEN_ERROR_CODE); - retryManager.updateBackoffOnFailure(FORBIDDEN_ERROR_CODE); - retryManager.updateBackoffOnFailure(FORBIDDEN_ERROR_CODE); + retryManager.updateBackoffOnFailure(BAD_REQUEST_ERROR_CODE); + retryManager.updateBackoffOnFailure(BAD_REQUEST_ERROR_CODE); + retryManager.updateBackoffOnFailure(BAD_REQUEST_ERROR_CODE); // The backoff period should not increase for consecutive failed retries with the ONE_DAY // strategy. @@ -133,7 +124,7 @@ public void updateBackoffOnFailure_exponentialRetryStrategy() { @Test public void canRetry_beforeNextRetryTime() { - retryManager.updateBackoffOnFailure(FORBIDDEN_ERROR_CODE); + retryManager.updateBackoffOnFailure(BAD_REQUEST_ERROR_CODE); // Sanity check. assertThat(mockClock.currentTimeMillis()).isEqualTo(CURRENT_TIME_MILLIS); @@ -145,7 +136,7 @@ public void canRetry_beforeNextRetryTime() { @Test public void canRetry_afterNextRetryTime() { - retryManager.updateBackoffOnFailure(FORBIDDEN_ERROR_CODE); + retryManager.updateBackoffOnFailure(BAD_REQUEST_ERROR_CODE); long nextRetryMillis = retryManager.getNextRetryTimeMillis(); when(mockClock.currentTimeMillis()).thenReturn(nextRetryMillis + 1); @@ -154,7 +145,7 @@ public void canRetry_afterNextRetryTime() { @Test public void resetBackoffOnSuccess() { - retryManager.updateBackoffOnFailure(FORBIDDEN_ERROR_CODE); + retryManager.updateBackoffOnFailure(BAD_REQUEST_ERROR_CODE); // Sanity check. assertThat(retryManager.getNextRetryTimeMillis()) .isEqualTo(CURRENT_TIME_MILLIS + ONE_DAY_MILLIS); diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/StorageHelperTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/StorageHelperTest.java index d57a5fcfd46..fd135c6ca13 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/StorageHelperTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/StorageHelperTest.java @@ -40,6 +40,7 @@ public class StorageHelperTest { private static final long RECEIVED_AT_TIMESTAMP = 10L; private static final long IAT = 10L; private static final long EXP = 30L; + private static final long ONE_SECOND_MILLIS = 1000L; private static final String TOKEN_PREFIX = "prefix"; private static final String TOKEN_SUFFIX = "suffix"; private static final String SEPARATOR = "."; @@ -108,8 +109,8 @@ public void testSaveAndRetrieveToken_TestAppCheckToken_expectEquivalentToken() t (DefaultAppCheckToken) storageHelper.retrieveAppCheckToken(); assertThat(retrievedToken).isNotNull(); assertThat(retrievedToken.getToken()).isEqualTo(rawToken); - assertThat(retrievedToken.getExpiresInMillis()).isEqualTo(EXP - IAT); - assertThat(retrievedToken.getReceivedAtTimestamp()).isEqualTo(IAT); + assertThat(retrievedToken.getExpiresInMillis()).isEqualTo((EXP - IAT) * ONE_SECOND_MILLIS); + assertThat(retrievedToken.getReceivedAtTimestamp()).isEqualTo(IAT * ONE_SECOND_MILLIS); } @Test diff --git a/appcheck/firebase-appcheck/test-app/src/main/AndroidManifest.xml b/appcheck/firebase-appcheck/test-app/src/main/AndroidManifest.xml index fbb5ebb6240..95347824781 100644 --- a/appcheck/firebase-appcheck/test-app/src/main/AndroidManifest.xml +++ b/appcheck/firebase-appcheck/test-app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ - + diff --git a/appcheck/firebase-appcheck/test-app/src/main/java/com/googletest/firebase/appcheck/MainActivity.java b/appcheck/firebase-appcheck/test-app/src/main/java/com/googletest/firebase/appcheck/MainActivity.java index 41299c2276d..1be95660b8b 100644 --- a/appcheck/firebase-appcheck/test-app/src/main/java/com/googletest/firebase/appcheck/MainActivity.java +++ b/appcheck/firebase-appcheck/test-app/src/main/java/com/googletest/firebase/appcheck/MainActivity.java @@ -30,7 +30,7 @@ import com.google.firebase.appcheck.FirebaseAppCheck; import com.google.firebase.appcheck.FirebaseAppCheck.AppCheckListener; import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory; -import com.google.firebase.appcheck.safetynet.SafetyNetAppCheckProviderFactory; +import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory; import com.google.firebase.storage.FirebaseStorage; import com.google.firebase.storage.ListResult; import com.google.firebase.storage.StorageReference; @@ -42,9 +42,10 @@ public class MainActivity extends AppCompatActivity { private FirebaseAppCheck firebaseAppCheck; private FirebaseStorage firebaseStorage; private AppCheckListener appCheckListener; - private Button installSafetyNetButton; + private Button installPlayIntegrityButton; private Button installDebugButton; private Button getAppCheckTokenButton; + private Button getLimitedUseTokenButton; private Button listStorageFilesButton; @Override @@ -72,7 +73,7 @@ private void initFirebase() { new AppCheckListener() { @Override public void onAppCheckTokenChanged(@NonNull AppCheckToken token) { - Log.d(TAG, "onAppCheckTokenChanged"); + Log.d(TAG, "onAppCheckTokenChanged: " + token.getToken()); } }; @@ -80,23 +81,24 @@ public void onAppCheckTokenChanged(@NonNull AppCheckToken token) { } private void initViews() { - installSafetyNetButton = findViewById(R.id.install_safety_net_app_check_button); + installPlayIntegrityButton = findViewById(R.id.install_play_integrity_app_check_button); installDebugButton = findViewById(R.id.install_debug_app_check_button); getAppCheckTokenButton = findViewById(R.id.exchange_app_check_button); + getLimitedUseTokenButton = findViewById(R.id.limited_use_app_check_button); listStorageFilesButton = findViewById(R.id.storage_list_files_button); setOnClickListeners(); } private void setOnClickListeners() { - installSafetyNetButton.setOnClickListener( + installPlayIntegrityButton.setOnClickListener( new OnClickListener() { @Override public void onClick(View v) { firebaseAppCheck.installAppCheckProviderFactory( - SafetyNetAppCheckProviderFactory.getInstance()); - Log.d(TAG, "Installed SafetyNetAppCheckProvider"); - showToast("Installed SafetyNetAppCheckProvider."); + PlayIntegrityAppCheckProviderFactory.getInstance()); + Log.d(TAG, "Installed PlayIntegrityAppCheckProvider"); + showToast("Installed PlayIntegrityAppCheckProvider."); } }); @@ -120,20 +122,55 @@ public void onClick(View v) { new OnSuccessListener() { @Override public void onSuccess(AppCheckToken appCheckToken) { - Log.d(TAG, "Successfully retrieved AppCheck token."); - showToast("Successfully retrieved AppCheck token."); + // Note: Logging App Check tokens is bad practice and should NEVER be done in a + // production application. We log the token here in our unpublished test + // application for easier debugging. + Log.d( + TAG, "Successfully retrieved App Check token: " + appCheckToken.getToken()); + showToast("Successfully retrieved App Check token."); } }); task.addOnFailureListener( new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { - Log.d(TAG, "AppCheck token exchange failed with error: " + e.getMessage()); - showToast("AppCheck token exchange failed."); + Log.d(TAG, "App Check token exchange failed with error: " + e.getMessage()); + showToast("App Check token exchange failed."); } }); } }); + + getLimitedUseTokenButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + Task task = firebaseAppCheck.getLimitedUseAppCheckToken(); + task.addOnSuccessListener( + new OnSuccessListener() { + @Override + public void onSuccess(AppCheckToken appCheckToken) { + // Note: Logging App Check tokens is bad practice and should NEVER be done in a + // production application. We log the token here in our unpublished test + // application for easier debugging. + Log.d( + TAG, + "Successfully retrieved limited-use App Check token: " + + appCheckToken.getToken()); + showToast("Successfully retrieved limited-use App Check token."); + } + }); + task.addOnFailureListener( + new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + Log.d(TAG, "App Check token exchange failed with error: " + e.getMessage()); + showToast("App Check token exchange failed."); + } + }); + } + }); + listStorageFilesButton.setOnClickListener( new OnClickListener() { @Override diff --git a/appcheck/firebase-appcheck/test-app/src/main/res/layout/activity_main.xml b/appcheck/firebase-appcheck/test-app/src/main/res/layout/activity_main.xml index eccdbf9ec6e..cd2dff67e27 100644 --- a/appcheck/firebase-appcheck/test-app/src/main/res/layout/activity_main.xml +++ b/appcheck/firebase-appcheck/test-app/src/main/res/layout/activity_main.xml @@ -6,6 +6,11 @@ android:gravity="center" android:orientation="vertical"> +